Code edit (18 edits merged)
Please save this source code
User prompt
Please fix the bug: 'backgroundLayer is not defined' in or related to this line: 'game.addChild(backgroundLayer);' Line Number: 581
Code edit (1 edits merged)
Please save this source code
User prompt
update as needed with: self.update = function (playerX, playerY, playerDir) { // ... // ✅ NEW: Create fresh sprites each time var floorSprite = LK.getAsset('floorTile', { anchorX: 0, anchorY: 0 }); var ceilingSprite = LK.getAsset('ceilingTile', { anchorX: 0, anchorY: 0 }); // Set position and size floorSprite.x = x; floorSprite.width = stepSize; floorSprite.height = 2; ceilingSprite.x = x; ceilingSprite.width = stepSize; ceilingSprite.height = 2; // Apply distance-based shading var shade = Math.max(0.3, 1 - realDistance / MAX_RENDER_DISTANCE); floorSprite.alpha = shade * 0.8; ceilingSprite.alpha = shade * 0.7; // Add to appropriate row floorRows[y].addChild(floorSprite); ceilingRows[y].addChild(ceilingSprite);
User prompt
update with: // Apply distance-based shading (increased for visibility) var shade = Math.max(0.5, 1 - realDistance / MAX_RENDER_DISTANCE); floorSprite.alpha = shade * 1.0; // Full alpha for testing ceilingSprite.alpha = shade * 1.0; // Full alpha for testing
User prompt
update with: // Add floor and ceiling tiles var floorSprite = floorTexture.clone(); floorSprite.x = x; floorSprite.width = stepSize; floorSprite.height = 2; // Apply texture offset floorSprite.texture.frame.x = tx; floorSprite.texture.frame.y = ty; var ceilingSprite = ceilingTexture.clone(); ceilingSprite.x = x; ceilingSprite.width = stepSize; ceilingSprite.height = 2; // Apply texture offset ceilingSprite.texture.frame.x = tx; ceilingSprite.texture.frame.y = ty;
Code edit (8 edits merged)
Please save this source code
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle.
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle. Make sure we don’t crash the game.
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle.
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle.
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle.
User prompt
Create a completely new system for the floor and ceiling: These surfaces aren’t rendered by raycasting; they need floor casting, which projects each screen row into the world based on player position and viewing angle.
User prompt
Design a new system for floor and ceiling casting using something like: for (let y = screenMiddle + 1; y < screenHeight; y++) { let rayDistance = screenHeight / (2.0 * y - screenHeight); let floorX = playerX + rayDistance * cosRayAngle; let floorY = playerY + rayDistance * sinRayAngle; let tileX = Math.floor(floorX); let tileY = Math.floor(floorY); let texX = Math.floor((floorX - tileX) * textureWidth); let texY = Math.floor((floorY - tileY) * textureHeight); // Draw pixel at (x, y) with texture[texX][texY] }
User prompt
Please fix the bug: 'ReferenceError: Can't find variable: floorLayer' in or related to this line: 'floorLayer.addChild(floorTexture);' Line Number: 942
User prompt
Here’s how I want to handle the floors and ceilings: design a system like: for (let y = screenMiddle + 1; y < screenHeight; y++) { let rayDistance = screenHeight / (2.0 * y - screenHeight); let floorX = playerX + rayDistance * cosRayAngle; let floorY = playerY + rayDistance * sinRayAngle; let tileX = Math.floor(floorX); let tileY = Math.floor(floorY); let texX = Math.floor((floorX - tileX) * textureWidth); let texY = Math.floor((floorY - tileY) * textureHeight); // Draw pixel at (x, y) with texture[texX][texY] }
User prompt
Adjust texture mapping using slices of the asset tile to reassemble the asset in its original form
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'wall.texture.height')' in or related to this line: 'wall.crop = new Rectangle(textureOffset, 0, textureStripWidth, wall.texture.height);' Line Number: 424
User prompt
Add texture mapping to the walls.
User prompt
Design a complete refactor for the raycasting system to use tiles for the walls, floors and ceiling to better represent a faux 3D environment.
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
Projectiles are spawning right at screen center when they should spawn from the the hand position.
User prompt
Lower the Y spawn position of projectiles by 10%
User prompt
Slow down projectile speed by a quarter.
/**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/**** 
* Classes
****/ 
var ControlButton = Container.expand(function (direction) {
	var self = Container.call(this);
	var buttonSprite = self.attachAsset('controlButton', {
		anchorX: 0.5,
		anchorY: 0.5,
		scaleX: 2.5,
		scaleY: 2.5
	});
	var arrowSprite = self.attachAsset('buttonArrow', {
		anchorX: 0.5,
		anchorY: 0.5,
		scaleX: 2.0,
		scaleY: 2.0
	});
	// Set arrow direction based on button type
	if (direction === 'up') {
		arrowSprite.rotation = 0;
	} else if (direction === 'right') {
		arrowSprite.rotation = Math.PI / 2;
	} else if (direction === 'down') {
		arrowSprite.rotation = Math.PI;
	} else if (direction === 'left') {
		arrowSprite.rotation = Math.PI * 1.5;
	} else if (direction === 'attack') {
		arrowSprite.visible = false;
		buttonSprite = self.attachAsset('attackButton', {
			anchorX: 0.5,
			anchorY: 0.5,
			scaleX: 3.5,
			scaleY: 3.5
		});
	}
	self.direction = direction;
	self.pressed = false;
	self.updateCooldown = function (ratio) {
		if (self.direction === 'attack') {
			// Update the button alpha based on cooldown ratio (0 to 1)
			buttonSprite.alpha = 0.3 + ratio * 0.7;
		}
	};
	self.down = function (x, y, obj) {
		self.pressed = true;
		buttonSprite.alpha = 0.7;
		if (self.direction === 'attack' && canAttack) {
			// Stop propagation to prevent the event from affecting the joystick
			obj.stopPropagation = true;
			// If joystick is active, store its current state for override
			if (controlButtons && controlButtons.joystick && controlButtons.joystick.active) {
				joystickOverrideActive = true;
				joystickOverrideX = controlButtons.joystick.normalizedX;
				joystickOverrideY = controlButtons.joystick.normalizedY;
			} else {
				// Ensure override is not active if joystick wasn't active at press time
				joystickOverrideActive = false;
			}
			attackAction();
		}
	};
	self.up = function (x, y, obj) {
		self.pressed = false;
		if (self.direction === 'attack') {
			// Clear joystick override when attack button is released
			joystickOverrideActive = false;
			if (!canAttack) {
				buttonSprite.alpha = 0.3; // Show as disabled
			} else {
				buttonSprite.alpha = 1;
			}
		} else {
			buttonSprite.alpha = 1;
		}
	};
	return self;
});
var Gate = Container.expand(function () {
	var self = Container.call(this);
	// Create gate visual using existing wall asset but with a different color
	var gateSprite = self.attachAsset('wall', {
		anchorX: 0.5,
		anchorY: 0.5
	});
	// Make the gate distinct with a green tint
	gateSprite.tint = 0x00FF00;
	self.mapX = 0;
	self.mapY = 0;
	// Pulse animation to make gate more visible
	var _animateGate = function animateGate() {
		tween(gateSprite, {
			alpha: 0.7,
			scaleX: 1.1,
			scaleY: 1.1
		}, {
			duration: 1000,
			onFinish: function onFinish() {
				tween(gateSprite, {
					alpha: 1,
					scaleX: 1.0,
					scaleY: 1.0
				}, {
					duration: 1000,
					onFinish: _animateGate
				});
			}
		});
	};
	// Start the pulsing animation
	_animateGate();
	return self;
});
var JoystickController = Container.expand(function () {
	var self = Container.call(this);
	// Create joystick base
	var baseRadius = 150;
	var baseSprite = self.attachAsset('controlButton', {
		anchorX: 0.5,
		anchorY: 0.5,
		scaleX: 5.0,
		scaleY: 5.0,
		alpha: 0.4
	});
	// Create joystick handle
	var handleRadius = 100;
	var handleSprite = self.attachAsset('controlButton', {
		anchorX: 0.5,
		anchorY: 0.5,
		scaleX: 3.0,
		scaleY: 3.0
	});
	// Initialize variables
	self.active = false;
	self.startX = 0;
	self.startY = 0;
	self.maxDistance = baseRadius;
	self.normalizedX = 0;
	self.normalizedY = 0;
	// Reset the joystick handle position
	self.resetHandle = function () {
		handleSprite.x = 0;
		handleSprite.y = 0;
		self.normalizedX = 0;
		self.normalizedY = 0;
		self.active = false;
	};
	// Handle touch down event
	self.down = function (x, y, obj) {
		self.active = true;
		self.startX = x;
		self.startY = y;
	};
	// Handle touch move event
	self.move = function (x, y, obj) {
		if (!self.active) {
			return;
		}
		// Calculate distance from start position
		var dx = x - self.startX;
		var dy = y - self.startY;
		var distance = Math.sqrt(dx * dx + dy * dy);
		// Normalize the distance to get direction vector
		if (distance > 0) {
			// Clamp to max distance
			if (distance > self.maxDistance) {
				dx = dx * self.maxDistance / distance;
				dy = dy * self.maxDistance / distance;
				distance = self.maxDistance;
			}
			// Set handle position
			handleSprite.x = dx;
			handleSprite.y = dy;
			// Calculate normalized values (-1 to 1)
			self.normalizedX = dx / self.maxDistance;
			self.normalizedY = dy / self.maxDistance;
		} else {
			self.resetHandle();
		}
	};
	// Handle touch up event
	self.up = function (x, y, obj) {
		self.resetHandle();
	};
	// Initialize with handle at center
	self.resetHandle();
	return self;
});
var MapCell = Container.expand(function () {
	var self = Container.call(this);
	self.type = 0; // 0 = floor, 1 = wall
	self.monster = null;
	self.treasure = null;
	self.gate = null;
	self.setType = function (type) {
		self.type = type;
		self.updateVisual();
	};
	self.updateVisual = function () {
		self.removeChildren();
		if (self.type === 1) {
			self.attachAsset('mapWall', {
				anchorX: 0,
				anchorY: 0
			});
		} else {
			self.attachAsset('mapFloor', {
				anchorX: 0,
				anchorY: 0
			});
		}
	};
	self.addMonster = function () {
		if (self.type === 0 && !self.monster && !self.treasure) {
			self.monster = true;
			return true;
		}
		return false;
	};
	self.addTreasure = function () {
		if (self.type === 0 && !self.monster && !self.treasure) {
			self.treasure = true;
			return true;
		}
		return false;
	};
	self.removeMonster = function () {
		self.monster = null;
	};
	self.removeTreasure = function () {
		self.treasure = null;
	};
	self.addGate = function () {
		if (self.type === 0 && !self.monster && !self.treasure && !self.gate) {
			self.gate = true;
			return true;
		}
		return false;
	};
	self.removeGate = function () {
		self.gate = null;
	};
	return self;
});
var Monster = Container.expand(function () {
	var self = Container.call(this);
	var monsterSprite = self.attachAsset('monster', {
		anchorX: 0.5,
		anchorY: 0.5
	});
	self.mapX = 0;
	self.mapY = 0;
	self.health = 3;
	self.takeDamage = function () {
		self.health -= 1;
		LK.getSound('hit').play();
		// Visual feedback for hit
		tween(monsterSprite, {
			alpha: 0.2
		}, {
			duration: 100,
			onFinish: function onFinish() {
				tween(monsterSprite, {
					alpha: 1
				}, {
					duration: 100
				});
			}
		});
		return self.health <= 0;
	};
	return self;
});
var Projectile = Container.expand(function () {
	var self = Container.call(this);
	var projectileSprite = self.attachAsset('projectile', {
		anchorX: 0.5,
		anchorY: 0.5
	});
	// World position and movement properties
	self.worldX = 0;
	self.worldY = 0;
	self.dirX = 0;
	self.dirY = 0;
	self.speed = 0.0375; // World units per tick (reduced to 1/4 of original)
	self.active = false;
	self.distance = 0;
	self.maxDistance = 10; // Maximum travel distance in world units
	// Initialize projectile
	self.fire = function (screenX, screenY) {
		// Start at player position, slightly offset in the firing direction
		self.worldX = player.x + Math.cos(player.dir) * 0.3;
		self.worldY = player.y + Math.sin(player.dir) * 0.3;
		// Direction is player's current facing direction
		self.dirX = Math.cos(player.dir);
		self.dirY = Math.sin(player.dir);
		self.active = true;
		self.distance = 0;
		// Play attack sound
		LK.getSound('attack').play();
		// Scale for visual effect
		projectileSprite.scale.set(0.2, 0.2);
		// Store initial screen coordinates from hand
		self.initialScreenX = screenX || 2048 / 2;
		self.initialScreenY = screenY || 2732 - 300;
		// Set initial position to hand location
		self.x = self.initialScreenX;
		self.y = self.initialScreenY;
		self.visible = true;
	};
	// Update projectile position and check for collisions
	self.update = function (deltaTime) {
		if (!self.active) {
			return false;
		}
		// Move projectile in world space
		self.worldX += self.dirX * self.speed;
		self.worldY += self.dirY * self.speed;
		// Track distance traveled
		self.distance += self.speed;
		// Check for wall collision
		var mapX = Math.floor(self.worldX);
		var mapY = Math.floor(self.worldY);
		// If hit wall or traveled max distance
		if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE || map[mapY][mapX].type === 1 || self.distance >= self.maxDistance) {
			return true; // Remove projectile
		}
		// Position on screen based on player view (raycasting principles)
		self.updateScreenPosition();
		return false; // Keep projectile
	};
	// Calculate screen position from world position
	self.updateScreenPosition = function () {
		// Vector from player to projectile
		var dx = self.worldX - player.x;
		var dy = self.worldY - player.y;
		// Distance from player to projectile
		var dist = Math.sqrt(dx * dx + dy * dy);
		// Angle from player to projectile
		var angle = Math.atan2(dy, dx) - player.dir;
		// Normalize angle (-PI to PI)
		while (angle < -Math.PI) {
			angle += Math.PI * 2;
		}
		while (angle > Math.PI) {
			angle -= Math.PI * 2;
		}
		// Check if projectile is in field of view
		if (Math.abs(angle) < HALF_FOV) {
			// Calculate screen X based on angle in FOV
			self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2);
			// Calculate target Y position (center of screen with slight adjustment)
			var targetY = 2732 / 2 + 20 - WALL_HEIGHT_FACTOR / dist * 0.1;
			// Calculate transition factor based on distance
			// As distance increases, move closer to target Y
			var transitionFactor = Math.min(1.0, self.distance * 0.5);
			// Interpolate between initial hand Y and target Y
			self.y = self.initialScreenY * (1 - transitionFactor) + targetY * transitionFactor;
			// Scale based on distance
			var scale = Math.max(0.1, 2 / dist);
			projectileSprite.scale.set(scale, scale);
			self.visible = true;
		} else {
			self.visible = false;
		}
	};
	return self;
});
var RaycastStrip = Container.expand(function () {
	var self = Container.call(this);
	var wall = self.attachAsset('wall', {
		anchorX: 0,
		anchorY: 0
	});
	var ceiling = self.attachAsset('ceiling', {
		anchorX: 0,
		anchorY: 0
	});
	var floor = self.attachAsset('floor', {
		anchorX: 0,
		anchorY: 0
	});
	self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance) {
		// Wall setup
		wall.width = stripWidth;
		wall.height = wallHeight;
		wall.y = (2732 - wallHeight) / 2;
		// Ceiling setup
		ceiling.width = stripWidth;
		ceiling.height = wall.y;
		ceiling.y = 0;
		// Floor setup
		floor.width = stripWidth;
		floor.height = ceiling.height;
		floor.y = wall.y + wallHeight;
		// Adjust positions
		self.x = stripIdx * stripWidth;
		// Add distance shading effect
		var shade = Math.max(0.3, 1 - distance / 10);
		wall.alpha = shade;
		ceiling.alpha = shade * 0.7;
		floor.alpha = shade * 0.8;
	};
	return self;
});
var Treasure = Container.expand(function () {
	var self = Container.call(this);
	var treasureSprite = self.attachAsset('treasure', {
		anchorX: 0.5,
		anchorY: 0.5
	});
	self.mapX = 0;
	self.mapY = 0;
	self.value = 1;
	// Animate the treasure to make it more appealing
	var _animateTreasure = function animateTreasure() {
		// Animation removed to stop spinning
	};
	// No longer calling animation
	return self;
});
/**** 
* Initialize Game
****/ 
var game = new LK.Game({
	backgroundColor: 0x111111
});
/**** 
* Game Code
****/ 
// Game constants
var MAP_SIZE = 16;
var CELL_SIZE = 20;
var MINI_MAP_SCALE = 1;
var STRIP_WIDTH = 8;
var NUM_RAYS = Math.ceil(2048 / STRIP_WIDTH);
var FOV = Math.PI / 3; // 60 degrees field of view
var HALF_FOV = FOV / 2;
var PLAYER_MOVE_SPEED = 0.002; // Reduced from 0.005 to make movement slower
var PLAYER_TURN_SPEED = 0.002; // Reduced from 0.005 to make turning slower
var WALL_HEIGHT_FACTOR = 600;
var MAX_RENDER_DISTANCE = 16;
var MONSTER_COUNT = 5;
var TREASURE_COUNT = 10;
// Game state
var map = [];
var player = {
	x: 1.5,
	y: 1.5,
	dir: 0,
	health: 5,
	score: 0,
	level: 1
};
var controls = {
	forward: false,
	backward: false,
	left: false,
	right: false,
	attack: false
};
var monsters = [];
var treasures = [];
var projectiles = [];
var gate = null;
var lastTime = Date.now();
var canAttack = true;
var attackCooldown = 500; // 500 millisecond cooldown
// Joystick override state
var joystickOverrideActive = false;
var joystickOverrideX = 0;
var joystickOverrideY = 0;
// UI elements
var miniMap;
var rayCastView;
var healthText;
var scoreText;
var levelText;
var monsterText;
var controlButtons = {};
var playerMarker;
var globalRightHand; // Stores the right hand game object
var activeControlForGlobalHandlers = null; // Tracks which control is active for game.down/up/move
// Create layer variables at the global scope
var gameLayer = new Container();
var projectileLayer = new Container();
var handLayer = new Container();
// Setup game
function setupGame() {
	// Set up the layer system using the globally defined variables
	// Add layers in the correct order (projectiles below hand)
	game.addChild(gameLayer);
	game.addChild(projectileLayer);
	game.addChild(handLayer);
	// Create the rayCast view container
	rayCastView = new Container();
	gameLayer.addChild(rayCastView);
	// Create raycast strips
	for (var i = 0; i < NUM_RAYS; i++) {
		var strip = new RaycastStrip();
		rayCastView.addChild(strip);
	}
	// Create minimap container
	miniMap = new Container();
	miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2; // Center horizontally
	miniMap.y = 20; // Keep at top
	gameLayer.addChild(miniMap);
	// Generate map
	generateMap();
	// Create player marker
	playerMarker = gameLayer.addChild(LK.getAsset('player', {
		anchorX: 0.5,
		anchorY: 0.5
	}));
	// Create UI elements
	createUI();
	// Create control buttons
	createControlButtons();
	// Start background music
	LK.playMusic('dungeon');
}
function generateMap() {
	// Clear existing map
	miniMap.removeChildren();
	map = [];
	// Remove existing monsters and treasures
	for (var i = 0; i < monsters.length; i++) {
		monsters[i].destroy();
	}
	monsters = [];
	for (var i = 0; i < treasures.length; i++) {
		treasures[i].destroy();
	}
	treasures = [];
	// Clear projectiles
	for (var i = 0; i < projectiles.length; i++) {
		projectiles[i].destroy();
	}
	projectiles = [];
	// Generate base map with borders
	for (var y = 0; y < MAP_SIZE; y++) {
		map[y] = [];
		for (var x = 0; x < MAP_SIZE; x++) {
			var cell = new MapCell();
			cell.x = x * CELL_SIZE * MINI_MAP_SCALE;
			cell.y = y * CELL_SIZE * MINI_MAP_SCALE;
			// Create outer walls
			if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) {
				cell.setType(1); // Wall
			} else {
				// Random interior walls based on level difficulty
				var wallChance = 0.2 + player.level * 0.03;
				if (Math.random() < wallChance && !(x === 1 && y === 1)) {
					// Ensure starting position is clear
					cell.setType(1); // Wall
				} else {
					cell.setType(0); // Floor
				}
			}
			map[y][x] = cell;
			miniMap.addChild(cell);
		}
	}
	// Check map connectivity and fix walled-off areas
	ensureMapConnectivity();
	// Create monsters
	var monstersToPlace = MONSTER_COUNT + Math.floor(player.level * 0.5);
	for (var i = 0; i < monstersToPlace; i++) {
		placeMonster();
	}
	// Create treasures
	var treasuresToPlace = TREASURE_COUNT;
	for (var i = 0; i < treasuresToPlace; i++) {
		placeTreasure();
	}
	// Reset player position
	player.x = 1.5;
	player.y = 1.5;
	player.dir = 0;
	// Place exit gate
	gate = placeGate();
}
function placeMonster() {
	// Find a random empty cell
	var x, y;
	var attempts = 0;
	do {
		x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		attempts++;
		// Make sure it's not too close to the player
		var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
		if (attempts > 100) {
			break;
		} // Prevent infinite loop
	} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || distToPlayer < 3);
	if (attempts <= 100) {
		map[y][x].addMonster();
		var monster = new Monster();
		monster.mapX = x;
		monster.mapY = y;
		monster.health = 2 + Math.floor(player.level / 3); // Monsters get tougher with level
		monsters.push(monster);
		gameLayer.addChild(monster);
	}
}
function placeTreasure() {
	// Find a random empty cell
	var x, y;
	var attempts = 0;
	do {
		x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		attempts++;
		if (attempts > 100) {
			break;
		} // Prevent infinite loop
	} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || map[y][x].gate);
	if (attempts <= 100) {
		map[y][x].addTreasure();
		var treasure = new Treasure();
		treasure.mapX = x;
		treasure.mapY = y;
		treasure.value = 1 + Math.floor(Math.random() * player.level);
		treasures.push(treasure);
		gameLayer.addChild(treasure);
	}
}
function placeGate() {
	// Place gate in a random valid location
	var x, y;
	var attempts = 0;
	var validPositions = [];
	// Collect all valid positions first
	for (var i = 0; i < 100; i++) {
		x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
		// Check if the cell is suitable
		if (map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) {
			// Make sure it's not too close to the player (at least 2 cells away)
			var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
			if (distToPlayer > 2) {
				// Add to valid positions
				validPositions.push({
					x: x,
					y: y
				});
			}
		}
	}
	// If we found valid spots, choose one randomly
	if (validPositions.length > 0) {
		// Pick a random position from the valid ones
		var randomIndex = Math.floor(Math.random() * validPositions.length);
		var chosenPos = validPositions[randomIndex];
		var gateX = chosenPos.x;
		var gateY = chosenPos.y;
		// Place the gate
		map[gateY][gateX].addGate();
		var gate = new Gate();
		gate.mapX = gateX;
		gate.mapY = gateY;
		gameLayer.addChild(gate);
		return gate;
	}
	return null;
}
function createUI() {
	// Health display
	healthText = new Text2('Health: ' + player.health, {
		size: 40,
		fill: 0xFF5555
	});
	healthText.anchor.set(0, 0);
	LK.gui.topRight.addChild(healthText);
	healthText.x = -200;
	healthText.y = 20;
	// Score display
	scoreText = new Text2('Score: ' + player.score, {
		size: 40,
		fill: 0xFFFF55
	});
	scoreText.anchor.set(0, 0);
	LK.gui.topRight.addChild(scoreText);
	scoreText.x = -200;
	scoreText.y = 80;
	// Level display
	levelText = new Text2('Level: ' + player.level, {
		size: 40,
		fill: 0x55FF55
	});
	levelText.anchor.set(0, 0);
	LK.gui.topRight.addChild(levelText);
	levelText.x = -200;
	levelText.y = 140;
	// Monster counter display
	monsterText = new Text2('Monsters: 0', {
		size: 40,
		fill: 0xFF9955
	});
	monsterText.anchor.set(0, 0);
	LK.gui.topRight.addChild(monsterText);
	monsterText.x = -200;
	monsterText.y = 200;
	// Update UI displays
	updateUI();
}
function updateUI() {
	healthText.setText('Health: ' + player.health);
	scoreText.setText('Score: ' + player.score);
	levelText.setText('Level: ' + player.level);
	monsterText.setText('Monsters: ' + monsters.length);
	// Update score in LK system
	LK.setScore(player.score);
}
function createControlButtons() {
	// Create joystick control for movement
	controlButtons.joystick = new JoystickController();
	controlButtons.joystick.x = 400;
	controlButtons.joystick.y = 2732 - 500;
	gameLayer.addChild(controlButtons.joystick);
	// Using the global layer system - no need to recreate layers here
	// Create attack button more centered on the right side
	controlButtons.attack = new ControlButton('attack');
	controlButtons.attack.x = 2048 - 400;
	controlButtons.attack.y = 2732 - 500;
	gameLayer.addChild(controlButtons.attack);
	// Add right hand at the bottom right for projectile firing
	globalRightHand = LK.getAsset('rightHand', {
		anchorX: 0.5,
		anchorY: 0.9,
		scaleX: 1.2,
		scaleY: 1.2
	});
	globalRightHand.x = 2048 * 0.6; // Position just off right of middle
	globalRightHand.y = 2732; // Bottom of the screen
	handLayer.addChild(globalRightHand);
	// Add pulse and float animation to give the hand some life
	var _animateHand = function animateHand() {
		// Get a small random x offset between -40 and 40 pixels
		var randomXOffset = Math.random() * 80 - 40;
		// Store original X position if not set yet
		if (globalRightHand.originalX === undefined) {
			globalRightHand.originalX = globalRightHand.x;
		}
		// Slow scale pulse animation with random left/right movement
		tween(globalRightHand, {
			scaleX: 1.1,
			scaleY: 1.1,
			x: globalRightHand.originalX + randomXOffset,
			y: 2732 - 15 // Float up slightly from base position
		}, {
			duration: 1800,
			easing: tween.easeInOut,
			onFinish: function onFinish() {
				// Get another random x offset for the return animation
				var returnRandomXOffset = Math.random() * 40 - 20;
				tween(globalRightHand, {
					scaleX: 1.1,
					scaleY: 1.1,
					x: globalRightHand.originalX + returnRandomXOffset,
					y: 2732 // Return to original position
				}, {
					duration: 1800,
					easing: tween.easeInOut,
					onFinish: _animateHand
				});
			}
		});
	};
	// Start the hand animation
	_animateHand();
}
function rayCasting() {
	var rayAngle, distToWall, rayDirX, rayDirY, mapCheckX, mapCheckY;
	var distX, distY;
	var rayStartX = player.x;
	var rayStartY = player.y;
	// Floor and ceiling rendering using floor casting
	// We'll render the floor and ceiling for each row of the screen, for each ray
	// We'll use a single Container for the floor/ceiling layer, and draw quads (tiles) for each row/column
	if (!rayCastView.floorLayer) {
		rayCastView.floorLayer = new Container();
		rayCastView.addChildAt(rayCastView.floorLayer, 0); // Add below strips
	}
	var floorLayer = rayCastView.floorLayer;
	floorLayer.removeChildren(); // Clear previous frame
	// Precompute some values for efficiency
	var screenCenterY = 2732 / 2;
	var screenHeight = 2732;
	var screenWidth = 2048;
	var horizon = screenCenterY;
	var tileSize = 1; // 1 world unit per tile
	// For each row from the center to the bottom (floor) and from center to top (ceiling)
	for (var y = 0; y < screenHeight; y += 8) {
		// Perspective: distance from player to the row in world space
		// Formula: rowDistance = (player height * projection plane distance) / (row - screen center)
		// We'll use player height = 0.5, projection plane = screenCenterY
		var isFloor = y > horizon;
		var rowDist = isFloor ? 0.5 * screenCenterY / (y - horizon) : 0.5 * screenCenterY / (horizon - y);
		// For each column (ray)
		for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
			var rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
			var rayDirX = Math.cos(rayAngle);
			var rayDirY = Math.sin(rayAngle);
			// Calculate the world position for this pixel
			// CameraX: -1 (left) to 1 (right)
			var cameraX = 2 * rayIdx / NUM_RAYS - 1;
			// World coordinates for the floor/ceiling point
			var worldX = player.x + rowDist * rayDirX;
			var worldY = player.y + rowDist * rayDirY;
			// Snap to tile
			var mapX = Math.floor(worldX);
			var mapY = Math.floor(worldY);
			// Only draw if inside map
			if (mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE) {
				// Choose asset for floor/ceiling
				var assetId = isFloor ? 'floorTile' : 'ceilingTile';
				var tileSprite = LK.getAsset(assetId, {
					anchorX: 0.5,
					anchorY: 0.5,
					scaleX: 0.08,
					scaleY: 0.08
				});
				// Place on screen
				tileSprite.x = rayIdx * STRIP_WIDTH + STRIP_WIDTH / 2;
				tileSprite.y = y + 4; // Center of the row
				// Add distance shading
				var shade = Math.max(0.2, 1 - rowDist / 10);
				tileSprite.alpha = shade * (isFloor ? 0.9 : 0.7);
				floorLayer.addChild(tileSprite);
			}
		}
	}
	// Raycasting for walls (unchanged)
	for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
		// Calculate ray angle (center ray + offset based on ray index)
		rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
		// Get direction vector
		rayDirX = Math.cos(rayAngle);
		rayDirY = Math.sin(rayAngle);
		// Distance to wall
		distToWall = 0;
		hitWall = false;
		// Step size for ray casting
		var stepSizeX = Math.abs(1 / rayDirX);
		var stepSizeY = Math.abs(1 / rayDirY);
		// Which block we're checking
		mapCheckX = Math.floor(rayStartX);
		mapCheckY = Math.floor(rayStartY);
		// Length of ray from current position to next x or y-side
		var sideDistX, sideDistY;
		// Direction to step in x or y direction (either +1 or -1)
		var stepX = rayDirX >= 0 ? 1 : -1;
		var stepY = rayDirY >= 0 ? 1 : -1;
		// Calculate distance to first x and y side
		if (rayDirX < 0) {
			sideDistX = (rayStartX - mapCheckX) * stepSizeX;
		} else {
			sideDistX = (mapCheckX + 1.0 - rayStartX) * stepSizeX;
		}
		if (rayDirY < 0) {
			sideDistY = (rayStartY - mapCheckY) * stepSizeY;
		} else {
			sideDistY = (mapCheckY + 1.0 - rayStartY) * stepSizeY;
		}
		// Perform DDA (Digital Differential Analysis)
		var hit = false;
		var side = 0; // 0 for x-side, 1 for y-side
		var maxDistance = MAX_RENDER_DISTANCE;
		while (!hit && distToWall < maxDistance) {
			// Jump to next map square
			if (sideDistX < sideDistY) {
				sideDistX += stepSizeX;
				mapCheckX += stepX;
				side = 0;
			} else {
				sideDistY += stepSizeY;
				mapCheckY += stepY;
				side = 1;
			}
			// Check if ray has hit a wall
			if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) {
				hit = true;
				distToWall = maxDistance;
			} else if (map[mapCheckY][mapCheckX].type === 1) {
				hit = true;
				// Calculate exact distance to avoid fisheye effect
				if (side === 0) {
					distToWall = (mapCheckX - rayStartX + (1 - stepX) / 2) / rayDirX;
				} else {
					distToWall = (mapCheckY - rayStartY + (1 - stepY) / 2) / rayDirY;
				}
			}
		}
		// Calculate height of wall based on distance
		var wallHeight = Math.min(2732, WALL_HEIGHT_FACTOR / distToWall);
		// Update the strip
		var strip = rayCastView.children[rayIdx];
		strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, 1, distToWall);
	}
	// Render monsters and treasures
	renderEntities();
}
function renderEntities() {
	// First, hide all entities
	for (var i = 0; i < monsters.length; i++) {
		monsters[i].visible = false;
	}
	for (var i = 0; i < treasures.length; i++) {
		treasures[i].visible = false;
	}
	// Hide gate initially
	if (gate) {
		gate.visible = false;
	}
	// Calculate entity positions relative to player
	for (var i = 0; i < monsters.length; i++) {
		var monster = monsters[i];
		// Vector from player to monster
		var dx = monster.mapX - player.x;
		var dy = monster.mapY - player.y;
		// Distance to monster
		var dist = Math.sqrt(dx * dx + dy * dy);
		// Angle between player's direction and monster
		var angle = Math.atan2(dy, dx) - player.dir;
		// Normalize angle
		while (angle < -Math.PI) {
			angle += Math.PI * 2;
		}
		while (angle > Math.PI) {
			angle -= Math.PI * 2;
		}
		// Check if monster is in field of view
		if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
			// Check if there's a wall between player and monster
			var rayDirX = Math.cos(player.dir + angle);
			var rayDirY = Math.sin(player.dir + angle);
			var rayHit = castRayToPoint(player.x, player.y, monster.mapX, monster.mapY);
			if (!rayHit.hit || rayHit.dist > dist - 0.5) {
				// Monster is visible
				monster.visible = true;
				// Calculate screen position
				var screenX = (0.5 + angle / FOV) * 2048;
				// Calculate height based on distance
				var height = WALL_HEIGHT_FACTOR / dist;
				// Position monster
				monster.x = screenX;
				monster.y = 2732 / 2;
				// Scale monster based on distance
				var scale = height / 100;
				monster.scale.set(scale, scale);
			}
		}
	}
	// Render treasures with the same logic
	for (var i = 0; i < treasures.length; i++) {
		var treasure = treasures[i];
		var dx = treasure.mapX - player.x;
		var dy = treasure.mapY - player.y;
		var dist = Math.sqrt(dx * dx + dy * dy);
		var angle = Math.atan2(dy, dx) - player.dir;
		while (angle < -Math.PI) {
			angle += Math.PI * 2;
		}
		while (angle > Math.PI) {
			angle -= Math.PI * 2;
		}
		if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
			var rayHit = castRayToPoint(player.x, player.y, treasure.mapX, treasure.mapY);
			if (!rayHit.hit || rayHit.dist > dist - 0.5) {
				treasure.visible = true;
				var screenX = (0.5 + angle / FOV) * 2048;
				var height = WALL_HEIGHT_FACTOR / dist;
				treasure.x = screenX;
				treasure.y = 2732 / 2;
				var scale = height / 100;
				treasure.scale.set(scale, scale);
			}
		}
	}
	// Render gate with the same logic as treasures and monsters
	if (gate) {
		var dx = gate.mapX - player.x;
		var dy = gate.mapY - player.y;
		var dist = Math.sqrt(dx * dx + dy * dy);
		var angle = Math.atan2(dy, dx) - player.dir;
		while (angle < -Math.PI) {
			angle += Math.PI * 2;
		}
		while (angle > Math.PI) {
			angle -= Math.PI * 2;
		}
		if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
			var rayHit = castRayToPoint(player.x, player.y, gate.mapX, gate.mapY);
			if (!rayHit.hit || rayHit.dist > dist - 0.5) {
				gate.visible = true;
				var screenX = (0.5 + angle / FOV) * 2048;
				var height = WALL_HEIGHT_FACTOR / dist;
				gate.x = screenX;
				gate.y = 2732 / 2;
				var scale = height / 100;
				gate.scale.set(scale, scale);
			}
		}
	}
}
function castRayToPoint(startX, startY, targetX, targetY) {
	var rayDirX = targetX - startX;
	var rayDirY = targetY - startY;
	var distance = Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
	rayDirX /= distance;
	rayDirY /= distance;
	var mapCheckX = Math.floor(startX);
	var mapCheckY = Math.floor(startY);
	var stepSizeX = Math.abs(1 / rayDirX);
	var stepSizeY = Math.abs(1 / rayDirY);
	var stepX = rayDirX >= 0 ? 1 : -1;
	var stepY = rayDirY >= 0 ? 1 : -1;
	var sideDistX, sideDistY;
	if (rayDirX < 0) {
		sideDistX = (startX - mapCheckX) * stepSizeX;
	} else {
		sideDistX = (mapCheckX + 1.0 - startX) * stepSizeX;
	}
	if (rayDirY < 0) {
		sideDistY = (startY - mapCheckY) * stepSizeY;
	} else {
		sideDistY = (mapCheckY + 1.0 - startY) * stepSizeY;
	}
	var hit = false;
	var side = 0;
	var distToWall = 0;
	while (!hit && distToWall < distance) {
		if (sideDistX < sideDistY) {
			sideDistX += stepSizeX;
			mapCheckX += stepX;
			side = 0;
			distToWall = sideDistX - stepSizeX;
		} else {
			sideDistY += stepSizeY;
			mapCheckY += stepY;
			side = 1;
			distToWall = sideDistY - stepSizeY;
		}
		if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) {
			break;
		} else if (map[mapCheckY][mapCheckX].type === 1) {
			hit = true;
		}
	}
	return {
		hit: hit,
		dist: distToWall
	};
}
function updateControls() {
	var joystick = controlButtons.joystick;
	var currentJoystickX, currentJoystickY, isJoystickConsideredActive;
	if (joystickOverrideActive) {
		currentJoystickX = joystickOverrideX;
		currentJoystickY = joystickOverrideY;
		isJoystickConsideredActive = true; // Movement continues based on pre-attack state
	} else if (joystick && joystick.active) {
		currentJoystickX = joystick.normalizedX;
		currentJoystickY = joystick.normalizedY;
		isJoystickConsideredActive = true;
	} else {
		currentJoystickX = 0;
		currentJoystickY = 0;
		isJoystickConsideredActive = false;
	}
	if (isJoystickConsideredActive) {
		controls.forward = currentJoystickY < -0.3;
		controls.backward = currentJoystickY > 0.3;
		controls.left = currentJoystickX < -0.3;
		controls.right = currentJoystickX > 0.3;
	} else {
		controls.forward = false;
		controls.backward = false;
		controls.left = false;
		controls.right = false;
	}
	// Read from attack button
	controls.attack = controlButtons.attack.pressed;
}
function updatePlayerMovement(deltaTime) {
	var moveSpeed = PLAYER_MOVE_SPEED * deltaTime;
	var turnSpeed = PLAYER_TURN_SPEED * deltaTime;
	var dx = 0,
		dy = 0;
	var didMove = false;
	// Track player's last position to detect state changes
	if (player.lastX === undefined) {
		player.lastX = player.x;
	}
	if (player.lastY === undefined) {
		player.lastY = player.y;
	}
	// Get joystick values for analog control
	var joystick = controlButtons.joystick;
	var turnAmount = 0;
	var moveAmount = 0;
	var currentJoystickX, currentJoystickY, isJoystickConsideredActiveForMovement;
	if (joystickOverrideActive) {
		currentJoystickX = joystickOverrideX;
		currentJoystickY = joystickOverrideY;
		isJoystickConsideredActiveForMovement = true;
	} else if (joystick && joystick.active) {
		currentJoystickX = joystick.normalizedX;
		currentJoystickY = joystick.normalizedY;
		isJoystickConsideredActiveForMovement = true;
	} else {
		currentJoystickX = 0;
		currentJoystickY = 0;
		isJoystickConsideredActiveForMovement = false;
	}
	// Handle rotation - use x-axis for turning
	if (isJoystickConsideredActiveForMovement) {
		turnAmount = currentJoystickX * turnSpeed;
		player.dir += turnAmount;
		while (player.dir < 0) {
			player.dir += Math.PI * 2;
		}
		while (player.dir >= Math.PI * 2) {
			player.dir -= Math.PI * 2;
		}
	}
	// Also support digital controls for rotation (these are now secondary if joystick was primary)
	else if (controls.left) {
		player.dir -= turnSpeed;
		while (player.dir < 0) {
			player.dir += Math.PI * 2;
		}
	} else if (controls.right) {
		player.dir += turnSpeed;
		while (player.dir >= Math.PI * 2) {
			player.dir -= Math.PI * 2;
		}
	}
	// Handle movement - use y-axis for forward/backward
	if (isJoystickConsideredActiveForMovement) {
		moveAmount = -currentJoystickY * moveSpeed; // Negative because up is negative y
		if (Math.abs(moveAmount) > 0.01) {
			dx += Math.cos(player.dir) * moveAmount;
			dy += Math.sin(player.dir) * moveAmount;
			didMove = true;
		}
	}
	// Also support digital controls for movement (secondary)
	else if (controls.forward) {
		dx += Math.cos(player.dir) * moveSpeed;
		dy += Math.sin(player.dir) * moveSpeed;
		didMove = true;
	} else if (controls.backward) {
		dx -= Math.cos(player.dir) * moveSpeed;
		dy -= Math.sin(player.dir) * moveSpeed;
		didMove = true;
	}
	// Collision detection
	var newX = player.x + dx;
	var newY = player.y + dy;
	var cellX = Math.floor(newX);
	var cellY = Math.floor(newY);
	// Check if we can move to the new position
	if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE && map[cellY] && map[cellY][cellX]) {
		if (map[cellY][cellX].type === 0) {
			player.x = newX;
			player.y = newY;
			if (didMove && LK.ticks % 20 === 0) {
				LK.getSound('walk').play();
			}
		}
	}
	// Only attempt to attack if attack button is pressed and we can attack
	if (controls.attack && canAttack) {
		attackAction();
	}
	// Check for collisions with monsters
	checkMonsterCollisions();
	// Check for collisions with treasures
	checkTreasureCollisions();
	// Check for gate collision
	checkGateCollision();
	// Update player marker on minimap
	updateMiniMap();
	// Update player's last position
	player.lastX = player.x;
	player.lastY = player.y;
}
function attackAction() {
	// Check if attack is on cooldown
	if (!canAttack) {
		return;
	}
	// Set attack on cooldown
	canAttack = false;
	// Create and fire projectile
	var projectile = new Projectile();
	projectile.fire(globalRightHand.x, globalRightHand.y - 400);
	// Add hand recoil animation
	tween(globalRightHand, {
		y: 2732 - 50,
		rotation: -0.1
	}, {
		duration: 100,
		onFinish: function onFinish() {
			tween(globalRightHand, {
				y: 2732,
				rotation: 0
			}, {
				duration: 300
			});
		}
	});
	// Add to projectile layer
	projectiles.push(projectile);
	projectileLayer.addChild(projectile);
	// Update attack button visual
	var attackButton = controlButtons.attack;
	attackButton.updateCooldown(0);
	// Cooldown animation
	var _cooldownTick = function cooldownTick(progress) {
		attackButton.updateCooldown(progress);
		if (progress < 1) {
			LK.setTimeout(function () {
				_cooldownTick(progress + 0.05);
			}, attackCooldown / 20);
		}
	};
	_cooldownTick(0);
	// Reset cooldown after specified time
	LK.setTimeout(function () {
		canAttack = true;
	}, attackCooldown);
}
function updateProjectiles(deltaTime) {
	for (var i = projectiles.length - 1; i >= 0; i--) {
		var projectile = projectiles[i];
		// Update projectile position
		var remove = projectile.update(deltaTime);
		// Check if projectile has gone off screen or should be removed
		if (remove) {
			// Remove projectile
			projectile.destroy();
			projectiles.splice(i, 1);
			continue;
		}
		// Check if projectile hits a monster
		var hitMonster = false;
		var hitMonsterIndex = -1;
		for (var j = 0; j < monsters.length; j++) {
			var monster = monsters[j];
			if (monster.visible) {
				// Get the vector from player to monster in world space
				var monsterDx = monster.mapX - player.x;
				var monsterDy = monster.mapY - player.y;
				// Calculate distance to monster in world space
				var monsterDist = Math.sqrt(monsterDx * monsterDx + monsterDy * monsterDy);
				// Calculate the projectile's travel distance in world units
				var projectileWorldDist = projectile.distance;
				// Calculate a hit window that gets smaller as monster is closer
				// This is the reverse of what we had before - closer monsters need more precise hits
				var depthAccuracy = Math.min(1.0, projectileWorldDist / monsterDist);
				// Adjust hit window based on visual size of monster
				// Monster scale is determined by height / 100 in renderEntities
				var monsterScale = WALL_HEIGHT_FACTOR / (monsterDist * 100);
				// Calculate hit window based on screen position and monster size
				var screenFactor = 1 - Math.abs(monster.x - projectile.x) / (2048 / 2);
				// Calculate hit threshold as difference between projectile world distance and monster distance
				// Smaller difference = more likely hit
				var distanceDifference = Math.abs(projectileWorldDist - monsterDist);
				// Hit if projectile is close enough to monster's actual distance
				// AND if projectile is visually aligned with monster on screen
				if (distanceDifference < 0.5 && screenFactor > 0.7) {
					hitMonster = true;
					hitMonsterIndex = j;
					break;
				}
			}
		}
		// Handle monster hit logic (unchanged)
		if (hitMonster && hitMonsterIndex !== -1) {
			var monster = monsters[hitMonsterIndex];
			var killed = monster.takeDamage();
			if (killed) {
				map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster();
				monster.destroy();
				monsters.splice(hitMonsterIndex, 1);
				player.score += 10;
				updateUI();
				checkLevelCompletion();
			}
			projectile.destroy();
			projectiles.splice(i, 1);
		}
	}
}
function checkMonsterCollisions() {
	for (var i = 0; i < monsters.length; i++) {
		var monster = monsters[i];
		var dx = monster.mapX - player.x;
		var dy = monster.mapY - player.y;
		var dist = Math.sqrt(dx * dx + dy * dy);
		if (dist < 0.5) {
			// Player hit by monster
			player.health--;
			updateUI();
			// Visual feedback
			LK.effects.flashScreen(0xff0000, 300);
			// Play sound
			LK.getSound('hit').play();
			// Push player back slightly
			player.x -= dx * 0.3;
			player.y -= dy * 0.3;
			// Check game over
			if (player.health <= 0) {
				LK.showGameOver();
			}
			break;
		}
	}
}
function checkTreasureCollisions() {
	for (var i = treasures.length - 1; i >= 0; i--) {
		var treasure = treasures[i];
		var dx = treasure.mapX - player.x;
		var dy = treasure.mapY - player.y;
		var dist = Math.sqrt(dx * dx + dy * dy);
		if (dist < 0.5) {
			// Collect treasure
			player.score += treasure.value * 5;
			updateUI();
			// Play sound
			LK.getSound('collect').play();
			// Remove treasure
			map[Math.floor(treasure.mapY)][Math.floor(treasure.mapX)].removeTreasure();
			treasure.destroy();
			treasures.splice(i, 1);
			// Check level completion
			checkLevelCompletion();
		}
	}
}
function checkGateCollision() {
	if (!gate) {
		return;
	}
	var dx = gate.mapX - player.x;
	var dy = gate.mapY - player.y;
	var dist = Math.sqrt(dx * dx + dy * dy);
	if (dist < 0.7) {
		// Check if all monsters need to be defeated first
		if (monsters.length > 0) {
			// Display message that monsters need to be defeated
			var warningText = new Text2('Defeat all monsters\nbefore exiting!', {
				size: 60,
				fill: 0xFF5555
			});
			warningText.anchor.set(0.5, 0.5);
			warningText.x = 2048 / 2;
			warningText.y = 2732 / 2;
			gameLayer.addChild(warningText);
			// Remove text after a few seconds
			LK.setTimeout(function () {
				warningText.destroy();
			}, 2000);
			return;
		}
		// Player reached the gate - complete level
		// Play collect sound
		LK.getSound('collect').play();
		// Add points for completing level
		player.score += 50 * player.level;
		// Remove gate from map
		map[Math.floor(gate.mapY)][Math.floor(gate.mapX)].removeGate();
		gate.destroy();
		gate = null;
		// Level up
		player.level++;
		storage.level = player.level;
		// Restore health
		player.health = Math.min(player.health + 2, 5);
		// Update UI
		updateUI();
		// Show level complete with gate messaging
		var levelCompleteText = new Text2('Level ' + (player.level - 1) + ' Complete!\nYou found the exit!', {
			size: 80,
			fill: 0x00FF55
		});
		levelCompleteText.anchor.set(0.5, 0.5);
		levelCompleteText.x = 2048 / 2;
		levelCompleteText.y = 2732 / 2;
		gameLayer.addChild(levelCompleteText);
		// Generate new level after a delay
		LK.setTimeout(function () {
			levelCompleteText.destroy();
			generateMap();
		}, 2000);
	}
}
function checkLevelCompletion() {
	// Level is only considered "complete" if all monsters are defeated
	// This allows the gate to activate
	if (monsters.length === 0) {
		// If gate is not visible, make it pulse more dramatically to draw attention
		if (gate) {
			tween(gate, {
				alpha: 0.2
			}, {
				duration: 300,
				onFinish: function onFinish() {
					tween(gate, {
						alpha: 1
					}, {
						duration: 300
					});
				}
			});
			// Show hint text
			var gateHintText = new Text2('Find the exit gate!', {
				size: 60,
				fill: 0x00FF55
			});
			gateHintText.anchor.set(0.5, 0.5);
			gateHintText.x = 2048 / 2;
			gateHintText.y = 200;
			gameLayer.addChild(gateHintText);
			// Remove hint after a few seconds
			LK.setTimeout(function () {
				gateHintText.destroy();
			}, 3000);
		}
	} else {
		// Show hint text about defeating monsters first
		if (gate && gate.visible && LK.ticks % 300 === 0) {
			var monsterHintText = new Text2('Defeat all monsters to activate the exit!', {
				size: 60,
				fill: 0xFF5555
			});
			monsterHintText.anchor.set(0.5, 0.5);
			monsterHintText.x = 2048 / 2;
			monsterHintText.y = 200;
			gameLayer.addChild(monsterHintText);
			// Remove hint after a few seconds
			LK.setTimeout(function () {
				monsterHintText.destroy();
			}, 3000);
		}
	}
}
function updateMiniMap() {
	// Update player marker position on minimap
	playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE;
	playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE;
	// Draw player direction indicator
	var dirX = Math.cos(player.dir) * 15;
	var dirY = Math.sin(player.dir) * 15;
}
// Game update method
game.update = function () {
	var currentTime = Date.now();
	var deltaTime = currentTime - lastTime;
	lastTime = currentTime;
	// Update controls
	updateControls();
	// Update player
	updatePlayerMovement(deltaTime);
	// Update monsters (only move every few ticks to make movement slower)
	if (LK.ticks % 20 === 0) {
		for (var i = 0; i < monsters.length; i++) {
			MonsterAI.moveTowardsPlayer(monsters[i], player.x, player.y, map);
		}
	}
	// Update projectiles
	updateProjectiles(deltaTime);
	// Update raycast view
	rayCasting();
};
// Game initialization
setupGame();
// Event handlers
game.down = function (x, y, obj) {
	activeControlForGlobalHandlers = null; // Reset at the start of a new touch
	var eventObjForAttack = Object.assign({}, obj); // Fresh event object for attack button
	eventObjForAttack.stopPropagation = false;
	var attackBtn = controlButtons.attack;
	var attackBtnLocalX = x - attackBtn.x;
	var attackBtnLocalY = y - attackBtn.y;
	// Use the same generous hit radius as in the original code
	var attackBtnHitRadius = 150 * 3.5;
	var attackBtnDistSq = attackBtnLocalX * attackBtnLocalX + attackBtnLocalY * attackBtnLocalY;
	if (attackBtnDistSq <= attackBtnHitRadius * attackBtnHitRadius) {
		activeControlForGlobalHandlers = attackBtn;
		attackBtn.down(attackBtnLocalX, attackBtnLocalY, eventObjForAttack);
		// If the touch is within the attack button's area, it's considered handled by the attack button,
		// regardless of whether it internally decided to stop propagation (e.g. attack on cooldown).
		// This prevents the joystick from also processing this touch.
		return;
	}
	// If we reach here, the touch was NOT on the attack button.
	// Now check for joystick interaction using original quadrant check.
	var joystick = controlButtons.joystick;
	if (x < 800 && y > 2732 - 800) {
		activeControlForGlobalHandlers = joystick;
		var eventObjForJoystick = Object.assign({}, obj); // Fresh event object for joystick
		eventObjForJoystick.stopPropagation = false;
		// Pass local coordinates to joystick.down
		joystick.down(x - joystick.x, y - joystick.y, eventObjForJoystick);
	}
};
game.up = function (x, y, obj) {
	if (activeControlForGlobalHandlers === controlButtons.attack) {
		var attackBtnLocalX = x - controlButtons.attack.x;
		var attackBtnLocalY = y - controlButtons.attack.y;
		controlButtons.attack.up(attackBtnLocalX, attackBtnLocalY, obj);
	} else if (activeControlForGlobalHandlers === controlButtons.joystick) {
		// Only call joystick.up if the joystick itself believes it's active
		if (controlButtons.joystick.active) {
			var joystickLocalX = x - controlButtons.joystick.x;
			var joystickLocalY = y - controlButtons.joystick.y;
			controlButtons.joystick.up(joystickLocalX, joystickLocalY, obj);
		}
	}
	activeControlForGlobalHandlers = null; // Reset after touch ends
};
game.move = function (x, y, obj) {
	// Handle general screen move
	if (activeControlForGlobalHandlers === controlButtons.joystick) {
		// Only call joystick.move if the joystick itself believes it's active
		if (controlButtons.joystick.active) {
			var joystickLocalX = x - controlButtons.joystick.x;
			var joystickLocalY = y - controlButtons.joystick.y;
			controlButtons.joystick.move(joystickLocalX, joystickLocalY, obj);
		}
	}
	// Attack button doesn't typically have a .move action, so no changes needed for it here.
};
var MonsterAI = {
	moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) {
		// If monster is not visible (no line of sight or out of FOV/range), it shouldn't attempt to move.
		// monster.visible is set by the renderEntities function, which includes line-of-sight checks.
		if (!monster.visible) {
			return;
		}
		// Get monster's map position
		var monsterX = monster.mapX;
		var monsterY = monster.mapY;
		// Vector from monster to player
		var dx = playerX - monsterX;
		var dy = playerY - monsterY;
		// Only move if monster is within a certain distance to the player (aggro range)
		// This check is in addition to the visibility/LoS check.
		var dist = Math.sqrt(dx * dx + dy * dy);
		if (dist > 8) {
			return;
		} // Don't move if too far away (but still visible)
		// Normalize direction vector
		var length = Math.sqrt(dx * dx + dy * dy);
		if (length > 0) {
			dx /= length;
			dy /= length;
		}
		// Movement speed (slower than player)
		var moveSpeed = 0.05; // Increased for smoother animation
		// Calculate potential new position
		var newX = monsterX + dx * moveSpeed;
		var newY = monsterY + dy * moveSpeed;
		// Round to get map cell coordinates
		var cellX = Math.floor(newX);
		var cellY = Math.floor(newY);
		// Check if new position is valid (not a wall or another monster)
		if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) {
			if (map[cellY][cellX].type === 0 && !map[cellY][cellX].monster) {
				// Update map cell references
				map[Math.floor(monsterY)][Math.floor(monsterX)].removeMonster();
				map[cellY][cellX].addMonster();
				// Update monster position with smooth animation
				tween(monster, {
					mapX: newX,
					mapY: newY
				}, {
					duration: 300,
					// 300ms smooth animation
					easing: function easing(t) {
						return 1 - Math.pow(1 - t, 4);
					} // quartOut implementation
				});
			}
		}
	}
};
function ensureMapConnectivity() {
	// Flood fill from player starting position to check accessibility
	var visited = [];
	for (var y = 0; y < MAP_SIZE; y++) {
		visited[y] = [];
		for (var x = 0; x < MAP_SIZE; x++) {
			visited[y][x] = false;
		}
	}
	var queue = [];
	// Start from player position (1,1)
	queue.push({
		x: 1,
		y: 1
	});
	visited[1][1] = true;
	// Perform flood fill
	while (queue.length > 0) {
		var current = queue.shift();
		var x = current.x;
		var y = current.y;
		// Check all four neighbors
		var neighbors = [{
			x: x + 1,
			y: y
		}, {
			x: x - 1,
			y: y
		}, {
			x: x,
			y: y + 1
		}, {
			x: x,
			y: y - 1
		}];
		for (var i = 0; i < neighbors.length; i++) {
			var nx = neighbors[i].x;
			var ny = neighbors[i].y;
			// Check if neighbor is valid and not visited
			if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) {
				visited[ny][nx] = true;
				queue.push({
					x: nx,
					y: ny
				});
			}
		}
	}
	// Check for unreachable areas and create paths to them
	for (var y = 1; y < MAP_SIZE - 1; y++) {
		for (var x = 1; x < MAP_SIZE - 1; x++) {
			// If it's a floor tile but wasn't visited, it's unreachable
			if (map[y][x].type === 0 && !visited[y][x]) {
				// Find the nearest accessible cell
				var nearestX = -1;
				var nearestY = -1;
				var minDist = MAP_SIZE * MAP_SIZE;
				for (var cy = 1; cy < MAP_SIZE - 1; cy++) {
					for (var cx = 1; cx < MAP_SIZE - 1; cx++) {
						if (visited[cy][cx]) {
							var dist = (cx - x) * (cx - x) + (cy - y) * (cy - y);
							if (dist < minDist) {
								minDist = dist;
								nearestX = cx;
								nearestY = cy;
							}
						}
					}
				}
				// Create a path from the unreachable area to the nearest accessible area
				if (nearestX !== -1) {
					createPath(x, y, nearestX, nearestY);
					// Update visited map by doing a mini flood fill from this newly connected point
					var pathQueue = [{
						x: x,
						y: y
					}];
					visited[y][x] = true;
					while (pathQueue.length > 0) {
						var current = pathQueue.shift();
						var px = current.x;
						var py = current.y;
						var pathNeighbors = [{
							x: px + 1,
							y: py
						}, {
							x: px - 1,
							y: py
						}, {
							x: px,
							y: py + 1
						}, {
							x: px,
							y: py - 1
						}];
						for (var j = 0; j < pathNeighbors.length; j++) {
							var nx = pathNeighbors[j].x;
							var ny = pathNeighbors[j].y;
							if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) {
								visited[ny][nx] = true;
								pathQueue.push({
									x: nx,
									y: ny
								});
							}
						}
					}
				}
			}
		}
	}
}
function createPath(startX, startY, endX, endY) {
	// Determine direction (horizontal or vertical first)
	if (Math.random() < 0.5) {
		// Horizontal first, then vertical
		var x = startX;
		while (x !== endX) {
			x += x < endX ? 1 : -1;
			if (map[startY][x].type === 1) {
				map[startY][x].setType(0); // Convert wall to floor
			}
		}
		var y = startY;
		while (y !== endY) {
			y += y < endY ? 1 : -1;
			if (map[y][endX].type === 1) {
				map[y][endX].setType(0); // Convert wall to floor
			}
		}
	} else {
		// Vertical first, then horizontal
		var y = startY;
		while (y !== endY) {
			y += y < endY ? 1 : -1;
			if (map[y][startX].type === 1) {
				map[y][startX].setType(0); // Convert wall to floor
			}
		}
		var x = startX;
		while (x !== endX) {
			x += x < endX ? 1 : -1;
			if (map[endY][x].type === 1) {
				map[endY][x].setType(0); // Convert wall to floor
			}
		}
	}
} ===================================================================
--- original.js
+++ change.js
@@ -372,28 +372,40 @@
 	return self;
 });
 var RaycastStrip = Container.expand(function () {
 	var self = Container.call(this);
-	var wallSprite = self.attachAsset('wall', {
+	var wall = self.attachAsset('wall', {
 		anchorX: 0,
 		anchorY: 0
 	});
-	// Removed ceiling and floor shape assets. Floor/ceiling will be handled by a new system.
-	self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distanceToWall) {
+	var ceiling = self.attachAsset('ceiling', {
+		anchorX: 0,
+		anchorY: 0
+	});
+	var floor = self.attachAsset('floor', {
+		anchorX: 0,
+		anchorY: 0
+	});
+	self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance) {
 		// Wall setup
-		wallSprite.width = stripWidth;
-		wallSprite.height = wallHeight;
-		wallSprite.y = (2732 - wallHeight) / 2;
-		wallSprite.visible = true;
-		// Adjust strip position
+		wall.width = stripWidth;
+		wall.height = wallHeight;
+		wall.y = (2732 - wallHeight) / 2;
+		// Ceiling setup
+		ceiling.width = stripWidth;
+		ceiling.height = wall.y;
+		ceiling.y = 0;
+		// Floor setup
+		floor.width = stripWidth;
+		floor.height = ceiling.height;
+		floor.y = wall.y + wallHeight;
+		// Adjust positions
 		self.x = stripIdx * stripWidth;
-		// Add distance shading effect to wall
-		var shade = Math.max(0.3, 1 - distanceToWall / MAX_RENDER_DISTANCE); // Use MAX_RENDER_DISTANCE for shading range
-		wallSprite.alpha = shade;
-		// If wall is effectively invisible due to distance or height, hide it.
-		if (wallHeight <= 0 || distanceToWall >= MAX_RENDER_DISTANCE) {
-			wallSprite.visible = false;
-		}
+		// Add distance shading effect
+		var shade = Math.max(0.3, 1 - distance / 10);
+		wall.alpha = shade;
+		ceiling.alpha = shade * 0.7;
+		floor.alpha = shade * 0.8;
 	};
 	return self;
 });
 var Treasure = Container.expand(function () {
@@ -436,13 +448,8 @@
 var WALL_HEIGHT_FACTOR = 600;
 var MAX_RENDER_DISTANCE = 16;
 var MONSTER_COUNT = 5;
 var TREASURE_COUNT = 10;
-// Constants for Floor/Ceiling Casting
-var PLAYER_EYE_HEIGHT = 0.5; // World units, assuming floor at 0, ceiling at 1 relative to player eye level.
-var NUM_FLOOR_CEILING_STRIPS = 20; // Number of horizontal strips for floor and ceiling each.
-var TEXTURE_TILE_WORLD_SIZE = 1.0; // Assume textures tile every 1.0 world unit.
-var FAR_CLIP_PLANE_FLOOR_CEILING = MAX_RENDER_DISTANCE * 1.5; // How far to render floor/ceiling.
 // Game state
 var map = [];
 var player = {
 	x: 1.5,
@@ -480,13 +487,8 @@
 var controlButtons = {};
 var playerMarker;
 var globalRightHand; // Stores the right hand game object
 var activeControlForGlobalHandlers = null; // Tracks which control is active for game.down/up/move
-// Containers and arrays for floor/ceiling strips
-var floorStripsContainer;
-var ceilingStripsContainer;
-var floorStrips = [];
-var ceilingStrips = [];
 // Create layer variables at the global scope
 var gameLayer = new Container();
 var projectileLayer = new Container();
 var handLayer = new Container();
@@ -509,40 +511,8 @@
 	miniMap = new Container();
 	miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2; // Center horizontally
 	miniMap.y = 20; // Keep at top
 	gameLayer.addChild(miniMap);
-	// Create containers for floor and ceiling strips
-	floorStripsContainer = new Container();
-	gameLayer.addChild(floorStripsContainer); // Add behind other entities, or adjust layer order if needed
-	ceilingStripsContainer = new Container();
-	gameLayer.addChild(ceilingStripsContainer); // Add behind other entities
-	// Populate floor and ceiling strips
-	var floorTileAsset = LK.getAsset('floorTile', {}); // Get asset once to read its original dimensions
-	var ceilingTileAsset = LK.getAsset('ceilingTile', {});
-	for (var i = 0; i < NUM_FLOOR_CEILING_STRIPS; i++) {
-		// Floor strips
-		var floorStrip = LK.getAsset('floorTile', {
-			anchorX: 0.5,
-			// Anchor at horizontal center for easier pivot calculations
-			anchorY: 0,
-			// Anchor at top for stacking
-			width: 2048,
-			// Full screen width
-			x: 2048 / 2 // Centered
-		});
-		floorStrips.push(floorStrip);
-		floorStripsContainer.addChild(floorStrip);
-		// Ceiling strips
-		var ceilingStrip = LK.getAsset('ceilingTile', {
-			anchorX: 0.5,
-			anchorY: 1,
-			// Anchor at bottom for stacking from horizon up
-			width: 2048,
-			x: 2048 / 2
-		});
-		ceilingStrips.push(ceilingStrip);
-		ceilingStripsContainer.addChild(ceilingStrip);
-	}
 	// Generate map
 	generateMap();
 	// Create player marker
 	playerMarker = gameLayer.addChild(LK.getAsset('player', {
@@ -812,8 +782,65 @@
 	var rayAngle, distToWall, rayDirX, rayDirY, mapCheckX, mapCheckY;
 	var distX, distY;
 	var rayStartX = player.x;
 	var rayStartY = player.y;
+	// Floor and ceiling rendering using floor casting
+	// We'll render the floor and ceiling for each row of the screen, for each ray
+	// We'll use a single Container for the floor/ceiling layer, and draw quads (tiles) for each row/column
+	if (!rayCastView.floorLayer) {
+		rayCastView.floorLayer = new Container();
+		rayCastView.addChildAt(rayCastView.floorLayer, 0); // Add below strips
+	}
+	var floorLayer = rayCastView.floorLayer;
+	floorLayer.removeChildren(); // Clear previous frame
+	// Precompute some values for efficiency
+	var screenCenterY = 2732 / 2;
+	var screenHeight = 2732;
+	var screenWidth = 2048;
+	var horizon = screenCenterY;
+	var tileSize = 1; // 1 world unit per tile
+	// For each row from the center to the bottom (floor) and from center to top (ceiling)
+	for (var y = 0; y < screenHeight; y += 8) {
+		// Perspective: distance from player to the row in world space
+		// Formula: rowDistance = (player height * projection plane distance) / (row - screen center)
+		// We'll use player height = 0.5, projection plane = screenCenterY
+		var isFloor = y > horizon;
+		var rowDist = isFloor ? 0.5 * screenCenterY / (y - horizon) : 0.5 * screenCenterY / (horizon - y);
+		// For each column (ray)
+		for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
+			var rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
+			var rayDirX = Math.cos(rayAngle);
+			var rayDirY = Math.sin(rayAngle);
+			// Calculate the world position for this pixel
+			// CameraX: -1 (left) to 1 (right)
+			var cameraX = 2 * rayIdx / NUM_RAYS - 1;
+			// World coordinates for the floor/ceiling point
+			var worldX = player.x + rowDist * rayDirX;
+			var worldY = player.y + rowDist * rayDirY;
+			// Snap to tile
+			var mapX = Math.floor(worldX);
+			var mapY = Math.floor(worldY);
+			// Only draw if inside map
+			if (mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE) {
+				// Choose asset for floor/ceiling
+				var assetId = isFloor ? 'floorTile' : 'ceilingTile';
+				var tileSprite = LK.getAsset(assetId, {
+					anchorX: 0.5,
+					anchorY: 0.5,
+					scaleX: 0.08,
+					scaleY: 0.08
+				});
+				// Place on screen
+				tileSprite.x = rayIdx * STRIP_WIDTH + STRIP_WIDTH / 2;
+				tileSprite.y = y + 4; // Center of the row
+				// Add distance shading
+				var shade = Math.max(0.2, 1 - rowDist / 10);
+				tileSprite.alpha = shade * (isFloor ? 0.9 : 0.7);
+				floorLayer.addChild(tileSprite);
+			}
+		}
+	}
+	// Raycasting for walls (unchanged)
 	for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
 		// Calculate ray angle (center ray + offset based on ray index)
 		rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
 		// Get direction vector
@@ -880,89 +907,9 @@
 		strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, 1, distToWall);
 	}
 	// Render monsters and treasures
 	renderEntities();
-	// Render floor and ceiling using the new casting method
-	renderFloorAndCeilingCasted();
 }
-function renderFloorAndCeilingCasted() {
-	var screenCenterY = 2732 / 2;
-	var projectionPlaneDist = 2048 / 2 / Math.tan(HALF_FOV);
-	// Get original texture dimensions (assuming they are consistent for all tile assets)
-	// These might not be perfectly accurate if LK.getAsset doesn't return a direct reference with .texture.width/height
-	// For now, assuming 100x100 as per asset definition, but ideally, this would be dynamic.
-	var textureWidth = 100;
-	var textureHeight = 100;
-	for (var i = 0; i < NUM_FLOOR_CEILING_STRIPS; i++) {
-		var floorStrip = floorStrips[i];
-		var ceilingStrip = ceilingStrips[i];
-		// Calculate y position for this strip on screen (from horizon outwards)
-		// This creates a non-linear distribution, more strips closer to horizon.
-		var stripDepthRatio = (i + 1) / NUM_FLOOR_CEILING_STRIPS; // 0 to 1
-		// Floor strip
-		var yFloor = screenCenterY + stripDepthRatio * screenCenterY;
-		var floorStripHeight = screenCenterY / NUM_FLOOR_CEILING_STRIPS * (1 + i * 0.5); // Strips get taller further from horizon
-		var pDistFloor = PLAYER_EYE_HEIGHT * projectionPlaneDist / (yFloor - screenCenterY);
-		if (yFloor - screenCenterY <= 0) pDistFloor = FAR_CLIP_PLANE_FLOOR_CEILING; // Avoid division by zero/negative
-		if (pDistFloor > 0 && pDistFloor < FAR_CLIP_PLANE_FLOOR_CEILING) {
-			floorStrip.visible = true;
-			floorStrip.y = yFloor - floorStripHeight; // Stacking from horizon down, anchorY = 0
-			floorStrip.height = floorStripHeight;
-			var scaleFactorFloor = projectionPlaneDist / pDistFloor; // How much 1 world unit scales to on screen at this distance
-			// Texture coordinates based on player world position and direction
-			// Rotate player's local X/Y based on player direction for texture mapping
-			var cosDir = Math.cos(player.dir);
-			var sinDir = Math.sin(player.dir);
-			// We are setting pivot, which means the texture's (pivot.x, pivot.y) point will be at strip's (x,y)
-			// For floor, texture should appear to move "under" the player.
-			// Player's world X maps to texture U, Player's world Y maps to texture V
-			// Scale of texture should reflect perspective.
-			// Effective texture coordinates for the center of the view at this depth:
-			var worldFloorCenterX = player.x + pDistFloor * cosDir;
-			var worldFloorCenterY = player.y + pDistFloor * sinDir;
-			floorStrip.pivot.x = worldFloorCenterX / TEXTURE_TILE_WORLD_SIZE * textureWidth;
-			floorStrip.pivot.y = worldFloorCenterY / TEXTURE_TILE_WORLD_SIZE * textureHeight;
-			floorStrip.scale.x = scaleFactorFloor;
-			floorStrip.scale.y = scaleFactorFloor; // Should this be different? Height is screen-space defined.
-			// The strip is already full screen width (2048), so scale.x affects texture tiling.
-			// floorStrip.width is 2048. So scale.x affects how many texture repetitions fit.
-			// If scaleFactorFloor = 1, texture appears 1:1 world units to screen pixels at projection plane.
-			// We need texture to be scaled such that floorStrip.width shows correct span of world.
-			// The horizontal span of world seen at this depth for FOV is pDistFloor * 2 * tan(HALF_FOV)
-			// This world span should be mapped to floorStrip.width (2048)
-			var worldSpanX = pDistFloor * 2 * Math.tan(HALF_FOV);
-			floorStrip.scale.x = textureWidth / TEXTURE_TILE_WORLD_SIZE * (worldSpanX / 2048);
-			// This makes the scale relative to texture's original size.
-			// No, simpler:
-			floorStrip.scale.x = 2048 / textureWidth * (TEXTURE_TILE_WORLD_SIZE / worldSpanX);
-			floorStrip.scale.y = floorStripHeight / textureHeight; // Make texture fill the strip's screen height. This might look bad.
-			// Let's try to keep Y scale proportional to X for less distortion.
-			floorStrip.scale.y = floorStrip.scale.x;
-		} else {
-			floorStrip.visible = false;
-		}
-		// Ceiling strip (similar logic, inverted Y)
-		var yCeiling = screenCenterY - stripDepthRatio * screenCenterY;
-		var ceilingStripHeight = screenCenterY / NUM_FLOOR_CEILING_STRIPS * (1 + i * 0.5);
-		var pDistCeiling = PLAYER_EYE_HEIGHT * projectionPlaneDist / (screenCenterY - yCeiling);
-		if (screenCenterY - yCeiling <= 0) pDistCeiling = FAR_CLIP_PLANE_FLOOR_CEILING;
-		if (pDistCeiling > 0 && pDistCeiling < FAR_CLIP_PLANE_FLOOR_CEILING) {
-			ceilingStrip.visible = true;
-			ceilingStrip.y = yCeiling + ceilingStripHeight; // Stacking from horizon up, anchorY = 1
-			ceilingStrip.height = ceilingStripHeight;
-			var scaleFactorCeiling = projectionPlaneDist / pDistCeiling;
-			var worldCeilingCenterX = player.x + pDistCeiling * cosDir;
-			var worldCeilingCenterY = player.y + pDistCeiling * sinDir;
-			ceilingStrip.pivot.x = worldCeilingCenterX / TEXTURE_TILE_WORLD_SIZE * textureWidth;
-			ceilingStrip.pivot.y = worldCeilingCenterY / TEXTURE_TILE_WORLD_SIZE * textureHeight;
-			var worldSpanXCeiling = pDistCeiling * 2 * Math.tan(HALF_FOV);
-			ceilingStrip.scale.x = 2048 / textureWidth * (TEXTURE_TILE_WORLD_SIZE / worldSpanXCeiling);
-			ceilingStrip.scale.y = ceilingStrip.scale.x; // Keep aspect ratio for texture scaling
-		} else {
-			ceilingStrip.visible = false;
-		}
-	}
-}
 function renderEntities() {
 	// First, hide all entities
 	for (var i = 0; i < monsters.length; i++) {
 		monsters[i].visible = false;
@@ -1533,12 +1480,10 @@
 		}
 	}
 	// Update projectiles
 	updateProjectiles(deltaTime);
-	// Update raycast view (now only walls and entities)
+	// Update raycast view
 	rayCasting();
-	// Note: renderFloorAndCeilingCasted is called inside rayCasting after renderEntities.
-	// If it were to be called separately, this is where it might go.
 };
 // Game initialization
 setupGame();
 // Event handlers
 Fullscreen modern App Store landscape banner, 16:9, high definition, for a game titled "RayCaster Dungeon Crawler" and with the description "A first-person dungeon crawler using ray casting technology to create a pseudo-3D experience. Navigate maze-like dungeons, defeat monsters, and collect treasures as you explore increasingly challenging levels with authentic retro visuals.". No text on banner!
 Tan wall. In-Game asset. 2d. High contrast. No shadows
 
 A blue glowing orb of magic. Pixel art. In-Game asset. 2d. High contrast. No shadows
 A tile of grey and mossy dungeon stone floor. Pixel art.. In-Game asset. 2d. High contrast. No shadows
 
 Arm up in the air.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 A unlit metal cage sconce like you find on a dungeon wall. White candle inside. Pixel art.. In-Game asset. 2d. High contrast. No shadows
 A white spider web. Pixelated retro.. In-Game asset. 2d. High contrast. No shadows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 A round fireball projectile. Straight on view as if it’s coming straight towards the camera. Retro pixel art.. In-Game asset. 2d. High contrast. No shadows
 A stone staircase icon. Side profile. Pixel art.. In-Game asset. 2d. High contrast. No shadows
 Pixel art logo for a game called ‘Demon’s Depths’. Big demon head with the title of the game split on top and bottom. The words are made of flame. White background In-Game asset. 2d. High contrast. No shadows
 Background image of gate leading into a dark dungeon. Walls are grey and mossy stones. Retro pixel art.. In-Game asset. 2d. High contrast. No shadows
 SVG made of grey stone bricks that says ‘Enter’. Retro pixel art. In-Game asset. 2d. High contrast. No shadows
 
 dungeon
Music
playerprojectile
Sound effect
wallhit
Sound effect
walk
Sound effect
impCry
Sound effect
playerhurt
Sound effect
enemyexplosion
Sound effect
pixeldrop
Sound effect
eyeball
Sound effect
treasureopen
Sound effect
fountainsplash
Sound effect
powerup
Sound effect
ogre
Sound effect
fireball
Sound effect
bosschant
Sound effect
demonlaugh
Sound effect