Code edit (1 edits merged)
Please save this source code
Code edit (3 edits merged)
Please save this source code
User prompt
Remove player pushback by monsters.
User prompt
Make the mini map cell where the exit gate is in green. Initialize a new shape asset for it.
User prompt
Too much of the mini map is being revealed. It should only be the cell the player is in and the 8 surrounding cells.
User prompt
Make the inner section of the mini map to be invisible off the top, except for the outer walls and the cells immediately around the player. As the player moves through the map, reveal the cells.
User prompt
Update with: var FloorCaster = Container.expand(function () { var self = Container.call(this); var SCREEN_WIDTH = 2048; var SCREEN_HEIGHT = 2732; var HORIZON_Y = SCREEN_HEIGHT / 2; var floorContainer = new Container(); var ceilingContainer = new Container(); self.addChild(floorContainer); self.addChild(ceilingContainer); self.update = function (playerX, playerY, playerDir) { floorContainer.removeChildren(); ceilingContainer.removeChildren(); // Camera plane vectors (same as walls) var planeX = Math.cos(playerDir + Math.PI / 2) * 0.66; var planeY = Math.sin(playerDir + Math.PI / 2) * 0.66; var dirX = Math.cos(playerDir); var dirY = Math.sin(playerDir); // Floor rendering for (var y = HORIZON_Y + 1; y < SCREEN_HEIGHT; y += 2) { var rowDistance = (WALL_HEIGHT_FACTOR * 0.5) / (y - HORIZON_Y); if (rowDistance > MAX_RENDER_DISTANCE) continue; // World coordinates for this scanline var floorStepX = rowDistance * (planeX * 2) / SCREEN_WIDTH; var floorStepY = rowDistance * (planeY * 2) / SCREEN_WIDTH; var floorX = playerX + rowDistance * (dirX - planeX); var floorY = playerY + rowDistance * (dirY - planeY); // Create complete floor tile using all 64 strips var tileContainer = new Container(); for (var stripIndex = 1; stripIndex <= 64; stripIndex++) { var currentFloorX = floorX + floorStepX * (stripIndex - 32.5) * (SCREEN_WIDTH / 64); var currentFloorY = floorY + floorStepY * (stripIndex - 32.5) * (SCREEN_WIDTH / 64); var mapX = Math.floor(currentFloorX); var mapY = Math.floor(currentFloorY); if (mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE && map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 0) { var stripSprite = LK.getAsset('walltile' + stripIndex, { anchorX: 0, anchorY: 0 }); stripSprite.x = (stripIndex - 1) * (SCREEN_WIDTH / 64); stripSprite.y = 0; stripSprite.width = SCREEN_WIDTH / 64; stripSprite.height = 2; // Apply distance shading var distanceRatio = rowDistance / MAX_RENDER_DISTANCE; var shadeFactor = Math.max(0.25, 1 - distanceRatio * 2); stripSprite.alpha = shadeFactor; tileContainer.addChild(stripSprite); } } tileContainer.x = 0; tileContainer.y = y; floorContainer.addChild(tileContainer); } // Ceiling (same concept) for (var y = HORIZON_Y - 2; y >= 0; y -= 2) { var rowDistance = (WALL_HEIGHT_FACTOR * 0.5) / (HORIZON_Y - y); if (rowDistance > MAX_RENDER_DISTANCE) continue; var ceilingStepX = rowDistance * (planeX * 2) / SCREEN_WIDTH; var ceilingStepY = rowDistance * (planeY * 2) / SCREEN_WIDTH; var ceilingX = playerX + rowDistance * (dirX - planeX); var ceilingY = playerY + rowDistance * (dirY - planeY); var tileContainer = new Container(); for (var stripIndex = 1; stripIndex <= 64; stripIndex++) { var currentCeilingX = ceilingX + ceilingStepX * (stripIndex - 32.5) * (SCREEN_WIDTH / 64); var currentCeilingY = ceilingY + ceilingStepY * (stripIndex - 32.5) * (SCREEN_WIDTH / 64); var mapX = Math.floor(currentCeilingX); var mapY = Math.floor(currentCeilingY); if (mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE && map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 0) { var stripSprite = LK.getAsset('walltile' + stripIndex, { anchorX: 0, anchorY: 0 }); stripSprite.x = (stripIndex - 1) * (SCREEN_WIDTH / 64); stripSprite.y = 0; stripSprite.width = SCREEN_WIDTH / 64; stripSprite.height = 2; stripSprite.alpha = Math.max(0.15, 1 - (rowDistance / MAX_RENDER_DISTANCE) * 2) * 0.7; tileContainer.addChild(stripSprite); } } tileContainer.x = 0; tileContainer.y = y; ceilingContainer.addChild(tileContainer); } }; return self; });
User prompt
Use wall tiles for floor casting strips in numerical order.
User prompt
Display the “Find the exit” in the center of the screen and increase font size to match Upgrade messages.
User prompt
Update as needed with: // In Particle.update method, modify the visibility section: if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE_PARTICLES) { // Check if particle is occluded by a wall var isOccluded = false; // Cast a ray from player to particle to see if wall blocks it var rayResult = { wallHit: false, distance: 0, side: 0, wallType: 0, mapX: 0, mapY: 0 }; var rayDirX = (self.worldX - player.x) / distToPlayerPlane; var rayDirY = (self.worldY - player.y) / distToPlayerPlane; castRayDDAInto(player.x, player.y, rayDirX, rayDirY, rayResult); if (rayResult.wallHit && rayResult.distance < distToPlayerPlane - 0.2) { isOccluded = true; } if (!isOccluded) { // ... existing screen positioning code ... self.visible = true; } else { self.visible = false; } } else { self.visible = false; }
User prompt
Update as needed with: // In initializeGamePlayElements, change this: function initializeGamePlayElements() { game.addChild(gameLayer); game.addChild(projectileLayer); game.addChild(handLayer); // ... existing code ... // Add this line to create particle container in gameLayer instead: particleExplosionContainer = new Container(); gameLayer.addChild(particleExplosionContainer); // This will render behind dynamicEntitiesContainer // ... rest of function } ``` And remove the particle container creation from `createParticleExplosion`: ```javascript function createParticleExplosion(worldExplosionX, worldExplosionY, count, tintColor, isWallHit) { // Remove or comment out these lines: // if (!particleExplosionContainer) { // particleExplosionContainer = new Container(); // projectileLayer.addChild(particleExplosionContainer); // } // ... rest of function unchanged }
User prompt
Update as needed with: // In initializeGamePlayElements, change this: function initializeGamePlayElements() { game.addChild(gameLayer); game.addChild(projectileLayer); game.addChild(handLayer); // ... existing code ... // Add this line to create particle container in gameLayer instead: particleExplosionContainer = new Container(); gameLayer.addChild(particleExplosionContainer); // This will render behind dynamicEntitiesContainer // ... rest of function } ``` And remove the particle container creation from `createParticleExplosion`: ```javascript function createParticleExplosion(worldExplosionX, worldExplosionY, count, tintColor, isWallHit) { // Remove or comment out these lines: // if (!particleExplosionContainer) { // particleExplosionContainer = new Container(); // projectileLayer.addChild(particleExplosionContainer); // } // ... rest of function unchanged }
User prompt
Update as needed with: // In Particle.update method, replace the visibility logic with: self.update = function () { // ... existing physics code ... var dx = self.worldX - player.x; var dy = self.worldY - player.y; var distToPlayerPlane = Math.sqrt(dx * dx + dy * dy); if (distToPlayerPlane < 0.1) { distToPlayerPlane = 0.1; } var angle = Math.atan2(dy, dx) - player.dir; while (angle < -Math.PI) { angle += MATH_PI_2; } while (angle > Math.PI) { angle -= MATH_PI_2; } if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE_PARTICLES) { // Calculate screen position (existing code) self.x = SCREEN_WIDTH_HALF + angle / HALF_FOV * SCREEN_WIDTH_HALF; // ... rest of screen positioning code ... // Add to renderable objects instead of setting visible directly renderableObjects.push({ distance: distToPlayerPlane, object: self, type: 'particle' }); self.visible = false; // Will be made visible by EntityRenderer } else { self.visible = false; } // ... rest of method }; ``` Then modify `EntityRenderer.updateEntities` to handle particles: ```javascript updateEntities: function updateEntities(entities) { this.currentFrameEntities = entities.slice(); if (!this.arraysEqual(this.lastFrameEntities, this.currentFrameEntities)) { dynamicEntitiesContainer.removeChildren(); // Sort by distance (particles will be sorted with everything else) this.currentFrameEntities.sort(function (a, b) { if (a.distance !== b.distance) { return b.distance - a.distance; } if (a.type === 'entity' && b.type === 'wall') { return -1; } if (a.type === 'wall' && b.type === 'entity') { return 1; } if (a.type === 'particle') { return -1; // Particles render in front of walls at same distance } if (a.type === 'wall' && b.type === 'wall') { return a.originalSortOrder - b.originalSortOrder; } return 0; }); for (var i = 0; i < this.currentFrameEntities.length; i++) { if (this.currentFrameEntities[i].object.visible || this.currentFrameEntities[i].type === 'particle') { dynamicEntitiesContainer.addChild(this.currentFrameEntities[i].object); this.currentFrameEntities[i].object.visible = true; } } this.lastFrameEntities = this.currentFrameEntities.slice(); } }
User prompt
Update as needed with: function renderEntities() { SpatialGrid.clear(); var allDynamicEntities = monsters.concat(treasures).concat(powerUps.filter(function (p) { return !p.collected; })).concat(fountains); if (gate) { allDynamicEntities.push(gate); } // Pre-filter by distance before adding to spatial grid var nearPlayerEntities = []; for (var i = 0; i < allDynamicEntities.length; i++) { var entity = allDynamicEntities[i]; var dx = entity.mapX - player.x; var dy = entity.mapY - player.y; var distSq = dx * dx + dy * dy; // Skip entities that are definitely too far (avoid sqrt) if (distSq > MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE) { entity.visible = false; continue; } entity.renderDistSq = distSq; // Cache for later nearPlayerEntities.push(entity); SpatialGrid.insert(entity); } // Now only process nearby entities for (var i = 0; i < nearPlayerEntities.length; i++) { var entity = nearPlayerEntities[i]; var dist = Math.sqrt(entity.renderDistSq); // Only sqrt when needed // ... rest of rendering logic } }
User prompt
Update as needed with: // Add this as a class property, calculate once if (!self.shadingCache) { self.shadingCache = []; var SHADE_START_FACTOR_WALLS = 0.3; var FLOOR_MIN_SHADE = 0.28; var CEILING_MIN_SHADE = 0.18; var WALL_BASE_GRADIENT = 0.20; for (var i = 0; i < numStrips; i++) { var y = HORIZON_Y + (i * stripHeight); var distanceFromHorizon = y - HORIZON_Y; var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; var distanceRatio = worldDistance / MAX_RENDER_DISTANCE; var shadeFactor = Math.max(FLOOR_MIN_SHADE, 1 - distanceRatio / SHADE_START_FACTOR_WALLS); var horizonProximity = 1 - distanceFromHorizon / (HORIZON_Y * 0.3); if (horizonProximity > 0) { shadeFactor -= horizonProximity * WALL_BASE_GRADIENT; } self.shadingCache[i] = shadeFactor; } } // Then in update, just use: floorStrip.alpha = self.shadingCache[stripIndex];
User prompt
Update as needed with: self.update = function (playerX, playerY, playerDir) { // Only update if player moved significantly or direction changed significantly if (self.lastPlayerX !== undefined) { var posChange = Math.abs(playerX - self.lastPlayerX) + Math.abs(playerY - self.lastPlayerY); var dirChange = Math.abs(playerDir - self.lastPlayerDir); // Floor/ceiling don't change much with small movements if (posChange < 0.1 && dirChange < 0.05) { return; // Skip update } } self.lastPlayerX = playerX; self.lastPlayerY = playerY; self.lastPlayerDir = playerDir; // ... rest of update code };
User prompt
Increase enemy numbers in the first level to 7. And make sure the monsters number accurately reflects the number when the level starts.
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var BossMonster = Container.expand(function () { var self = Container.call(this); var frameContainer = new Container(); self.addChild(frameContainer); var walkFrame1 = LK.getAsset('bosswalk1', { anchorX: 0.5, anchorY: 0.5 }); var walkFrame2 = LK.getAsset('bosswalk2', { anchorX: 0.5, anchorY: 0.5 }); var attackFrameAsset = LK.getAsset('bossattack', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); frameContainer.addChild(walkFrame1); frameContainer.addChild(walkFrame2); frameContainer.addChild(attackFrameAsset); walkFrame1.alpha = 1; walkFrame2.alpha = 0; self.currentFrame = 1; self.animationTick = 0; self.currentFireballTimer = null; self.mapX = 0; self.mapY = 0; self.health = BOSS_CURRENT_BASE_HEALTH; self.moveSpeed = 0.15; self.lastMoveTime = 0; self.canSeePlayer = false; self.pathToPlayer = []; self.attackCooldown = 3000; self.lastAttackTime = 0; self.canAttack = true; self.isAttacking = false; self.attackDuration = 800; self.fireballCooldown = BOSS_FIREBALL_COOLDOWN; self.lastFireballTime = 0; self.canShootFireball = true; self.createChargingEffect = function (targetX, targetY, targetZ) { if (!particlePoolManager || !particleExplosionContainer) { return; } var chargingParticleCount = 25; var chargingRadius = 2.0; var chargingDuration = 950; for (var i = 0; i < chargingParticleCount; i++) { var particle = particlePoolManager.getParticle(); if (!particle) { continue; } var angle = i / chargingParticleCount * MATH_PI_2; var spawnRadius = chargingRadius * (0.7 + Math.random() * 0.6); var startX = self.mapX + Math.cos(angle) * spawnRadius; var startY = self.mapY + Math.sin(angle) * spawnRadius; var startZ = targetZ + (Math.random() - 0.5) * 0.4; var dx = targetX - startX; var dy = targetY - startY; var dz = targetZ - startZ; var distance = Math.sqrt(dx * dx + dy * dy + dz * dz); var durationInTicks = chargingDuration / 16.67; var vx = dx / durationInTicks; var vy = dy / durationInTicks; var vz = dz / durationInTicks; var spiralSpeed = 0.002; var spiralAngle = angle + Math.random() * MATH_PI_HALF; vx += Math.cos(spiralAngle) * spiralSpeed; vy += Math.sin(spiralAngle) * spiralSpeed; var life = Math.floor(durationInTicks) + 10; var chargingColor = 0xFF8C00; particle.init(startX, startY, startZ, vx, vy, vz, life, chargingColor); particle.collisionImmunity = life; particle.isChargingParticle = true; particleExplosionContainer.addChild(particle); } }; self.performFireballAttack = function (playerWorldX, playerWorldY) { if (!self.canShootFireball || !self.parent) { return; } self.canShootFireball = false; self.lastFireballTime = Date.now(); if (!self.isAttacking) { self.performAttackAnimation(); } var targetX = playerWorldX; var targetY = playerWorldY; var fireballStartX = self.mapX + Math.cos(player.dir) * 0.5; var fireballStartY = self.mapY + Math.sin(player.dir) * 0.5; var fireballStartZ = 0.8; self.createChargingEffect(fireballStartX, fireballStartY, fireballStartZ); LK.getSound('bosschant').play(); var fireballTimer = LK.setTimeout(function () { if (!self.parent) { return; } var fireball = new FireballProjectile(); fireball.fire(fireballStartX, fireballStartY, fireballStartZ, targetX, targetY); projectiles.push(fireball); projectileLayer.addChild(fireball); LK.getSound('fireball').play(); }, 1000); self.currentFireballTimer = fireballTimer; var resetCooldown = function resetCooldown() { if (self.parent) { self.canShootFireball = true; self.currentFireballTimer = null; } }; LK.setTimeout(resetCooldown, self.fireballCooldown); }; self.updateAnimation = function () { if (self.isAttacking) { return; } self.animationTick++; if (self.animationTick >= 20) { self.animationTick = 0; self.currentFrame = self.currentFrame === 1 ? 2 : 1; if (self.currentFrame === 1) { walkFrame1.alpha = 1; walkFrame2.alpha = 0; } else { walkFrame1.alpha = 0; walkFrame2.alpha = 1; } } }; self.performAttackAnimation = function () { if (self.isAttacking || !self.parent) { return; } self.isAttacking = true; walkFrame1.alpha = 0; walkFrame2.alpha = 0; attackFrameAsset.alpha = 1; LK.setTimeout(function () { if (!self.parent) { return; } attackFrameAsset.alpha = 0; if (self.currentFrame === 1) { walkFrame1.alpha = 1; walkFrame2.alpha = 0; } else { walkFrame1.alpha = 0; walkFrame2.alpha = 1; } self.isAttacking = false; }, self.attackDuration); }; self.takeDamage = function (damageAmount) { self.health -= damageAmount; LK.getSound('hit').play(); LK.effects.flashObject(frameContainer, 0xff0000, 400); if (self.health <= 0) { if (self.currentFireballTimer) { LK.clearTimeout(self.currentFireballTimer); self.currentFireballTimer = null; } walkFrame1.alpha = 0; walkFrame2.alpha = 0; if (attackFrameAsset) { attackFrameAsset.alpha = 0; } return true; } return false; }; return self; }); 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 }); if (direction === 'up') { arrowSprite.rotation = 0; } else if (direction === 'right') { arrowSprite.rotation = MATH_PI_HALF; } 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') { if (self.isInteract) { buttonSprite.tint = 0x33aa33; buttonSprite.alpha = 1; } else { buttonSprite.tint = 0xaa3333; buttonSprite.alpha = 0.3 + ratio * 0.7; } } }; self.down = function (x, y, obj) { self.pressed = true; buttonSprite.alpha = 0.7; if (self.direction === 'attack') { obj.stopPropagation = true; if (controlButtons && controlButtons.joystick && controlButtons.joystick.active) { joystickOverrideActive = true; joystickOverrideX = controlButtons.joystick.normalizedX; joystickOverrideY = controlButtons.joystick.normalizedY; } else { joystickOverrideActive = false; } if (self.isInteract && self.interactTarget) { if (self.interactTarget instanceof Treasure && !self.interactTarget.isOpen) { var treasureToOpen = self.interactTarget; treasureToOpen.openTreasure(); LK.getSound('treasureopen').play(); // player.score += treasureToOpen.value * 5; // Score removed updateUI(); var t = treasureToOpen; var powerUpTypes = ['speed', 'defense', 'health', 'attack']; var randomType = powerUpTypes[fastFloor(Math.random() * powerUpTypes.length)]; var powerUpFromChest = new PowerUp(randomType); powerUpFromChest.mapX = t.mapX; powerUpFromChest.mapY = t.mapY; powerUpFromChest.collected = false; powerUpFromChest.spawnScaleMultiplier = 0.1; powerUpFromChest.verticalOffset = 0; powerUpFromChest.currentBobOffset = 0; powerUpFromChest.hasReachedBobHeight = false; powerUps.push(powerUpFromChest); if (typeof dynamicEntitiesContainer !== "undefined") { dynamicEntitiesContainer.addChild(powerUpFromChest); } tween(powerUpFromChest, { spawnScaleMultiplier: 1.0, verticalOffset: 0.10 }, { duration: 1200, easing: tween.elasticOut, onFinish: function onFinish() { powerUpFromChest.hasReachedBobHeight = true; powerUpFromChest.startBobbingAnimation(); } }); LK.setTimeout(function () { map[fastFloor(t.mapY)][fastFloor(t.mapX)].removeTreasure(); var idx = treasures.indexOf(t); if (idx !== -1) { treasures.splice(idx, 1); } checkLevelCompletion(); }, 500); } else if (self.interactTarget instanceof PowerUp && !self.interactTarget.collected) { var powerUpToCollect = self.interactTarget; powerUpToCollect.applyAndRemove(); } else if (self.interactTarget instanceof Fountain) { var fountainToUse = self.interactTarget; fountainToUse.interact(); } else if (self.interactTarget instanceof Gate) { var currentGateInstance = self.interactTarget; if (monsters.length > 0) { var warningText = new Text2('Defeat all monsters\nbefore exiting!', { size: 60, fill: 0xFF5555, stroke: 0x000000, strokeThickness: 6, align: 'center' }); warningText.anchor.set(0.5, 0.5); warningText.x = SCREEN_WIDTH_HALF; warningText.y = SCREEN_HEIGHT_HALF; gameLayer.addChild(warningText); LK.setTimeout(function () { if (warningText.parent) { warningText.parent.removeChild(warningText); } warningText.destroy(); }, 2000); } else { if (gateInteractionInProgress) { return; } gateInteractionInProgress = true; LK.getSound('collect').play(); // player.score += 50 * player.level; // Score removed if (map[fastFloor(currentGateInstance.mapY)] && map[fastFloor(currentGateInstance.mapY)][fastFloor(currentGateInstance.mapX)]) { map[fastFloor(currentGateInstance.mapY)][fastFloor(currentGateInstance.mapX)].removeGate(); } currentGateInstance.destroy(); gate = null; player.level++; storage.level = player.level; player.health = Math.min(player.health + 2, player.maxHealth); updateUI(); var levelCompleteText = new Text2('Level ' + (player.level - 1) + ' Complete!\nYou found the exit!', { size: 80, fill: 0x00FF55, stroke: 0x000000, strokeThickness: 7, align: 'center' }); levelCompleteText.anchor.set(0.5, 0.5); levelCompleteText.x = SCREEN_WIDTH_HALF; levelCompleteText.y = SCREEN_HEIGHT_HALF; gameLayer.addChild(levelCompleteText); LK.setTimeout(function () { if (levelCompleteText.parent) { levelCompleteText.parent.removeChild(levelCompleteText); } levelCompleteText.destroy(); generateMap(); gateInteractionInProgress = false; }, 2000); } } } else if (canAttack) { attackAction(); } } }; self.up = function (x, y, obj) { self.pressed = false; if (self.direction === 'attack') { joystickOverrideActive = false; if (!canAttack) { buttonSprite.alpha = 0.3; } else { buttonSprite.alpha = 1; } } else { buttonSprite.alpha = 1; } }; return self; }); var EyeballMonster = Container.expand(function () { var self = Container.call(this); self.flySprite = self.attachAsset('eyeballfly', { anchorX: 0.5, anchorY: 0.5 }); self.attackSprite = self.attachAsset('eyeballattack', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.health = EYEBALL_CURRENT_BASE_HEALTH; self.baseAttack = EYEBALL_CURRENT_BASE_ATTACK; self.mapX = 0; self.mapY = 0; self.moveSpeed = 0.25; self.isAttacking = false; self.attackDuration = 600; self.attackCooldown = 2000; self.lastAttackTime = 0; self.canAttack = true; self.lastSoundTime = 0; self.soundInterval = 2000 + Math.random() * 1000; self.soundAudibleDistance = 10.0; self.baseWorldYOffset = 0.4; self.canSeePlayer = false; self.pathToPlayer = []; self.lastMoveTime = 0; self.performAttackAnimation = function () { if (self.isAttacking || !self.parent) { return; } self.isAttacking = true; self.attackSprite.alpha = 1; self.flySprite.alpha = 0; LK.setTimeout(function () { if (!self.parent) { return; } self.attackSprite.alpha = 0; self.flySprite.alpha = 1; self.isAttacking = false; }, self.attackDuration); }; self.takeDamage = function (damageAmount) { self.health -= damageAmount; LK.getSound('hit').play(); LK.effects.flashObject(self, 0xff0000, 400); if (self.health <= 0) { if (self.attackSprite) { self.attackSprite.alpha = 0; } if (self.flySprite) { self.flySprite.alpha = 0; } return true; } return false; }; self.updateAnimation = function () {}; self.updateSoundBehavior = function () { if (!self.visible) { return; } var currentTime = Date.now(); if (currentTime - self.lastSoundTime > self.soundInterval) { var dx = player.x - self.mapX; var dy = player.y - self.mapY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.soundAudibleDistance) { var volume = Math.max(0, 1.0 - dist / self.soundAudibleDistance); if (isNaN(volume) || volume < 0) { volume = 0; } if (volume > 0) { LK.getSound('eyeball').play({ volume: volume }); } self.lastSoundTime = currentTime; self.soundInterval = 2000 + Math.random() * 1000; } else { self.lastSoundTime = currentTime - Math.random() * self.soundInterval * 0.5; } } }; return self; }); var FireballProjectile = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('fireballprojectile', { anchorX: 0.5, anchorY: 0.5 }); self.sprite = sprite; self.worldX = 0; self.worldY = 0; self.worldZ = 0.5; self.dirX = 0; self.dirY = 0; self.dirZ = 0; self.speed = FIREBALL_SPEED; self.damage = FIREBALL_DAMAGE; self.active = false; self.distanceTraveled = 0; self.maxDistance = FIREBALL_MAX_DISTANCE; self.fire = function (startX, startY, startZ, targetX, targetY) { self.worldX = startX; self.worldY = startY; self.worldZ = startZ; var dx = targetX - startX; var dy = targetY - startY; var targetPlayerZ = 0.5; var dz = targetPlayerZ - startZ; var dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist > 0) { self.dirX = dx / dist; self.dirY = dy / dist; self.dirZ = dz / dist; } else { self.dirX = 1; self.dirY = 0; self.dirZ = 0; } self.active = true; self.distanceTraveled = 0; self.visible = true; sprite.rotation = Math.atan2(self.dirY, self.dirX); }; self.update = function () { if (!self.active) { return false; } self.worldX += self.dirX * self.speed; self.worldY += self.dirY * self.speed; self.worldZ += self.dirZ * self.speed; self.distanceTraveled += self.speed; if (self.distanceTraveled >= self.maxDistance) { createParticleExplosion(self.worldX, self.worldY, 10, 0xFF6347, false); return true; } var mapX = fastFloor(self.worldX); var mapY = fastFloor(self.worldY); if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE || map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 1) { createParticleExplosion(self.worldX, self.worldY, PROJECTILE_WALL_HIT_PARTICLE_COUNT, 0xFF4500, true); return true; } if (self.worldZ < 0.1) { createParticleExplosion(self.worldX, self.worldY, 10, 0xFF6347, false); return true; } self.updateScreenPosition(); return false; }; self.updateScreenPosition = function () { if (!self.visible || !self.active) { return; } var dx = self.worldX - player.x; var dy = self.worldY - player.y; var distToPlayerPlane = Math.sqrt(dx * dx + dy * dy); if (distToPlayerPlane < 0.1) { distToPlayerPlane = 0.1; } var angle = Math.atan2(dy, dx) - player.dir; while (angle < -Math.PI) { angle += MATH_PI_2; } while (angle > Math.PI) { angle -= MATH_PI_2; } if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE) { self.x = SCREEN_WIDTH_HALF + angle / HALF_FOV * SCREEN_WIDTH_HALF; var screenY_horizon = SCREEN_HEIGHT_HALF; var z_to_screen_pixels_factor = WALL_HEIGHT_FACTOR; var player_camera_height_projection_factor = WALL_HEIGHT_FACTOR * 0.5; var screenY_for_floor_at_dist = screenY_horizon + player_camera_height_projection_factor / distToPlayerPlane; var screenY_offset_due_to_worldZ = self.worldZ * z_to_screen_pixels_factor / distToPlayerPlane; self.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ; var fireballAssetOriginalHeight = self.sprite.height || 100; var visualScaleFactor = 0.6; var scale = Math.max(0.05, WALL_HEIGHT_FACTOR / distToPlayerPlane / fireballAssetOriginalHeight * visualScaleFactor); self.sprite.scale.set(scale); var baseColor = 0xFFFFAA; var shadeFactor = 1.0; var r_base = baseColor >> 16 & 0xFF; var g_base = baseColor >> 8 & 0xFF; var b_base = baseColor & 0xFF; var r = fastFloor(r_base * shadeFactor); var g = fastFloor(g_base * shadeFactor); var b = fastFloor(b_base * shadeFactor); self.sprite.tint = r << 16 | g << 8 | b; self.visible = true; } else { self.visible = false; } }; return self; }); var FloorCaster = Container.expand(function () { var self = Container.call(this); var SCREEN_WIDTH = 2048; var SCREEN_HEIGHT = 2732; var HORIZON_Y = SCREEN_HEIGHT / 2; var floorContainer = new Container(); var ceilingContainer = new Container(); self.addChild(floorContainer); self.addChild(ceilingContainer); var floorStrips = []; var ceilingStrips = []; var stripHeight = 4; var numStrips = Math.ceil(HORIZON_Y / stripHeight); self.lastPlayerX = undefined; self.lastPlayerY = undefined; self.lastPlayerDir = undefined; self.shadingCache = null; // Initialize for floor shading cache // Pre-create all strips once for (var i = 0; i < numStrips; i++) { var floorStrip = LK.getAsset('mapFloor', { anchorX: 0, anchorY: 0 }); floorStrips.push(floorStrip); floorContainer.addChild(floorStrip); var ceilingStrip = LK.getAsset('ceiling', { anchorX: 0, anchorY: 0 }); ceilingStrips.push(ceilingStrip); ceilingContainer.addChild(ceilingStrip); } self.update = function (playerX, playerY, playerDir) { // Only update if player moved significantly or direction changed significantly if (self.lastPlayerX !== undefined) { var posChange = Math.abs(playerX - self.lastPlayerX) + Math.abs(playerY - self.lastPlayerY); var dirChange = Math.abs(playerDir - self.lastPlayerDir); // Floor/ceiling don't change much with small movements if (posChange < 0.1 && dirChange < 0.05) { return; // Skip update } } self.lastPlayerX = playerX; self.lastPlayerY = playerY; self.lastPlayerDir = playerDir; // Remove the expensive removeChildren() calls // floorContainer.removeChildren(); // DELETE THIS // ceilingContainer.removeChildren(); // DELETE THIS var SHADE_START_FACTOR_WALLS = 0.3; var FLOOR_MIN_SHADE = 0.28; var CEILING_MIN_SHADE = 0.18; // This and other constants are kept for the ceiling calculation later var WALL_BASE_GRADIENT = 0.20; // Calculate floor shading cache once if (!self.shadingCache) { self.shadingCache = []; // Constants for cache calculation from the provided snippet // These are local to this block and won't conflict with constants for ceiling rendering var SHADE_START_FACTOR_WALLS_CACHE = 0.3; var FLOOR_MIN_SHADE_CACHE = 0.28; // var CEILING_MIN_SHADE_CACHE = 0.18; // Not used for floor cache, but part of provided snippet var WALL_BASE_GRADIENT_CACHE = 0.20; // numStrips, HORIZON_Y, stripHeight are accessible from FloorCaster's outer scope // WALL_HEIGHT_FACTOR, MAX_RENDER_DISTANCE are global for (var i_cache = 0; i_cache < numStrips; i_cache++) { var y_cache_val = HORIZON_Y + i_cache * stripHeight; var distanceFromHorizon_cache = y_cache_val - HORIZON_Y; var shadeFactor_cached; if (distanceFromHorizon_cache <= 0) { shadeFactor_cached = FLOOR_MIN_SHADE_CACHE; } else { var worldDistance_cache = WALL_HEIGHT_FACTOR / distanceFromHorizon_cache; if (worldDistance_cache > MAX_RENDER_DISTANCE) { shadeFactor_cached = FLOOR_MIN_SHADE_CACHE; // Or a value indicating it's too far } else { var distanceRatio_cache = worldDistance_cache / MAX_RENDER_DISTANCE; shadeFactor_cached = Math.max(FLOOR_MIN_SHADE_CACHE, 1 - distanceRatio_cache / SHADE_START_FACTOR_WALLS_CACHE); var horizonProximity_cache = 1 - distanceFromHorizon_cache / (HORIZON_Y * 0.3); if (horizonProximity_cache > 0) { shadeFactor_cached -= horizonProximity_cache * WALL_BASE_GRADIENT_CACHE; } } } self.shadingCache[i_cache] = Math.max(0, Math.min(1, shadeFactor_cached)); // Clamp alpha } } var stripIndex = 0; for (var y = HORIZON_Y; y < SCREEN_HEIGHT; y += stripHeight) { var distanceFromHorizon = y - HORIZON_Y; // Still needed for visibility logic if (distanceFromHorizon <= 0 || stripIndex >= floorStrips.length) { // Ensure strip exists before trying to set visible false if we continue early if (stripIndex < floorStrips.length) { floorStrips[stripIndex].visible = false; } stripIndex++; continue; } var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; //{5A} // Still needed for visibility optimization var floorStrip = floorStrips[stripIndex]; // Check if strip should be visible based on distance and if cache entry exists if (worldDistance > MAX_RENDER_DISTANCE || stripIndex >= self.shadingCache.length) { floorStrip.visible = false; stripIndex++; continue; } floorStrip.x = 0; floorStrip.y = y; floorStrip.width = SCREEN_WIDTH; floorStrip.height = stripHeight; floorStrip.alpha = self.shadingCache[stripIndex]; // Use cached value floorStrip.visible = true; stripIndex++; } // Hide unused strips for (var i = stripIndex; i < floorStrips.length; i++) { floorStrips[i].visible = false; } // Same optimization for ceiling... stripIndex = 0; for (var y = HORIZON_Y - stripHeight; y >= 0; y -= stripHeight) { var distanceFromHorizon = HORIZON_Y - y; if (distanceFromHorizon <= 0 || stripIndex >= ceilingStrips.length) { continue; } var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; if (worldDistance > MAX_RENDER_DISTANCE) { ceilingStrips[stripIndex].visible = false; stripIndex++; continue; } var distanceRatio = worldDistance / MAX_RENDER_DISTANCE; var shadeFactor = Math.max(CEILING_MIN_SHADE, 1 - distanceRatio / SHADE_START_FACTOR_WALLS); var horizonProximity = 1 - distanceFromHorizon / (HORIZON_Y * 0.3); if (horizonProximity > 0) { shadeFactor -= horizonProximity * WALL_BASE_GRADIENT; } var ceilingStrip = ceilingStrips[stripIndex]; ceilingStrip.x = 0; ceilingStrip.y = y; ceilingStrip.width = SCREEN_WIDTH; ceilingStrip.height = stripHeight; ceilingStrip.alpha = shadeFactor; ceilingStrip.visible = true; stripIndex++; } // Hide unused ceiling strips for (var i = stripIndex; i < ceilingStrips.length; i++) { ceilingStrips[i].visible = false; } }; return self; }); var Fountain = Container.expand(function () { var self = Container.call(this); self.sprite = self.attachAsset('fountain1', { anchorX: 0.5, anchorY: 0.5 }); self.mapX = 0; self.mapY = 0; self.state = 'full'; self.isInteractable = true; self.particleTimer = 0; self.particleInterval = 4; self.soundAudibleDistance = FOUNTAIN_SOUND_AUDIBLE_DISTANCE; self.soundPlaybackInterval = FOUNTAIN_SOUND_INTERVAL; self.lastSoundPlayTime = 0; self.init = function (mapX, mapY) { self.mapX = mapX; self.mapY = mapY; self.sprite.tint = 0xFFFFFF; }; self.updateParticles = function () { if (self.state === 'full' && self.visible && particlePoolManager && particleExplosionContainer) { self.particleTimer++; if (self.particleTimer >= self.particleInterval) { self.particleTimer = 0; var count = 1 + fastFloor(Math.random() * 2); for (var i = 0; i < count; i++) { var particle = particlePoolManager.getParticle(); if (!particle) { continue; } var emissionWorldX = self.mapX; var emissionWorldY = self.mapY; var emissionWorldZ = 1.0; var splashAngle = Math.random() * MATH_PI_2; var horizontalSpeed = Math.random() * 0.0020 + 0.0005; var vx = Math.cos(splashAngle) * horizontalSpeed; var vy = Math.sin(splashAngle) * horizontalSpeed; var vz = Math.random() * 0.018 + 0.012; var life = 50 + Math.random() * 40; var tintColor = 0x66B2FF; particle.init(emissionWorldX, emissionWorldY, emissionWorldZ, vx, vy, vz, life, tintColor); particle.collisionImmunity = 3; particleExplosionContainer.addChild(particle); } } } }; self.interact = function () { if (self.state === 'full' && self.isInteractable) { self.state = 'empty'; self.isInteractable = false; self.sprite.tint = 0x707070; player.health = player.maxHealth; updateUI(); showPowerUpEffectText("Your health has been fully restored!"); LK.getSound('collect').play(); } }; self.updateSoundBehavior = function () { if (!self.visible || self.state !== 'full') { return; } var currentTime = Date.now(); if (currentTime - self.lastSoundPlayTime > self.soundPlaybackInterval) { var dx = player.x - self.mapX; var dy = player.y - self.mapY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.soundAudibleDistance) { var volume = Math.max(0, 1.0 - dist / self.soundAudibleDistance); if (isNaN(volume) || volume < 0) { volume = 0; } if (volume > 0) { LK.getSound('fountainsplash').play({ volume: volume }); self.lastSoundPlayTime = currentTime; } } } }; return self; }); var Gate = Container.expand(function () { var self = Container.call(this); var gateSprite = self.attachAsset('staircase', { anchorX: 0.5, anchorY: 0.5 }); gateSprite.tint = 0x00FF00; self.gateSprite = gateSprite; self.mapX = 0; self.mapY = 0; 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 }); } }); }; _animateGate(); return self; }); var ImpMonster = Container.expand(function () { var self = Container.call(this); var frameContainer = new Container(); self.addChild(frameContainer); var impScale = 0.85; self.sizeMultiplier = 0.7; var walkFrame1 = LK.getAsset('impwalk1', { anchorX: 0.5, anchorY: 0.5, scaleX: impScale, scaleY: impScale }); var walkFrame2 = LK.getAsset('impwalk2', { anchorX: 0.5, anchorY: 0.5, scaleX: impScale, scaleY: impScale }); var attackFrameAsset = LK.getAsset('impattack', { anchorX: 0.5, anchorY: 0.5, scaleX: impScale, scaleY: impScale, alpha: 0 }); frameContainer.addChild(walkFrame1); frameContainer.addChild(walkFrame2); frameContainer.addChild(attackFrameAsset); walkFrame1.alpha = 1; walkFrame2.alpha = 0; self.currentFrame = 1; self.animationTick = 0; self.mapX = 0; self.mapY = 0; self.health = IMP_CURRENT_BASE_HEALTH; self.baseAttack = IMP_CURRENT_BASE_ATTACK; self.moveSpeed = 0.3; self.isAggroed = false; self.lastCryTime = 0; self.cryInterval = 3000 + Math.random() * 2000; self.lastMoveTime = 0; self.canSeePlayer = false; self.pathToPlayer = []; self.attackCooldown = 1500; self.lastAttackTime = 0; self.canAttack = true; self.isAttacking = false; self.attackDuration = 500; self.updateAnimation = function () { if (self.isAttacking) { return; } self.animationTick++; if (self.animationTick >= 15) { self.animationTick = 0; self.currentFrame = self.currentFrame === 1 ? 2 : 1; if (self.currentFrame === 1) { walkFrame1.alpha = 1; walkFrame2.alpha = 0; } else { walkFrame1.alpha = 0; walkFrame2.alpha = 1; } } }; self.performAttackAnimation = function () { if (self.isAttacking || !self.parent) { return; } self.isAttacking = true; walkFrame1.alpha = 0; walkFrame2.alpha = 0; attackFrameAsset.alpha = 1; LK.setTimeout(function () { if (!self.parent) { return; } attackFrameAsset.alpha = 0; if (self.currentFrame === 1) { walkFrame1.alpha = 1; walkFrame2.alpha = 0; } else { walkFrame1.alpha = 0; walkFrame2.alpha = 1; } self.isAttacking = false; }, self.attackDuration); }; self.updateSoundBehavior = function () { if (!self.isAggroed || !self.parent || !self.visible) { return; } var currentTime = Date.now(); if (currentTime - self.lastCryTime > self.cryInterval) { var dx = player.x - self.mapX; var dy = player.y - self.mapY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < MAX_AUDIBLE_DISTANCE_IMP_CRY) { var volume = Math.max(0, 1.0 - dist / MAX_AUDIBLE_DISTANCE_IMP_CRY); if (isNaN(volume) || volume < 0) { volume = 0; } if (volume > 0) { LK.getSound('impCry').play({ volume: volume }); } self.lastCryTime = currentTime; self.cryInterval = 3000 + Math.random() * 2000; } else { self.lastCryTime = currentTime - Math.random() * self.cryInterval * 0.5; } } }; self.takeDamage = function (damageAmount) { self.health -= damageAmount; LK.getSound('hit').play(); LK.effects.flashObject(frameContainer, 0xff0000, 400); if (self.health <= 0) { walkFrame1.alpha = 0; walkFrame2.alpha = 0; if (attackFrameAsset) { attackFrameAsset.alpha = 0; } return true; } return false; }; return self; }); var JoystickController = Container.expand(function () { var self = Container.call(this); var baseRadius = 150; var baseSprite = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 5.0, scaleY: 5.0, alpha: 0.4 }); var handleRadius = 100; var handleSprite = self.attachAsset('controlButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.0, scaleY: 3.0 }); self.active = false; self.startX = 0; self.startY = 0; self.maxDistance = baseRadius; self.normalizedX = 0; self.normalizedY = 0; self.resetHandle = function () { handleSprite.x = 0; handleSprite.y = 0; self.normalizedX = 0; self.normalizedY = 0; self.active = false; }; self.down = function (x, y, obj) { self.active = true; self.startX = x; self.startY = y; }; self.move = function (x, y, obj) { if (!self.active) { return; } var dx = x - self.startX; var dy = y - self.startY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { if (distance > self.maxDistance) { dx = dx * self.maxDistance / distance; dy = dy * self.maxDistance / distance; distance = self.maxDistance; } handleSprite.x = dx; handleSprite.y = dy; self.normalizedX = dx / self.maxDistance; self.normalizedY = dy / self.maxDistance; } else { self.resetHandle(); } }; self.up = function (x, y, obj) { self.resetHandle(); }; self.resetHandle(); return self; }); var MapCell = Container.expand(function () { var self = Container.call(this); self.type = 0; 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.gate) { // Check if this cell contains the gate self.attachAsset('mapGateCell', { anchorX: 0, anchorY: 0 }); } else if (self.type === 1) { self.attachAsset('mapWall', { anchorX: 0, anchorY: 0 }); } else { // type === 0 and not gate self.attachAsset('mapFloor', { anchorX: 0, anchorY: 0 }); } }; self.addMonster = function () { if (self.type === 0 && !self.monster && !self.treasure && !self.gate) { self.monster = true; return true; } return false; }; self.addTreasure = function () { if (self.type === 0 && !self.monster && !self.treasure && !self.gate) { 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 frameContainer = new Container(); self.addChild(frameContainer); var monsterFrame1 = LK.getAsset('demonOgreWalk1', { anchorX: 0.5, anchorY: 0.5 }); var monsterFrame2 = LK.getAsset('demonOgreWalk2', { anchorX: 0.5, anchorY: 0.5 }); frameContainer.addChild(monsterFrame1); frameContainer.addChild(monsterFrame2); monsterFrame1.alpha = 1; monsterFrame2.alpha = 0; self.currentFrame = 1; self.animationTick = 0; self.mapX = 0; self.mapY = 0; self.health = OGRE_CURRENT_BASE_HEALTH; self.baseAttack = OGRE_CURRENT_BASE_ATTACK; self.lastMoveTime = 0; self.canSeePlayer = false; self.pathToPlayer = []; self.attackCooldown = 2000; self.lastAttackTime = 0; self.canAttack = true; self.soundAudibleDistance = OGRE_SOUND_AUDIBLE_DISTANCE; self.soundPlaybackInterval = OGRE_SOUND_INTERVAL + Math.random() * OGRE_SOUND_INTERVAL_RANDOMNESS; self.lastSoundTime = 0; self.updateSoundBehavior = function () { if (!self.visible || !self.parent) { return; } var currentTime = Date.now(); if (currentTime - self.lastSoundTime > self.soundPlaybackInterval) { var dx = player.x - self.mapX; var dy = player.y - self.mapY; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.soundAudibleDistance) { var volume = Math.max(0, 1.0 - dist / self.soundAudibleDistance); if (isNaN(volume) || volume < 0) { volume = 0; } if (volume > 0) { LK.getSound('ogre').play({ volume: volume }); } self.lastSoundTime = currentTime; self.soundPlaybackInterval = OGRE_SOUND_INTERVAL + Math.random() * OGRE_SOUND_INTERVAL_RANDOMNESS; } else { self.lastSoundTime = currentTime - Math.random() * self.soundPlaybackInterval * 0.5; } } }; self.updateAnimation = function () { self.animationTick++; if (self.animationTick >= 15) { self.animationTick = 0; self.currentFrame = self.currentFrame === 1 ? 2 : 1; if (self.currentFrame === 1) { monsterFrame1.alpha = 1; monsterFrame2.alpha = 0; } else { monsterFrame1.alpha = 0; monsterFrame2.alpha = 1; } } }; self.takeDamage = function (damageAmount) { self.health -= damageAmount; LK.getSound('hit').play(); LK.effects.flashObject(frameContainer, 0xff0000, 400); return self.health <= 0; }; return self; }); var Particle = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5 }); self.worldX = 0; self.worldY = 0; self.worldZ = 0; self.vx = 0; self.vy = 0; self.vz = 0; self.life = 0; self.maxLife = 480; self.bounces = 0; self.tintColor = 0xFFFFFF; var PARTICLE_WORLD_GRAVITY_EFFECT = 0.0035; var PARTICLE_Z_DAMPING = 0.6; var PARTICLE_XY_DAMPING = 0.4; var PARTICLE_GROUND_FRICTION = 0.8; var PARTICLE_MAX_BOUNCES = 4; var PARTICLE_VISUAL_SIZE_AT_UNIT_DISTANCE = 0.08; var MAX_RENDER_DISTANCE_PARTICLES = 12; self.init = function (wX, wY, wZ, velX, velY, velZ, particleLife, tintColor) { self.worldX = wX; self.worldY = wY; self.worldZ = wZ; self.vx = velX; self.vy = velY; self.vz = velZ; self.life = particleLife || self.maxLife; self.bounces = 0; self.collisionImmunity = 0; self.isChargingParticle = false; self.tintColor = tintColor || 0xFFFFFF; graphics.tint = self.tintColor; graphics.alpha = 1; self.visible = false; }; self.update = function () { if (self.life <= 0 || self.bounces >= PARTICLE_MAX_BOUNCES) { self.visible = false; return false; } var prevWorldX, prevWorldY; if (self.isChargingParticle) { prevWorldX = self.worldX; prevWorldY = self.worldY; self.worldX += self.vx; self.worldY += self.vy; self.worldZ += self.vz; } else { prevWorldX = self.worldX; prevWorldY = self.worldY; self.worldX += self.vx; self.worldY += self.vy; self.worldZ += self.vz; self.vz -= PARTICLE_WORLD_GRAVITY_EFFECT; if (self.worldZ < 0) { self.worldZ = 0; self.vz *= -PARTICLE_Z_DAMPING; self.vx *= PARTICLE_GROUND_FRICTION; self.vy *= PARTICLE_GROUND_FRICTION; self.bounces++; if (Math.abs(self.vz) < 0.005 && PARTICLE_WORLD_GRAVITY_EFFECT > Math.abs(self.vz)) { self.vz = 0; } } if (self.collisionImmunity > 0) { self.collisionImmunity--; } else { var currentMapX = fastFloor(self.worldX); var currentMapY = fastFloor(self.worldY); if (currentMapX < 0 || currentMapX >= MAP_SIZE || currentMapY < 0 || currentMapY >= MAP_SIZE || map[currentMapY] && map[currentMapY][currentMapX] && map[currentMapY][currentMapX].type === 1) { self.worldX = prevWorldX; self.worldY = prevWorldY; var pt_map_prev_x = fastFloor(prevWorldX); var pt_map_prev_y = fastFloor(prevWorldY); var reflectedX = false; var reflectedY = false; if (currentMapX !== pt_map_prev_x && map[pt_map_prev_y] && map[pt_map_prev_y][currentMapX] && map[pt_map_prev_y][currentMapX].type === 1) { self.vx *= -PARTICLE_XY_DAMPING; reflectedX = true; } if (currentMapY !== pt_map_prev_y && map[currentMapY] && map[currentMapY][pt_map_prev_x] && map[currentMapY][pt_map_prev_x].type === 1) { self.vy *= -PARTICLE_XY_DAMPING; reflectedY = true; } if (!reflectedX && !reflectedY) { if (Math.abs(self.vx) > Math.abs(self.vy) * 1.2) { self.vx *= -PARTICLE_XY_DAMPING; } else if (Math.abs(self.vy) > Math.abs(self.vx) * 1.2) { self.vy *= -PARTICLE_XY_DAMPING; } else { self.vx *= -PARTICLE_XY_DAMPING; self.vy *= -PARTICLE_XY_DAMPING; } } self.bounces++; } } } var dx = self.worldX - player.x; var dy = self.worldY - player.y; var distToPlayerPlane = Math.sqrt(dx * dx + dy * dy); if (distToPlayerPlane < 0.1) { distToPlayerPlane = 0.1; } var angle = Math.atan2(dy, dx) - player.dir; while (angle < -Math.PI) { angle += MATH_PI_2; } while (angle > Math.PI) { angle -= MATH_PI_2; } if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE_PARTICLES) { // Check if particle is occluded by a wall var isOccluded = false; // Cast a ray from player to particle to see if wall blocks it var rayResult = { wallHit: false, distance: 0, side: 0, wallType: 0, mapX: 0, mapY: 0 }; var rayDirX = (self.worldX - player.x) / distToPlayerPlane; var rayDirY = (self.worldY - player.y) / distToPlayerPlane; castRayDDAInto(player.x, player.y, rayDirX, rayDirY, rayResult); if (rayResult.wallHit && rayResult.distance < distToPlayerPlane - 0.2) { isOccluded = true; } if (!isOccluded) { self.visible = true; self.x = SCREEN_WIDTH_HALF + angle / HALF_FOV * SCREEN_WIDTH_HALF; var screenY_horizon = SCREEN_HEIGHT_HALF; var z_to_screen_pixels_factor = WALL_HEIGHT_FACTOR; var player_camera_height_projection_factor = WALL_HEIGHT_FACTOR * 0.5; var screenY_for_floor_at_dist = screenY_horizon + player_camera_height_projection_factor / distToPlayerPlane; var screenY_offset_due_to_worldZ = self.worldZ * z_to_screen_pixels_factor / distToPlayerPlane; self.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ; var particleAssetBaseSize = 100.0; var effectiveScreenSize = PARTICLE_VISUAL_SIZE_AT_UNIT_DISTANCE * WALL_HEIGHT_FACTOR / distToPlayerPlane; var screenScaleFactor = Math.max(0.05, effectiveScreenSize / particleAssetBaseSize); graphics.scale.set(screenScaleFactor); var lifeRatio = Math.max(0, self.life / self.maxLife); graphics.alpha = lifeRatio * 0.8 + 0.2; } else { self.visible = false; } } else { self.visible = false; } self.life--; if (self.life <= 0 || self.bounces >= PARTICLE_MAX_BOUNCES) { self.visible = false; return false; } return true; }; return self; }); var PowerUp = Container.expand(function (type) { var self = Container.call(this); self.powerUpType = type; self.spawnScaleMultiplier = 1.0; self.mapX = 0; self.mapY = 0; self.collected = false; self.verticalOffset = 0; self.currentBobOffset = 0; self.hasReachedBobHeight = false; var assetId = ''; switch (self.powerUpType) { case 'speed': assetId = 'speedup'; break; case 'defense': assetId = 'armorup'; break; case 'health': assetId = 'healthup'; break; case 'attack': assetId = 'attackup'; break; default: assetId = 'treasure'; break; } var powerUpSprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.powerUpSprite = powerUpSprite; self.startBobbingAnimation = function () { if (self.collected || !self.parent || !self.hasReachedBobHeight) { return; } var bobAmplitude = 15; var bobSpeed = 1800; function animateUpInternal() { if (self.collected || !self.parent) { return; } tween(self, { currentBobOffset: -bobAmplitude }, { duration: bobSpeed / 2, easing: tween.easeInOut, onFinish: function onFinish() { if (self.collected || !self.parent) { return; } animateDownInternal(); } }); } function animateDownInternal() { if (self.collected || !self.parent) { return; } tween(self, { currentBobOffset: 0 }, { duration: bobSpeed / 2, easing: tween.easeInOut, onFinish: function onFinish() { if (self.collected || !self.parent) { return; } animateUpInternal(); } }); } animateUpInternal(); }; self.applyAndRemove = function () { if (self.collected) { return; } self.collected = true; var effectMessage = ""; switch (self.powerUpType) { case 'health': player.maxHealth += 1; player.health = Math.min(player.health + 1, player.maxHealth); effectMessage = "Max Health +1\nHealth Refilled +1"; updateUI(); break; case 'speed': player.moveSpeedMultiplier += 0.15; effectMessage = "Movement Speed Increased!"; break; case 'defense': player.defense += 1; effectMessage = "Defense Stat Increased!"; break; case 'attack': player.attackPower += 1; effectMessage = "Attack Power Increased!"; break; } if (effectMessage) { showPowerUpEffectText(effectMessage); } LK.getSound('powerup').play(); tween(self, { scaleX: self.scale.x * 0.1, scaleY: self.scale.y * 0.1, alpha: 0 }, { duration: 600, easing: tween.easeIn, onFinish: function onFinish() { if (self.parent) { self.parent.removeChild(self); } var index = powerUps.indexOf(self); if (index !== -1) { powerUps.splice(index, 1); } self.destroy(); } }); }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); var projectileSprite = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5 }); self.worldX = 0; self.worldY = 0; self.dirX = 0; self.dirY = 0; self.speed = 0.0375; self.active = false; self.distance = 0; self.maxDistance = 10; self.fire = function (screenX, screenY) { self.worldX = player.x + Math.cos(player.dir) * 0.3; self.worldY = player.y + Math.sin(player.dir) * 0.3; self.dirX = Math.cos(player.dir); self.dirY = Math.sin(player.dir); self.active = true; self.distance = 0; self.hitMonsters = []; self.wallHitSoundPlayed = false; LK.getSound('attack').play(); projectileSprite.scale.set(0.2, 0.2); self.initialScreenX = screenX || SCREEN_WIDTH_HALF; self.initialScreenY = screenY || 2732 - 300; self.x = self.initialScreenX; self.y = self.initialScreenY; self.visible = true; }; self.update = function (deltaTime) { if (!self.active) { return false; } self.worldX += self.dirX * self.speed; self.worldY += self.dirY * self.speed; self.distance += self.speed; var mapX = fastFloor(self.worldX); var mapY = fastFloor(self.worldY); var shouldRemove = false; var hitActualMapWall = false; if (self.distance >= self.maxDistance) { shouldRemove = true; } else if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) { shouldRemove = true; } else if (map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 1) { shouldRemove = true; hitActualMapWall = true; } if (shouldRemove) { if (hitActualMapWall) { createParticleExplosion(self.worldX, self.worldY, PROJECTILE_WALL_HIT_PARTICLE_COUNT, 0x87CEFA, true); if (!self.wallHitSoundPlayed) { LK.getSound('wallhit').play(); self.wallHitSoundPlayed = true; } } return true; } self.updateScreenPosition(); return false; }; self.updateScreenPosition = function () { var dx = self.worldX - player.x; var dy = self.worldY - 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) { self.x = SCREEN_WIDTH_HALF + angle / HALF_FOV * SCREEN_WIDTH_HALF; var targetY = SCREEN_HEIGHT_HALF + 40 - WALL_HEIGHT_FACTOR / dist * 0.1; var transitionFactor = Math.min(1.0, self.distance * 1); self.y = self.initialScreenY * (1 - transitionFactor) + targetY * transitionFactor; var scale = Math.max(0.1, 2 / dist); projectileSprite.scale.set(scale, scale); self.visible = true; } else { self.visible = false; } }; return self; }); var RaycastStrip = Container.expand(function () { var self = Container.call(this); var activeWallSprite = null; var currentTileIndex = null; self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance, textureX, side, tileIndex, wallMapX, wallMapY) { if (tileIndex < 1 || tileIndex > 64 || !Number.isInteger(tileIndex)) { self.clearStrip(); return; } var newTileAssetName = wallTextureLookup[tileIndex]; if (activeWallSprite && currentTileIndex !== tileIndex) { wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex); self.removeChild(activeWallSprite); activeWallSprite = null; currentTileIndex = null; } if (!activeWallSprite) { activeWallSprite = wallSpritePoolManager.getSprite(tileIndex); if (activeWallSprite) { self.addChild(activeWallSprite); currentTileIndex = tileIndex; } else { self.clearStrip(); return; } } activeWallSprite.width = stripWidth; activeWallSprite.height = wallHeight; activeWallSprite.x = 0; activeWallSprite.y = (2732 - wallHeight) / 2; var SHADE_START_FACTOR_WALLS = 0.3; var MIN_SHADE_WALLS = 0.2; var distanceRatio = distance / MAX_RENDER_DISTANCE; var shadeFactor = Math.max(MIN_SHADE_WALLS, 1 - distanceRatio / SHADE_START_FACTOR_WALLS); var totalProjectileLightInfluence = 0; var wallWorldCenterX = wallMapX + 0.5; var wallWorldCenterY = wallMapY + 0.5; for (var k = 0; k < projectiles.length; k++) { var proj = projectiles[k]; if (proj.active) { var dxProj = proj.worldX - wallWorldCenterX; var dyProj = proj.worldY - wallWorldCenterY; var distToProjectileSq = dxProj * dxProj + dyProj * dyProj; if (distToProjectileSq < PROJECTILE_LIGHT_RADIUS * PROJECTILE_LIGHT_RADIUS) { var distToProjectile = Math.sqrt(distToProjectileSq); var distRatioProj = distToProjectile / PROJECTILE_LIGHT_RADIUS; var falloff = Math.pow(Math.max(0, 1 - distRatioProj), PROJECTILE_LIGHT_FALLOFF_EXPONENT); totalProjectileLightInfluence += PROJECTILE_LIGHT_MAX_CONTRIBUTION * falloff; } } } shadeFactor = Math.min(1.0, shadeFactor + totalProjectileLightInfluence); if (side === 1) { shadeFactor *= 0.7; shadeFactor = Math.max(MIN_SHADE_WALLS * 0.5, shadeFactor); } var baseWallTint = 0xFFFFFF; var r_wall = baseWallTint >> 16 & 0xFF; var g_wall = baseWallTint >> 8 & 0xFF; var b_wall = baseWallTint & 0xFF; r_wall = fastFloor(r_wall * shadeFactor); g_wall = fastFloor(g_wall * shadeFactor); b_wall = fastFloor(b_wall * shadeFactor); activeWallSprite.tint = r_wall << 16 | g_wall << 8 | b_wall; activeWallSprite.alpha = 1.0; activeWallSprite.visible = true; }; self.clearStrip = function () { if (activeWallSprite && typeof currentTileIndex === 'number') { wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex); self.removeChild(activeWallSprite); activeWallSprite = null; currentTileIndex = null; } }; return self; }); var Treasure = Container.expand(function () { var self = Container.call(this); var treasureSprite = self.attachAsset('treasure', { anchorX: 0.5, anchorY: 0.5 }); treasureSprite.tint = 0xdddddd; var treasureOpenSprite = self.attachAsset('treasureopen', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.treasureSprite = treasureSprite; self.treasureOpenSprite = treasureOpenSprite; self.isOpen = false; self.mapX = 0; self.mapY = 0; self.value = 1; self.openTreasure = function () { if (!self.isOpen) { self.isOpen = true; self.treasureSprite.alpha = 0; self.treasureOpenSprite.alpha = 1; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x111111 }); /**** * Game Code ****/ var MATH_PI_2 = Math.PI * 2; var MATH_PI_HALF = Math.PI / 2; var SCREEN_WIDTH_HALF = 1024; var SCREEN_HEIGHT_HALF = 1366; var tempVec2 = { x: 0, y: 0 }; var tempRect = { x: 0, y: 0, width: 0, height: 0 }; var rayHitResult = { hit: false, dist: 0 }; var floorCache = {}; var floorCacheSize = 0; function fastFloor(value) { var key = Math.round(value * 100); if (floorCache[key] !== undefined) { return floorCache[key]; } var result = Math.floor(value); if (floorCacheSize > 1000) { floorCache = {}; floorCacheSize = 0; } floorCache[key] = result; floorCacheSize++; return result; } function getDistanceToPoint(x1, y1, x2, y2) { tempVec2.x = x2 - x1; tempVec2.y = y2 - y1; return Math.sqrt(tempVec2.x * tempVec2.x + tempVec2.y * tempVec2.y); } var SpatialGrid = { cellSize: 2.0, grid: {}, clear: function clear() { this.grid = {}; }, insert: function insert(entity) { var cellX = fastFloor(entity.mapX / this.cellSize); var cellY = fastFloor(entity.mapY / this.cellSize); var key = cellX + ',' + cellY; if (!this.grid[key]) { this.grid[key] = []; } this.grid[key].push(entity); }, getNearby: function getNearby(x, y, radius) { var nearby = []; var cellRadius = Math.ceil(radius / this.cellSize); var centerX = fastFloor(x / this.cellSize); var centerY = fastFloor(y / this.cellSize); for (var dx = -cellRadius; dx <= cellRadius; dx++) { for (var dy = -cellRadius; dy <= cellRadius; dy++) { var key = centerX + dx + ',' + (centerY + dy); if (this.grid[key]) { nearby = nearby.concat(this.grid[key]); } } } return nearby; } }; var MemoryManager = { cleanupInterval: 5000, lastCleanup: 0, garbageCollectionTargets: [], registerForCleanup: function registerForCleanup(object) { this.garbageCollectionTargets.push({ object: object, timestamp: Date.now() }); }, performCleanup: function performCleanup() { var now = Date.now(); if (now - this.lastCleanup < this.cleanupInterval) { return; } this.garbageCollectionTargets = this.garbageCollectionTargets.filter(function (target) { if (now - target.timestamp > 30000) { if (target.object && typeof target.object.destroy === 'function') { target.object.destroy(); } return false; } return true; }); if (floorCache && floorCache.size > 1000) { floorCache.clear(); } this.lastCleanup = now; } }; var EntityRenderer = { lastFrameEntities: [], currentFrameEntities: [], updateEntities: function updateEntities(entities) { this.currentFrameEntities = entities.slice(); if (!this.arraysEqual(this.lastFrameEntities, this.currentFrameEntities)) { dynamicEntitiesContainer.removeChildren(); this.currentFrameEntities.sort(function (a, b) { if (a.distance !== b.distance) { return b.distance - a.distance; } if (a.type === 'entity' && b.type === 'wall') { return -1; } if (a.type === 'wall' && b.type === 'entity') { return 1; } if (a.type === 'wall' && b.type === 'wall') { return a.originalSortOrder - b.originalSortOrder; } return 0; }); for (var i = 0; i < this.currentFrameEntities.length; i++) { if (this.currentFrameEntities[i].object.visible) { dynamicEntitiesContainer.addChild(this.currentFrameEntities[i].object); } } this.lastFrameEntities = this.currentFrameEntities.slice(); } }, arraysEqual: function arraysEqual(a, b) { if (a.length !== b.length) { return false; } for (var i = 0; i < a.length; i++) { if (a[i].object !== b[i].object) { return false; } } return true; } }; var gameState = 'title'; var titleScreenContainer; var dynamicEntitiesContainer; var OGRE_CURRENT_BASE_HEALTH = 6; var OGRE_CURRENT_BASE_ATTACK = 3; var EYEBALL_CURRENT_BASE_HEALTH = 2; var EYEBALL_CURRENT_BASE_ATTACK = 2; var IMP_CURRENT_BASE_HEALTH = 1; var IMP_CURRENT_BASE_ATTACK = 1; var BOSS_CURRENT_BASE_HEALTH = 10; var wallTextureLookup = new Array(65); for (var i = 1; i <= 64; i++) { wallTextureLookup[i] = 'walltile' + i; } var wallSpritePoolManager = { pools: function () { var p = {}; var PREALLOCATE_PER_TILE = 48; if (graphicsQuality === 'Low') { var lowQualityAssets = ['lowqualitywall1', 'lowqualitywall2']; for (var i = 0; i < lowQualityAssets.length; i++) { var poolIndex = i + 1; var assetName = lowQualityAssets[i]; var tilePool = []; p[poolIndex] = tilePool; for (var j = 0; j < PREALLOCATE_PER_TILE; j++) { var sprite = LK.getAsset(assetName, { anchorX: 0, anchorY: 0, visible: false }); tilePool.push(sprite); } } } else { for (var i = 1; i <= 64; i++) { var assetNameForLK = wallTextureLookup[i]; if (!assetNameForLK) { continue; } var tilePool = []; p[i] = tilePool; for (var j = 0; j < PREALLOCATE_PER_TILE; j++) { var sprite = LK.getAsset(assetNameForLK, { anchorX: 0, anchorY: 0, visible: false }); tilePool.push(sprite); } } } return p; }(), getSprite: function getSprite(tileIndex) { var actualIndex = tileIndex; var assetNameForLK; if (graphicsQuality === 'Low') { actualIndex = (tileIndex - 1) % 2 + 1; assetNameForLK = actualIndex === 1 ? 'lowqualitywall1' : 'lowqualitywall2'; } else { assetNameForLK = wallTextureLookup[tileIndex]; } if (!this.pools[actualIndex]) { var isValidTile = Number.isInteger(actualIndex) && actualIndex >= 1; if (isValidTile) { this.pools[actualIndex] = []; } else { return null; } } var pool = this.pools[actualIndex]; if (pool.length > 0) { var sprite = pool.pop(); return sprite; } else { if (!assetNameForLK) { return null; } var newSprite = LK.getAsset(assetNameForLK, { anchorX: 0, anchorY: 0, visible: false }); return newSprite; } }, releaseSprite: function releaseSprite(sprite, tileIndex) { var actualIndex = tileIndex; if (graphicsQuality === 'Low') { actualIndex = (tileIndex - 1) % 2 + 1; } if (!sprite || typeof actualIndex !== 'number') { return; } if (!this.pools[actualIndex]) { var isValidTile = Number.isInteger(actualIndex) && actualIndex >= 1; if (isValidTile) { this.pools[actualIndex] = []; } else { return; } } sprite.visible = false; sprite.alpha = 1.0; sprite.scale.set(1.0, 1.0); sprite.rotation = 0; sprite.x = 0; sprite.y = 0; sprite.tint = 0xFFFFFF; this.pools[actualIndex].push(sprite); } }; var PROJECTILE_LIGHT_RADIUS = 5.0; var PROJECTILE_LIGHT_MAX_CONTRIBUTION = 0.6; var PROJECTILE_LIGHT_FALLOFF_EXPONENT = 2.0; var MAP_SIZE = 16; var PROJECTILE_WALL_HIT_PARTICLE_COUNT = 8; var particlePoolManager; var particleExplosionContainer; var CELL_SIZE = 20; var MINI_MAP_SCALE = 1.35; // Increased from 1 to 1.25 (25% increase) function getStripWidth() { return graphicsQuality === 'Low' ? 20 : 5; } function getNumRays() { return Math.ceil(2048 / getStripWidth()); } var rayResults = []; function initializeRayResults() { var numRays = getNumRays(); rayResults = new Array(numRays); for (var i = 0; i < numRays; i++) { rayResults[i] = { wallHit: false, distance: 0, actualDistance: 0, side: 0, wallType: 0, mapX: 0, mapY: 0, screenX: 0, wallHeight: 0, textureIndex: 0 }; } } var FOV = Math.PI / 3; var lastPlayerX = -999; var lastPlayerY = -999; var lastPlayerDir = -999; var shouldRaycast = true; var HALF_FOV = FOV / 2; var PLAYER_MOVE_SPEED = 0.002; var PLAYER_TURN_SPEED = 0.002; var WALL_HEIGHT_FACTOR = 700; var MAX_RENDER_DISTANCE = 16; var MONSTER_AGGRO_RANGE = 7.0; var MONSTER_CLOSE_PROXIMITY_RANGE = 4.0; var MIN_MONSTER_PLAYER_DISTANCE = 0.6; var MAX_MONSTER_PLAYER_ENGAGEMENT_DISTANCE = 0.6; var WALL_BUFFER = 0.22; var MONSTER_WALL_BUFFER = 0.22; var MONSTER_COUNT = 8; var TREASURE_COUNT = 10; var BOSS_FIREBALL_COOLDOWN = 3000; var FIREBALL_SPEED = 0.042; var FIREBALL_DAMAGE = 2; var FIREBALL_MAX_DISTANCE = 12; var BOSS_PREFERRED_DISTANCE = 6.0; var BOSS_TOO_CLOSE_DISTANCE = 3.5; var BOSS_SHOOT_RANGE_MAX = 10.0; var BOSS_SHOOT_RANGE_MIN = 2.0; var MAX_AUDIBLE_DISTANCE_IMP_CRY = 20.0; var FOUNTAIN_SOUND_AUDIBLE_DISTANCE = 6.0; var FOUNTAIN_SOUND_INTERVAL = 2500; var OGRE_SOUND_AUDIBLE_DISTANCE = 10.0; var OGRE_SOUND_INTERVAL = 3000; var OGRE_SOUND_INTERVAL_RANDOMNESS = 1500; var map = []; var floorCaster; var player = { x: 1.5, y: 1.5, dir: 0, health: 10, maxHealth: 10, score: 0, level: 1, moveSpeedMultiplier: 1.0, defense: 0, attackPower: 1 }; var powerUps = []; var POWERUP_COUNT = 3; var controls = { forward: false, backward: false, left: false, right: false, attack: false }; var monsters = []; var treasures = []; var fountains = []; var fountainPlacedThisLevel = false; var projectiles = []; var gate = null; var wallSegments = []; var lastWallCheck = []; var lastTime = Date.now(); var canAttack = true; var attackCooldown = 500; var joystickOverrideActive = false; var joystickOverrideX = 0; var joystickOverrideY = 0; function handlePlayerDeathSequence() { if (isGameOverAnimating) { return; } // Prevent re-triggering isGameOverAnimating = true; LK.stopMusic(); // Stop music here, ensures it's always stopped on player death // Stop existing hand animation & animate sinking if (globalRightHand) { tween.stop(globalRightHand); // Stop all tweens on the hand var sinkTargetY = 2732 + globalRightHand.height * globalRightHand.scale.y; // Ensure fully off-screen tween(globalRightHand, { y: sinkTargetY }, { duration: 2000, easing: tween.easeIn // Use a standard easing function }); } // Play demon laugh sound LK.getSound('demonlaugh').play(); // Fade screen to red var gameOverScreenCoverContainer = new Container(); game.addChild(gameOverScreenCoverContainer); // Add to main game stage to be on top var redOverlay = LK.getAsset('menubackground', { anchorX: 0, anchorY: 0, width: 2048, // Full game width height: 2732, // Full game height alpha: 0, tint: 0xFF0000 // Red color }); gameOverScreenCoverContainer.addChild(redOverlay); tween(redOverlay, { alpha: 1.0 }, { duration: 2000, easing: tween.linear }); // Call LK.showGameOver after 2 seconds LK.setTimeout(function () { LK.showGameOver(); // isGameOverAnimating will be reset when the game re-initializes }, 2000); } var gateInteractionInProgress = false; var isGameOverAnimating = false; // Flag to prevent updates during game over animation var miniMap; var revealedCells = []; // For mini-map fog of war var MINI_MAP_REVEAL_RADIUS = 1; // How many cells around player to reveal (player cell + 1 in each direction) var rayCastView; var healthText; var attackText; var defenseText; var speedText; var levelText; var monsterText; var controlButtons = {}; var playerMarker; var globalRightHand; var activeControlForGlobalHandlers = null; var renderableObjects = []; var graphicsQuality = 'High'; var interactionPromptContainer; var interactionPromptMainText; var interactionPromptParentheticalGroup; var interactionPromptPressOpenText; var interactionPromptButtonImage; var interactionPromptPressCloseText; var gameLayer = new Container(); var projectileLayer = new Container(); var handLayer = new Container(); particlePoolManager = { pool: [], activeParticles: [], maxParticles: 750, poolGrowthSize: 50, initialize: function initialize() { this.pool = []; for (var i = 0; i < this.poolGrowthSize; i++) { this.pool.push(new Particle()); } }, getParticle: function getParticle() { var particle; if (this.pool.length > 0) { particle = this.pool.pop(); } else if (this.activeParticles.length < this.maxParticles) { particle = new Particle(); } else { particle = this.activeParticles.shift(); if (particle.parent) { particle.parent.removeChild(particle); } } if (particle) { this.activeParticles.push(particle); } return particle; }, releaseParticle: function releaseParticle(particle) { var index = this.activeParticles.indexOf(particle); if (index !== -1) { this.activeParticles.splice(index, 1); } if (particle.parent) { particle.parent.removeChild(particle); } particle.visible = false; particle.worldX = 0; particle.worldY = 0; particle.worldZ = 0; particle.vx = 0; particle.vy = 0; particle.vz = 0; particle.life = 0; if (this.pool.length < this.maxParticles * 0.5) { this.pool.push(particle); } }, updateActiveParticles: function updateActiveParticles() { for (var i = this.activeParticles.length - 1; i >= 0; i--) { var particle = this.activeParticles[i]; if (!particle.update()) { this.releaseParticle(particle); } } } }; function createParticleExplosion(worldExplosionX, worldExplosionY, count, tintColor, isWallHit) { if (!particleExplosionContainer) { particleExplosionContainer = new Container(); projectileLayer.addChild(particleExplosionContainer); } var finalExplosionX = worldExplosionX; var finalExplosionY = worldExplosionY; var wallNormalX = 0; var wallNormalY = 0; var applyWallBias = isWallHit === true; if (applyWallBias) { var cellX = fastFloor(worldExplosionX); var cellY = fastFloor(worldExplosionY); if (cellX + 1 < MAP_SIZE && map[cellY][cellX + 1].type === 1) { wallNormalX = -1; } else if (cellX - 1 >= 0 && map[cellY][cellX - 1].type === 1) { wallNormalX = 1; } if (cellY + 1 < MAP_SIZE && map[cellY + 1][cellX].type === 1) { wallNormalY = -1; } else if (cellY - 1 >= 0 && map[cellY - 1][cellX].type === 1) { wallNormalY = 1; } if (wallNormalX === 0 && wallNormalY === 0) { wallNormalX = worldExplosionX - (cellX + 0.5); wallNormalY = worldExplosionY - (cellY + 0.5); var normalLength = Math.sqrt(wallNormalX * wallNormalX + wallNormalY * wallNormalY); if (normalLength > 0) { wallNormalX /= normalLength; wallNormalY /= normalLength; } else { wallNormalX = -Math.cos(player.dir); wallNormalY = -Math.sin(player.dir); } } var offsetDistance = 0.1; finalExplosionX = worldExplosionX + wallNormalX * offsetDistance; finalExplosionY = worldExplosionY + wallNormalY * offsetDistance; } var worldExplosionZ = 0.5; for (var i = 0; i < count; i++) { var particle = particlePoolManager.getParticle(); var phi = Math.random() * MATH_PI_2; var theta = Math.acos(Math.random() * 2 - 1); var speed = (Math.random() * 0.015 + 0.0075) * (player.level > 3 ? 1.5 : 1); var vx = Math.sin(theta) * Math.cos(phi) * speed; var vy = Math.sin(theta) * Math.sin(phi) * speed; var vz = Math.cos(theta) * speed + 0.03; if (applyWallBias) { vx += wallNormalX * speed * 0.5; vy += wallNormalY * speed * 0.5; } var life = 360 + Math.random() * 240; particle.init(finalExplosionX, finalExplosionY, worldExplosionZ, vx, vy, vz, life, tintColor); particle.collisionImmunity = applyWallBias ? 10 : 0; particleExplosionContainer.addChild(particle); } } function reinitializeWallSpritePoolManager() { for (var poolKey in wallSpritePoolManager.pools) { var pool = wallSpritePoolManager.pools[poolKey]; for (var i = 0; i < pool.length; i++) { if (pool[i] && typeof pool[i].destroy === 'function') { pool[i].destroy(); } } } wallSpritePoolManager.pools = function () { var p = {}; var PREALLOCATE_PER_TILE = graphicsQuality === 'Low' ? 150 : 48; if (graphicsQuality === 'Low') { var lowQualityAssets = ['lowqualitywall1', 'lowqualitywall2']; for (var i = 0; i < lowQualityAssets.length; i++) { var poolIndex = i + 1; var assetName = lowQualityAssets[i]; var tilePool = []; p[poolIndex] = tilePool; for (var j = 0; j < PREALLOCATE_PER_TILE; j++) { var sprite = LK.getAsset(assetName, { anchorX: 0, anchorY: 0, visible: false }); tilePool.push(sprite); } } } else { for (var i = 1; i <= 64; i++) { var assetNameForLK = wallTextureLookup[i]; if (!assetNameForLK) { continue; } var tilePool = []; p[i] = tilePool; for (var j = 0; j < PREALLOCATE_PER_TILE; j++) { var sprite = LK.getAsset(assetNameForLK, { anchorX: 0, anchorY: 0, visible: false }); tilePool.push(sprite); } } } return p; }(); } function showTitleScreen() { if (titleScreenContainer) { titleScreenContainer.destroy(); titleScreenContainer = null; } titleScreenContainer = new Container(); game.addChild(titleScreenContainer); var bgAsset = titleScreenContainer.attachAsset('titlebackground', { anchorX: 0.5, anchorY: 0.5 }); bgAsset.x = SCREEN_WIDTH_HALF; bgAsset.y = SCREEN_HEIGHT_HALF; var logoAsset = titleScreenContainer.attachAsset('logo', { anchorX: 0.5, anchorY: 0.5 }); logoAsset.x = SCREEN_WIDTH_HALF; logoAsset.y = SCREEN_HEIGHT_HALF - logoAsset.height / 2 + 300; var startButtonAsset = titleScreenContainer.attachAsset('startbutton', { anchorX: 0.5, anchorY: 0.5 }); startButtonAsset.x = SCREEN_WIDTH_HALF; startButtonAsset.y = logoAsset.y + logoAsset.height / 2 + startButtonAsset.height / 2 + 150; startButtonAsset.down = function () { // Prevent multiple clicks if animation/fade is in progress if (startButtonAsset.isStartingGame) { return; } startButtonAsset.isStartingGame = true; LK.getSound('demonlaugh').play(); // Button scale animation tween(startButtonAsset, { scaleX: 0.9, scaleY: 0.9 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(startButtonAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeIn }); } }); // Fade screen to black // Using 'menubackground' asset, ensuring it's black and full screen var fadeOverlay = LK.getAsset('menubackground', { anchorX: 0, anchorY: 0, width: 2048, // Full game width height: 2732, // Full game height alpha: 0, tint: 0x000000 // Ensure it's black }); // Add to titleScreenContainer so it's removed with it and covers other title elements titleScreenContainer.addChild(fadeOverlay); tween(fadeOverlay, { alpha: 1.0 }, { duration: 2000, // Fade duration easing: tween.linear, onFinish: function onFinish() { gameState = 'game'; if (titleScreenContainer) { game.removeChild(titleScreenContainer); // This will also remove fadeOverlay titleScreenContainer.destroy(); titleScreenContainer = null; } initializeGamePlayElements(); // isStartingGame flag will be effectively reset when title screen is recreated on game restart } }); }; var qualityText = new Text2("Graphics quality: ".concat(graphicsQuality), { size: 120, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 4, align: 'center' }); qualityText.anchor.set(0.5, 0.5); qualityText.x = SCREEN_WIDTH_HALF; qualityText.y = startButtonAsset.y + startButtonAsset.height / 2 + 200; qualityText.down = function () { graphicsQuality = graphicsQuality === 'High' ? 'Low' : 'High'; qualityText.setText("Graphics quality: ".concat(graphicsQuality)); reinitializeWallSpritePoolManager(); if (gameState === 'game') { reinitializeRaycastSystem(); } qualityText.tint = 0xAAFFAA; LK.setTimeout(function () { qualityText.tint = 0xFFFFFF; }, 150); }; titleScreenContainer.addChild(qualityText); } function initializeGamePlayElements() { game.addChild(gameLayer); game.addChild(projectileLayer); game.addChild(handLayer); floorCaster = new FloorCaster(); gameLayer.addChild(floorCaster); wallSegments = []; initializeRayResults(); var numRays = getNumRays(); for (var i = 0; i < numRays; i++) { var strip = new RaycastStrip(); wallSegments.push(strip); } dynamicEntitiesContainer = new Container(); gameLayer.addChild(dynamicEntitiesContainer); miniMap = new Container(); miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2 * 0.40; miniMap.y = 40; gameLayer.addChild(miniMap); generateMap(); playerMarker = gameLayer.addChild(LK.getAsset('player', { anchorX: 0.5, anchorY: 0.5 })); createUI(); createControlButtons(); LK.playMusic('dungeon'); shouldRaycast = true; lastPlayerX = -999; lastPlayerY = -999; lastPlayerDir = -999; particlePoolManager.initialize(); } function setupGame() { if (gameState === 'title') { showTitleScreen(); } else { initializeGamePlayElements(); } } function generateMap() { // Initialize or re-initialize revealedCells for the new map revealedCells = []; for (var y_reveal = 0; y_reveal < MAP_SIZE; y_reveal++) { revealedCells[y_reveal] = []; for (var x_reveal = 0; x_reveal < MAP_SIZE; x_reveal++) { revealedCells[y_reveal][x_reveal] = false; } } miniMap.removeChildren(); map = []; for (var i = 0; i < monsters.length; i++) { // {km} if (monsters[i] && typeof monsters[i].destroy === 'function') { monsters[i].destroy(); } } monsters = []; for (var i = 0; i < treasures.length; i++) { if (treasures[i] && typeof treasures[i].destroy === 'function') { treasures[i].destroy(); } } treasures = []; for (var i = 0; i < projectiles.length; i++) { if (projectiles[i] && typeof projectiles[i].destroy === 'function') { projectiles[i].destroy(); } } projectiles = []; for (var i = 0; i < powerUps.length; i++) { if (powerUps[i] && typeof powerUps[i].destroy === 'function') { powerUps[i].destroy(); } } powerUps = []; if ((player.level - 1) % 5 === 0 && player.level > 1) { OGRE_CURRENT_BASE_HEALTH += 3; OGRE_CURRENT_BASE_ATTACK += 1; EYEBALL_CURRENT_BASE_HEALTH += 3; EYEBALL_CURRENT_BASE_ATTACK += 1; IMP_CURRENT_BASE_HEALTH += 3; IMP_CURRENT_BASE_ATTACK += 1; BOSS_CURRENT_BASE_HEALTH += 10; FIREBALL_DAMAGE += 2; } for (var i = 0; i < fountains.length; i++) { if (fountains[i]) { if (fountains[i].parent) { fountains[i].parent.removeChild(fountains[i]); } if (typeof fountains[i].destroy === 'function') { fountains[i].destroy(); } } } fountains = []; fountainPlacedThisLevel = false; var mapType; if (player.level !== 0 && player.level % 5 === 0) { mapType = 1; } else { var availableMapTypes = [0, 0, 2, 4]; mapType = availableMapTypes[fastFloor(Math.random() * availableMapTypes.length)]; } for (var y = 0; y < MAP_SIZE; y++) { map[y] = []; for (var x = 0; x < MAP_SIZE; x++) { var cell = new MapCell(); cell.x = x * CELL_SIZE * MINI_MAP_SCALE; cell.y = y * CELL_SIZE * MINI_MAP_SCALE; cell.setType(1); map[y][x] = cell; miniMap.addChild(cell); } } if (mapType === 0) { var stack = []; var startX = 1; var startY = 1; map[startY][startX].setType(0); stack.push({ x: startX, y: startY }); while (stack.length > 0) { var current = stack[stack.length - 1]; var potentialMoves = [{ dx: 0, dy: -2, wallXOffset: 0, wallYOffset: -1 }, { dx: 2, dy: 0, wallXOffset: 1, wallYOffset: 0 }, { dx: 0, dy: 2, wallXOffset: 0, wallYOffset: 1 }, { dx: -2, dy: 0, wallXOffset: -1, wallYOffset: 0 }]; for (var i = potentialMoves.length - 1; i > 0; i--) { var j = fastFloor(Math.random() * (i + 1)); var temp = potentialMoves[i]; potentialMoves[i] = potentialMoves[j]; potentialMoves[j] = temp; } var moved = false; for (var i = 0; i < potentialMoves.length; i++) { var move = potentialMoves[i]; var nextX = current.x + move.dx; var nextY = current.y + move.dy; var wallBetweenX = current.x + move.wallXOffset; var wallBetweenY = current.y + move.wallYOffset; if (nextX >= 0 && nextX < MAP_SIZE && nextY >= 0 && nextY < MAP_SIZE && map[nextY][nextX].type === 1) { if (wallBetweenX >= 0 && wallBetweenX < MAP_SIZE && wallBetweenY >= 0 && wallBetweenY < MAP_SIZE) { map[wallBetweenY][wallBetweenX].setType(0); map[nextY][nextX].setType(0); stack.push({ x: nextX, y: nextY }); moved = true; break; } } } if (!moved) { stack.pop(); } } } else if (mapType === 1) { var margin = 1; for (var y = margin; y < MAP_SIZE - margin; y++) { for (var x = margin; x < MAP_SIZE - margin; x++) { map[y][x].setType(0); } } for (var py = margin + 2; py < MAP_SIZE - margin - 1; py += 3) { for (var px = margin + 2; px < MAP_SIZE - margin - 1; px += 3) { if (py >= 0 && py < MAP_SIZE && px >= 0 && px < MAP_SIZE) { map[py][px].setType(1); } } } } else if (mapType === 4) { var spiralSteps = []; var x = 1, y = 1; var dx = 1, dy = 0; var segmentLength = MAP_SIZE - 3; var segmentPassed = 0; var turnsCompleted = 0; if (y >= 0 && y < MAP_SIZE && x >= 0 && x < MAP_SIZE) { map[y][x].setType(0); spiralSteps.push({ x: x, y: y }); } while (segmentLength > 0) { x += dx; y += dy; if (x >= 1 && x < MAP_SIZE - 1 && y >= 1 && y < MAP_SIZE - 1) { map[y][x].setType(0); spiralSteps.push({ x: x, y: y }); } else { break; } segmentPassed++; if (segmentPassed >= segmentLength) { segmentPassed = 0; var temp = dx; dx = -dy; dy = temp; turnsCompleted++; if (turnsCompleted % 2 === 0) { segmentLength -= 2; } } } var numShortcuts = 2 + fastFloor(Math.random() * 3); for (var s = 0; s < numShortcuts; s++) { if (spiralSteps.length <= 10) { continue; } var pointIndex = fastFloor(Math.random() * (spiralSteps.length - 10)) + 5; var point = spiralSteps[pointIndex]; var branchDir = fastFloor(Math.random() * 4); var branchDirs = [{ dx: 0, dy: -2 }, { dx: 2, dy: 0 }, { dx: 0, dy: 2 }, { dx: -2, dy: 0 }]; var dir = branchDirs[branchDir]; var branchX = point.x + dir.dx; var branchY = point.y + dir.dy; var connectX = point.x + dir.dx / 2; var connectY = point.y + dir.dy / 2; if (branchX >= 1 && branchX < MAP_SIZE - 1 && branchY >= 1 && branchY < MAP_SIZE - 1 && connectX >= 1 && connectX < MAP_SIZE - 1 && connectY >= 1 && connectY < MAP_SIZE - 1) { var hasConnection = false; var checkDirs = [{ dx: -1, dy: 0 }, { dx: 1, dy: 0 }, { dx: 0, dy: -1 }, { dx: 0, dy: 1 }]; for (var d = 0; d < checkDirs.length; d++) { var checkX = branchX + checkDirs[d].dx; var checkY = branchY + checkDirs[d].dy; if (checkX >= 1 && checkX < MAP_SIZE - 1 && checkY >= 1 && checkY < MAP_SIZE - 1 && map[checkY][checkX].type === 0 && (Math.abs(checkX - point.x) > 1 || Math.abs(checkY - point.y) > 1)) { hasConnection = true; break; } } if (hasConnection || Math.random() < 0.3) { map[connectY][connectX].setType(0); map[branchY][branchX].setType(0); if (Math.random() < 0.5) { var extX = branchX + dir.dx; var extY = branchY + dir.dy; var extConnectX = branchX + dir.dx / 2; var extConnectY = branchY + dir.dy / 2; if (extX >= 1 && extX < MAP_SIZE - 1 && extY >= 1 && extY < MAP_SIZE - 1 && extConnectX >= 1 && extConnectX < MAP_SIZE - 1 && extConnectY >= 1 && extConnectY < MAP_SIZE - 1 && map[extY][extX].type === 1) { map[extConnectY][extConnectX].setType(0); map[extY][extX].setType(0); } } } } } var numRooms = 1 + fastFloor(Math.random() * 3); for (var r = 0; r < numRooms; r++) { if (spiralSteps.length === 0) { continue; } var roomPointIndex = fastFloor(Math.random() * spiralSteps.length); var roomPoint = spiralSteps[roomPointIndex]; var roomSize = Math.random() < 0.5 ? 2 : 3; var roomStartX = roomPoint.x - fastFloor(roomSize / 2); var roomStartY = roomPoint.y - fastFloor(roomSize / 2); var canPlaceRoom = true; for (var ry_check = 0; ry_check < roomSize; ry_check++) { for (var rx_check = 0; rx_check < roomSize; rx_check++) { var checkX = roomStartX + rx_check; var checkY = roomStartY + ry_check; if (checkX < 1 || checkX >= MAP_SIZE - 1 || checkY < 1 || checkY >= MAP_SIZE - 1) { canPlaceRoom = false; break; } } if (!canPlaceRoom) { break; } } if (canPlaceRoom) { for (var ry_carve = 0; ry_carve < roomSize; ry_carve++) { for (var rx_carve = 0; rx_carve < roomSize; rx_carve++) { map[roomStartY + ry_carve][roomStartX + rx_carve].setType(0); } } } } } else { var stack = []; var startX = 1; var startY = 1; map[startY][startX].setType(0); stack.push({ x: startX, y: startY }); while (stack.length > 0) { var current = stack[stack.length - 1]; var potentialMoves = [{ dx: 0, dy: -2, wallXOffset: 0, wallYOffset: -1 }, { dx: 2, dy: 0, wallXOffset: 1, wallYOffset: 0 }, { dx: 0, dy: 2, wallXOffset: 0, wallYOffset: 1 }, { dx: -2, dy: 0, wallXOffset: -1, wallYOffset: 0 }]; for (var i = potentialMoves.length - 1; i > 0; i--) { var j = fastFloor(Math.random() * (i + 1)); var temp = potentialMoves[i]; potentialMoves[i] = potentialMoves[j]; potentialMoves[j] = temp; } var moved = false; for (var i = 0; i < potentialMoves.length; i++) { var move = potentialMoves[i]; var nextX = current.x + move.dx; var nextY = current.y + move.dy; var wallBetweenX = current.x + move.wallXOffset; var wallBetweenY = current.y + move.wallYOffset; if (nextX >= 0 && nextX < MAP_SIZE && nextY >= 0 && nextY < MAP_SIZE && map[nextY][nextX].type === 1) { if (wallBetweenX >= 0 && wallBetweenX < MAP_SIZE && wallBetweenY >= 0 && wallBetweenY < MAP_SIZE) { map[wallBetweenY][wallBetweenX].setType(0); map[nextY][nextX].setType(0); stack.push({ x: nextX, y: nextY }); moved = true; break; } } } if (!moved) { stack.pop(); } } var arenaMargin = 3; for (var y = arenaMargin; y < MAP_SIZE - arenaMargin; y++) { for (var x = arenaMargin; x < MAP_SIZE - arenaMargin; x++) { if (y >= 0 && y < MAP_SIZE && x >= 0 && x < MAP_SIZE) { map[y][x].setType(0); } } } for (var p = 0; p < 8; p++) { var px = fastFloor(Math.random() * (MAP_SIZE - 2 * arenaMargin - 2)) + arenaMargin + 1; var py = fastFloor(Math.random() * (MAP_SIZE - 2 * arenaMargin - 2)) + arenaMargin + 1; if (py >= 0 && py < MAP_SIZE && px >= 0 && px < MAP_SIZE) { map[py][px].setType(1); } } } for (var y_border = 0; y_border < MAP_SIZE; y_border++) { map[y_border][0].setType(1); map[y_border][MAP_SIZE - 1].setType(1); } for (var x_border = 0; x_border < MAP_SIZE; x_border++) { map[0][x_border].setType(1); map[MAP_SIZE - 1][x_border].setType(1); } ensureMapConnectivity(); var intendedPlayerX, intendedPlayerY; if (mapType === 0) { intendedPlayerX = 1.5; intendedPlayerY = 1.5; } else if (mapType === 1) { var arenaMarginForSpawn = 2; intendedPlayerX = arenaMarginForSpawn + 0.5; intendedPlayerY = arenaMarginForSpawn + 0.5; } else if (mapType === 2) { var mixedArenaMarginForSpawn = 3; intendedPlayerX = mixedArenaMarginForSpawn + 0.5; intendedPlayerY = mixedArenaMarginForSpawn + 0.5; } else if (mapType === 4) { intendedPlayerX = 1.5; intendedPlayerY = 1.5; } else if (mapType === 3) { intendedPlayerX = fastFloor(MAP_SIZE / 2) + 0.5; intendedPlayerY = fastFloor(MAP_SIZE / 2) + 0.5; } var spawnIsValid = false; var intendedCellX = fastFloor(intendedPlayerX); var intendedCellY = fastFloor(intendedPlayerY); if (intendedCellX >= 0 && intendedCellX < MAP_SIZE && intendedCellY >= 0 && intendedCellY < MAP_SIZE) { if (map[intendedCellY][intendedCellX].type === 0 && getDistanceToNearestWall(intendedPlayerX, intendedPlayerY) >= WALL_BUFFER) { spawnIsValid = true; player.x = intendedPlayerX; player.y = intendedPlayerY; } } if (!spawnIsValid) { var alternativeSpawns = []; for (var y_scan = 1; y_scan < MAP_SIZE - 1; y_scan++) { for (var x_scan = 1; x_scan < MAP_SIZE - 1; x_scan++) { var currentSpawnCandidateX = x_scan + 0.5; var currentSpawnCandidateY = y_scan + 0.5; if (map[y_scan][x_scan].type === 0 && getDistanceToNearestWall(currentSpawnCandidateX, currentSpawnCandidateY) >= WALL_BUFFER) { alternativeSpawns.push({ x: currentSpawnCandidateX, y: currentSpawnCandidateY }); } } } if (alternativeSpawns.length > 0) { var randomIndex = fastFloor(Math.random() * alternativeSpawns.length); player.x = alternativeSpawns[randomIndex].x; player.y = alternativeSpawns[randomIndex].y; } else { player.x = 1.5; player.y = 1.5; if (map[1] && map[1][1] && map[1][1].type === 1) { map[1][1].setType(0); } } } player.dir = 0; var initialPlayerDirForRotationCheck = player.dir; var playerRotationAttempts = 0; while (playerRotationAttempts < 4) { var lookAheadDist = 1.0; var cellInFrontMapX = fastFloor(player.x + Math.cos(player.dir) * lookAheadDist); var cellInFrontMapY = fastFloor(player.y + Math.sin(player.dir) * lookAheadDist); if (cellInFrontMapX >= 0 && cellInFrontMapX < MAP_SIZE && cellInFrontMapY >= 0 && cellInFrontMapY < MAP_SIZE && map[cellInFrontMapY] && map[cellInFrontMapY][cellInFrontMapX] && map[cellInFrontMapY][cellInFrontMapX].type === 0) { break; } player.dir += MATH_PI_HALF; if (player.dir >= MATH_PI_2) { player.dir -= MATH_PI_2; } playerRotationAttempts++; } if (playerRotationAttempts === 4) { player.dir = initialPlayerDirForRotationCheck; } if (!(player.level !== 0 && player.level % 5 === 0)) { // This condition means "if it's a non-boss level" var monstersToPlace; if (player.level === 1) { monstersToPlace = 7; // Changed from 5 to 7 } else if (player.level < 6) { // Covers levels 2, 3, 4 (since level 5 is boss) monstersToPlace = 5 + (player.level - 1); } else { // Covers level 6+ (and is a non-boss level) // Calculate base monster count up to level 4 var countUpToLevel4 = 5 + (4 - 1); // This is 8 monsters for level 4 // Calculate how many non-boss levels have passed since level 6 (inclusive) var nonBossLevelsSinceL6 = 0; for (var l = 6; l <= player.level; l++) { if (l % 5 !== 0) { // Check if 'l' is a non-boss level nonBossLevelsSinceL6++; } } monstersToPlace = countUpToLevel4 + nonBossLevelsSinceL6 * 2; } for (var i = 0; i < monstersToPlace; i++) { placeMonster(); } } else { monsters = []; var boss = new BossMonster(); var arenaMarginBoss = 1; var bossSpawnX, bossSpawnY; var foundBossSpawn = false; var attempts = 0; var preferredSpawns = [{ x: MAP_SIZE - 1 - arenaMarginBoss - 0.5, y: MAP_SIZE - 1 - arenaMarginBoss - 0.5 }, { x: MAP_SIZE / 2 + 0.5, y: MAP_SIZE / 2 + 0.5 }]; for (var s = 0; s < preferredSpawns.length; s++) { var spawn = preferredSpawns[s]; var cellX = fastFloor(spawn.x); var cellY = fastFloor(spawn.y); if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE && map[cellY][cellX].type === 0 && !map[cellY][cellX].monster && getDistanceToNearestWall(spawn.x, spawn.y) >= MONSTER_WALL_BUFFER) { bossSpawnX = spawn.x; bossSpawnY = spawn.y; foundBossSpawn = true; break; } } if (!foundBossSpawn) { do { bossSpawnX = fastFloor(Math.random() * (MAP_SIZE - 2 * arenaMarginBoss - 2)) + arenaMarginBoss + 1 + 0.5; bossSpawnY = fastFloor(Math.random() * (MAP_SIZE - 2 * arenaMarginBoss - 2)) + arenaMarginBoss + 1 + 0.5; attempts++; if (attempts > 100) { bossSpawnX = MAP_SIZE / 2 + 0.5; bossSpawnY = MAP_SIZE / 2 + 0.5; break; } var cellX = fastFloor(bossSpawnX); var cellY = fastFloor(bossSpawnY); } while (!(cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE && map[cellY][cellX].type === 0 && !map[cellY][cellX].monster && getDistanceToNearestWall(bossSpawnX, bossSpawnY) >= MONSTER_WALL_BUFFER)); } boss.mapX = bossSpawnX; boss.mapY = bossSpawnY; monsters.push(boss); dynamicEntitiesContainer.addChild(boss); if (map[fastFloor(boss.mapY)] && map[fastFloor(boss.mapY)][fastFloor(boss.mapX)]) { map[fastFloor(boss.mapY)][fastFloor(boss.mapX)].addMonster(); } } placeTreasure(); gate = placeGate(); placeFountain(); } function placePowerUp() { var x, y; var attempts = 0; var powerUpTypes = ['speed', 'defense', 'health', 'attack']; var randomType = powerUpTypes[fastFloor(Math.random() * powerUpTypes.length)]; do { x = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; y = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; attempts++; if (attempts > 100) { break; } } while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || map[y][x].gate || mapCellHasPowerUp(x, y)); if (attempts <= 100) { var powerUp = new PowerUp(randomType); powerUp.mapX = x + 0.5; powerUp.mapY = y + 0.5; powerUps.push(powerUp); dynamicEntitiesContainer.addChild(powerUp); } } function placeFountain() { if (fountainPlacedThisLevel) { return; } var potentialSpots = []; var deadEndSpots = []; for (var y = 1; y < MAP_SIZE - 1; y++) { for (var x = 1; x < MAP_SIZE - 1; x++) { if (map[y] && map[y][x] && map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) { var alreadyFountain = false; for (var fIdx = 0; fIdx < fountains.length; fIdx++) { if (fastFloor(fountains[fIdx].mapX) === x && fastFloor(fountains[fIdx].mapY) === y) { alreadyFountain = true; break; } } if (!alreadyFountain) { potentialSpots.push({ x: x, y: y }); } } } } if (potentialSpots.length === 0) { return; } for (var i = 0; i < potentialSpots.length; i++) { var spot = potentialSpots[i]; var openNeighbors = 0; var neighbors = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; for (var j = 0; j < neighbors.length; j++) { var nx = spot.x + neighbors[j].dx; var ny = spot.y + neighbors[j].dy; if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny] && map[ny][nx] && map[ny][nx].type === 0) { openNeighbors++; } } if (openNeighbors === 1) { deadEndSpots.push(spot); } } if (deadEndSpots.length === 0) { return; } for (var i = deadEndSpots.length - 1; i > 0; i--) { var j = fastFloor(Math.random() * (i + 1)); var temp = deadEndSpots[i]; deadEndSpots[i] = deadEndSpots[j]; deadEndSpots[j] = temp; } var placed = false; for (var k = 0; k < deadEndSpots.length; k++) { var spotToPlace = deadEndSpots[k]; var cellX = spotToPlace.x; var cellY = spotToPlace.y; var isPlayerSpot = cellX === fastFloor(player.x) && cellY === fastFloor(player.y); var isGateSpot = gate && cellX === fastFloor(gate.mapX) && cellY === fastFloor(gate.mapY); var cellIsOccupiedByOtherMapEntity = map[cellY][cellX].monster || map[cellY][cellX].treasure || map[cellY][cellX].gate; if (!isPlayerSpot && !isGateSpot && !cellIsOccupiedByOtherMapEntity && map[cellY][cellX].type === 0) { var fountain = new Fountain(); fountain.init(cellX + 0.5, cellY + 0.5); fountains.push(fountain); dynamicEntitiesContainer.addChild(fountain); fountainPlacedThisLevel = true; placed = true; break; } } } function mapCellHasPowerUp(cellX, cellY) { for (var i = 0; i < powerUps.length; i++) { if (fastFloor(powerUps[i].mapX) === cellX && fastFloor(powerUps[i].mapY) === cellY) { return true; } } return false; } function placeMonster() { var x, y; var attempts = 0; do { x = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; y = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; attempts++; var distToPlayer = getDistanceToPoint(x, player.x, y, player.y); if (attempts > 100) { break; } } 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; var randMonsterType = Math.random(); if (randMonsterType < 0.1) { monster = new Monster(); } else if (randMonsterType < 0.4) { monster = new EyeballMonster(); } else { monster = new ImpMonster(); } monster.mapX = x + 0.5; monster.mapY = y + 0.5; if (monster.attackCooldown === undefined) { monster.attackCooldown = 2000; } if (monster.lastAttackTime === undefined) { monster.lastAttackTime = 0; } if (monster.canAttack === undefined) { monster.canAttack = true; } monsters.push(monster); dynamicEntitiesContainer.addChild(monster); } } function placeTreasure() { var potentialSpots = []; var deadEndSpots = []; for (var y = 1; y < MAP_SIZE - 1; y++) { for (var x = 1; x < MAP_SIZE - 1; x++) { if (map[y] && map[y][x] && map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) { potentialSpots.push({ x: x, y: y }); } } } if (potentialSpots.length === 0) { return; } for (var i = 0; i < potentialSpots.length; i++) { var spot = potentialSpots[i]; var openNeighbors = 0; var neighbors = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; for (var j = 0; j < neighbors.length; j++) { var nx = spot.x + neighbors[j].dx; var ny = spot.y + neighbors[j].dy; if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny] && map[ny][nx] && map[ny][nx].type === 0) { openNeighbors++; } } if (openNeighbors === 1) { deadEndSpots.push(spot); } } var treasuresPlaced = 0; var maxTreasures = 1 + fastFloor(Math.random() * 3); if (deadEndSpots.length > 0) { if (deadEndSpots.length > 1) { for (var i = deadEndSpots.length - 1; i > 0; i--) { var j = fastFloor(Math.random() * (i + 1)); var temp = deadEndSpots[i]; deadEndSpots[i] = deadEndSpots[j]; deadEndSpots[j] = temp; } } for (var i = 0; i < deadEndSpots.length && treasuresPlaced < maxTreasures; i++) { var spot = deadEndSpots[i]; var cellX = spot.x; var cellY = spot.y; if (cellX === fastFloor(player.x) && cellY === fastFloor(player.y)) { continue; } if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].addTreasure()) { var treasure = new Treasure(); treasure.mapX = cellX + 0.5; treasure.mapY = cellY + 0.5; treasure.value = 1 + fastFloor(Math.random() * player.level); treasures.push(treasure); dynamicEntitiesContainer.addChild(treasure); treasuresPlaced++; } } } if (treasuresPlaced === 0 && potentialSpots.length > 0) { for (var i = potentialSpots.length - 1; i > 0; i--) { var j = fastFloor(Math.random() * (i + 1)); var temp = potentialSpots[i]; potentialSpots[i] = potentialSpots[j]; potentialSpots[j] = temp; } for (var i = 0; i < potentialSpots.length; i++) { var spot = potentialSpots[i]; var cellX = spot.x; var cellY = spot.y; if (cellX === fastFloor(player.x) && cellY === fastFloor(player.y)) { continue; } if (gate && cellX === fastFloor(gate.mapX) && cellY === fastFloor(gate.mapY)) { continue; } if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].type === 0 && map[cellY][cellX].addTreasure()) { var treasure = new Treasure(); treasure.mapX = cellX + 0.5; treasure.mapY = cellY + 0.5; treasure.value = 1 + fastFloor(Math.random() * player.level); treasures.push(treasure); dynamicEntitiesContainer.addChild(treasure); treasuresPlaced++; break; } } } } function placeGate() { var x, y; var attempts = 0; var validPositions = []; for (var i = 0; i < 100; i++) { x = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; y = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1; if (map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) { var distToPlayer = getDistanceToPoint(x, player.x, y, player.y); if (distToPlayer > MAP_SIZE / 2.5) { validPositions.push({ x: x, y: y }); } } } if (validPositions.length > 0) { var randomIndex = fastFloor(Math.random() * validPositions.length); var chosenPos = validPositions[randomIndex]; var gateX = chosenPos.x; var gateY = chosenPos.y; map[gateY][gateX].addGate(); if (map[gateY] && map[gateY][gateX] && typeof map[gateY][gateX].updateVisual === 'function') { map[gateY][gateX].updateVisual(); } var gate = new Gate(); gate.mapX = gateX + 0.5; gate.mapY = gateY + 0.5; dynamicEntitiesContainer.addChild(gate); return gate; } return null; } function createUI() { // Create stats panel in top right - moved further left to account for larger text var statsPanel = new Container(); LK.gui.topRight.addChild(statsPanel); statsPanel.x = -380; // Increased from -280 to accommodate larger text statsPanel.y = 20; // Health with current/max format - 50% larger healthText = new Text2("\u2764\uFE0F Health: ".concat(player.health, "/").concat(player.maxHealth), { size: 54, // Increased from 36 fill: 0xFF5555, stroke: 0x000000, strokeThickness: 3 // Increased stroke for larger text }); healthText.anchor.set(0, 0); statsPanel.addChild(healthText); healthText.y = 0; // Attack stat - 50% larger attackText = new Text2("\u2694\uFE0F Attack: ".concat(player.attackPower), { size: 54, // Increased from 36 fill: 0xFFAA55, stroke: 0x000000, strokeThickness: 3 }); attackText.anchor.set(0, 0); statsPanel.addChild(attackText); attackText.y = 75; // Increased from 50 to account for larger text // Defense stat - 50% larger defenseText = new Text2("\uD83D\uDEE1\uFE0F Defense: ".concat(player.defense), { // Used shield emoji for consistency size: 54, // Increased from 36 fill: 0x5555FF, stroke: 0x000000, strokeThickness: 3 }); defenseText.anchor.set(0, 0); statsPanel.addChild(defenseText); defenseText.y = 150; // Increased from 100 // Speed stat - 50% larger speedText = new Text2("\uD83D\uDC5F Speed: ".concat(player.moveSpeedMultiplier.toFixed(1), "x"), { // Used shoe emoji for consistency size: 54, // Increased from 36 fill: 0x55FF55, stroke: 0x000000, strokeThickness: 3 // Increased stroke for larger text }); speedText.anchor.set(0, 0); statsPanel.addChild(speedText); speedText.y = 225; // Increased from 150 // Level and Monsters under mini map levelText = new Text2("Level: ".concat(player.level), { size: 50, fill: 0x55FF55, stroke: 0x000000, strokeThickness: 2 }); levelText.anchor.set(0, 0); gameLayer.addChild(levelText); levelText.x = miniMap.x; levelText.y = miniMap.y + MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE + 10; // Calculate miniMap height monsterText = new Text2("Monsters: ".concat(monsters.length), { // Initialized with monsters.length size: 50, fill: 0xFF9955, stroke: 0x000000, strokeThickness: 2 }); monsterText.anchor.set(0, 0); gameLayer.addChild(monsterText); monsterText.x = miniMap.x + 180; monsterText.y = levelText.y; // Interaction prompt (copied from original createUI, as it's not removed in the goal) interactionPromptContainer = new Container(); interactionPromptMainText = new Text2('', { size: 70, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 6, align: 'center' }); interactionPromptMainText.anchor.set(0.5, 0.5); interactionPromptContainer.addChild(interactionPromptMainText); interactionPromptParentheticalGroup = new Container(); interactionPromptPressOpenText = new Text2('(Press ', { size: 60, fill: 0xDDDDDD, stroke: 0x000000, strokeThickness: 5 }); interactionPromptPressOpenText.anchor.set(0, 0.5); interactionPromptButtonImage = LK.getAsset('attackButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.6, scaleY: 0.6 }); interactionPromptPressCloseText = new Text2(')', { size: 60, fill: 0xDDDDDD, stroke: 0x000000, strokeThickness: 5 }); interactionPromptPressCloseText.anchor.set(0, 0.5); interactionPromptParentheticalGroup.addChild(interactionPromptPressOpenText); interactionPromptParentheticalGroup.addChild(interactionPromptButtonImage); interactionPromptParentheticalGroup.addChild(interactionPromptPressCloseText); interactionPromptContainer.addChild(interactionPromptParentheticalGroup); LK.gui.center.addChild(interactionPromptContainer); interactionPromptContainer.visible = false; updateUI(); // Initial UI update } function updateUI() { if (healthText) { healthText.setText("\u2764\uFE0F Health: ".concat(player.health, "/").concat(player.maxHealth)); } if (attackText) { attackText.setText("\u2694\uFE0F Attack: ".concat(player.attackPower)); } if (defenseText) { defenseText.setText("\uD83D\uDEE1\uFE0F Defense: ".concat(player.defense)); } if (speedText) { speedText.setText("\uD83D\uDC5F Speed: ".concat(player.moveSpeedMultiplier.toFixed(1), "x")); } if (levelText) { levelText.setText("Level: ".concat(player.level)); } if (monsterText) { monsterText.setText("Monsters: ".concat(monsters.length)); } // LK.setScore is handled by the engine, no need to call it manually with player.score } function createControlButtons() { controlButtons.joystick = new JoystickController(); controlButtons.joystick.x = 400; controlButtons.joystick.y = 2732 - 500; gameLayer.addChild(controlButtons.joystick); controlButtons.attack = new ControlButton('attack'); controlButtons.attack.x = 2048 - 400; controlButtons.attack.y = 2732 - 500; gameLayer.addChild(controlButtons.attack); globalRightHand = LK.getAsset('rightHand', { anchorX: 0.5, anchorY: 0.9, scaleX: 1.2, scaleY: 1.2 }); globalRightHand.x = 2048 * 0.6; globalRightHand.y = 2732; handLayer.addChild(globalRightHand); var _animateHand = function animateHand() { var randomXOffset = Math.random() * 80 - 40; if (globalRightHand.originalX === undefined) { globalRightHand.originalX = globalRightHand.x; } tween(globalRightHand, { scaleX: 1.1, scaleY: 1.1, x: globalRightHand.originalX + randomXOffset, y: 2732 - 15 }, { duration: 1800, easing: tween.easeInOut, onFinish: function onFinish() { var returnRandomXOffset = Math.random() * 40 - 20; tween(globalRightHand, { scaleX: 1.1, scaleY: 1.1, x: globalRightHand.originalX + returnRandomXOffset, y: 2732 }, { duration: 1800, easing: tween.easeInOut, onFinish: _animateHand }); } }); }; _animateHand(); } function castRayDDAInto(startX, startY, rayDirX, rayDirY, result) { if (!castRayDDAInto._cache) { castRayDDAInto._cache = { deltaDistX: 0, deltaDistY: 0, mapX: 0, mapY: 0, stepX: 0, stepY: 0, sideDistX: 0, sideDistY: 0 }; } var cache = castRayDDAInto._cache; cache.mapX = fastFloor(startX); cache.mapY = fastFloor(startY); if (cache.mapX < 0 || cache.mapX >= MAP_SIZE || cache.mapY < 0 || cache.mapY >= MAP_SIZE) { result.wallHit = true; result.distance = MAX_RENDER_DISTANCE; result.side = 0; result.wallType = 1; result.mapX = cache.mapX; result.mapY = cache.mapY; return; } var absDirX = Math.abs(rayDirX); var absDirY = Math.abs(rayDirY); cache.deltaDistX = absDirX < 0.0001 ? 9999999 : 1 / absDirX; cache.deltaDistY = absDirY < 0.0001 ? 9999999 : 1 / absDirY; cache.stepX = rayDirX < 0 ? -1 : 1; cache.sideDistX = rayDirX < 0 ? (startX - cache.mapX) * cache.deltaDistX : (cache.mapX + 1.0 - startX) * cache.deltaDistX; cache.stepY = rayDirY < 0 ? -1 : 1; cache.sideDistY = rayDirY < 0 ? (startY - cache.mapY) * cache.deltaDistY : (cache.mapY + 1.0 - startY) * cache.deltaDistY; var hit = false; var side = 0; var maxIterations = MAP_SIZE * 2; var iterations = 0; while (!hit && iterations < maxIterations) { if (cache.sideDistX < cache.sideDistY) { cache.sideDistX += cache.deltaDistX; cache.mapX += cache.stepX; side = 0; } else { cache.sideDistY += cache.deltaDistY; cache.mapY += cache.stepY; side = 1; } if (cache.mapX < 0 || cache.mapX >= MAP_SIZE || cache.mapY < 0 || cache.mapY >= MAP_SIZE) { hit = true; continue; } if (map[cache.mapY] && map[cache.mapY][cache.mapX] && map[cache.mapY][cache.mapX].type === 1) { hit = true; } iterations++; } var perpWallDist; if (hit) { if (side === 0) { perpWallDist = (cache.mapX - startX + (1 - cache.stepX) / 2) / rayDirX; } else { perpWallDist = (cache.mapY - startY + (1 - cache.stepY) / 2) / rayDirY; } perpWallDist = Math.max(0.001, Math.min(MAX_RENDER_DISTANCE, perpWallDist)); } else { perpWallDist = MAX_RENDER_DISTANCE; } result.wallHit = hit; result.distance = perpWallDist; result.side = side; if (hit && cache.mapX >= 0 && cache.mapX < MAP_SIZE && cache.mapY >= 0 && cache.mapY < MAP_SIZE && map[cache.mapY] && map[cache.mapY][cache.mapX]) { result.wallType = map[cache.mapY][cache.mapX].type; } else { result.wallType = 1; } result.mapX = cache.mapX; result.mapY = cache.mapY; } function fullRayCasting() { floorCaster.update(player.x, player.y, player.dir); var numRays = getNumRays(); var stripWidth = getStripWidth(); var playerX = player.x; var playerY = player.y; var playerDir = player.dir; for (var rayIdx = 0; rayIdx < numRays; rayIdx++) { var rayResult = rayResults[rayIdx]; var rayAngle = playerDir - HALF_FOV + rayIdx / numRays * FOV; var rayDirX = Math.cos(rayAngle); var rayDirY = Math.sin(rayAngle); castRayDDAInto(playerX, playerY, rayDirX, rayDirY, rayResult); if (rayResult.wallHit) { rayResult.actualDistance = rayResult.distance * Math.cos(rayAngle - playerDir); rayResult.wallHeight = Math.max(stripWidth, Math.min(2732, WALL_HEIGHT_FACTOR / rayResult.actualDistance)); var wallX; if (rayResult.side === 0) { wallX = playerY + rayResult.distance * rayDirY; } else { wallX = playerX + rayResult.distance * rayDirX; } var texturePos = wallX * 64; var baseIndexOriginal64 = fastFloor(texturePos) % 64; var tileIndex = baseIndexOriginal64 + 1; var minTexIdNewRange = 1; var maxTexIdNewRange = 64; if (rayResult.side === 0 && rayDirX > 0) { tileIndex = minTexIdNewRange + (maxTexIdNewRange - tileIndex); } if (rayResult.side === 1 && rayDirY < 0) { tileIndex = minTexIdNewRange + (maxTexIdNewRange - tileIndex); } rayResult.textureIndex = tileIndex; rayResult.screenX = Math.round(rayIdx * stripWidth + (2048 - numRays * stripWidth) / 2); var strip = wallSegments[rayIdx]; strip.x = rayResult.screenX; strip.updateStrip(stripWidth, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY); strip.visible = true; renderableObjects.push({ distance: rayResult.actualDistance, object: strip, type: 'wall', originalSortOrder: rayIdx }); } else { var strip = wallSegments[rayIdx]; if (strip && strip.clearStrip) { strip.clearStrip(); strip.visible = false; } rayResult.wallHit = false; } } } function updateRaycastVisuals() { var numRays = getNumRays(); var stripWidth = getStripWidth(); for (var rayIdx = 0; rayIdx < numRays; rayIdx++) { var rayResult = rayResults[rayIdx]; if (rayResult.wallHit) { var strip = wallSegments[rayIdx]; strip.x = rayResult.screenX; strip.updateStrip(stripWidth, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY); strip.visible = true; renderableObjects.push({ distance: rayResult.actualDistance, object: strip, type: 'wall', originalSortOrder: rayIdx }); } else { var strip = wallSegments[rayIdx]; if (strip && strip.clearStrip) { strip.clearStrip(); strip.visible = false; } } } } function reinitializeRaycastSystem() { var newNumRays = getNumRays(); for (var i = 0; i < wallSegments.length; i++) { if (wallSegments[i] && wallSegments[i].clearStrip) { wallSegments[i].clearStrip(); } } wallSegments = []; for (var i = 0; i < newNumRays; i++) { var strip = new RaycastStrip(); wallSegments.push(strip); } initializeRayResults(); shouldRaycast = true; } function rayCasting() { var playerMoved = Math.abs(player.x - lastPlayerX) > 0.001 || Math.abs(player.y - lastPlayerY) > 0.001; // Only reduce rotation sensitivity for low quality var rotationThreshold = graphicsQuality === 'Low' ? 0.02 : 0.001; var significantRotation = Math.abs(player.dir - lastPlayerDir) > rotationThreshold; if (playerMoved || significantRotation || shouldRaycast) { fullRayCasting(); lastPlayerX = player.x; lastPlayerY = player.y; lastPlayerDir = player.dir; shouldRaycast = false; } else { updateRaycastVisuals(); } } function renderEntities() { SpatialGrid.clear(); var allDynamicEntities = monsters.concat(treasures).concat(powerUps.filter(function (p) { return !p.collected; })).concat(fountains); if (gate) { allDynamicEntities.push(gate); } // Pre-filter by distance before adding to spatial grid var nearPlayerEntities = []; for (var i = 0; i < allDynamicEntities.length; i++) { var entity = allDynamicEntities[i]; var dx = entity.mapX - player.x; var dy = entity.mapY - player.y; var distSq = dx * dx + dy * dy; // Skip entities that are definitely too far (avoid sqrt) if (distSq > MAX_RENDER_DISTANCE * MAX_RENDER_DISTANCE) { entity.visible = false; continue; } entity.renderDistSq = distSq; // Cache for later nearPlayerEntities.push(entity); SpatialGrid.insert(entity); } // Now only process nearby entities for (var i = 0; i < nearPlayerEntities.length; i++) { var entity = nearPlayerEntities[i]; entity.visible = false; var dist = Math.sqrt(entity.renderDistSq); entity.renderDist = dist; if (dist >= MAX_RENDER_DISTANCE) { continue; } var dx = entity.mapX - player.x; var dy = entity.mapY - player.y; 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) { var rayHit = castRayToPoint(player.x, player.y, entity.mapX, entity.mapY); if (!rayHit.hit || rayHit.dist > dist - 0.5) { entity.visible = true; var screenX = (0.5 + angle / FOV) * 2048; var height = WALL_HEIGHT_FACTOR / dist; var scale = height / 100; entity.x = screenX; var screenY_horizon = SCREEN_HEIGHT_HALF; var player_camera_height_projection_factor = WALL_HEIGHT_FACTOR * 0.5; var screenY_for_floor_at_dist = screenY_horizon + player_camera_height_projection_factor / dist; var z_to_screen_pixels_factor = WALL_HEIGHT_FACTOR; if (entity instanceof Treasure) { var currentSprite = entity.isOpen ? entity.treasureOpenSprite : entity.treasureSprite; var currentSpriteOriginalHeight = currentSprite.height; var actualRenderedHeight = currentSpriteOriginalHeight * scale; entity.y = screenY_for_floor_at_dist - actualRenderedHeight * 0.2; } else if (entity instanceof PowerUp) { var powerUpWorldZ = entity.verticalOffset; var screenY_offset_due_to_worldZ = powerUpWorldZ * z_to_screen_pixels_factor / dist; entity.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ + entity.currentBobOffset - height / 2; } else if (entity instanceof Fountain) { var fountainSprite = entity.sprite; var fountainSpriteOriginalHeight = fountainSprite.height; var actualRenderedHeightFountain = fountainSpriteOriginalHeight * scale; entity.y = screenY_for_floor_at_dist - actualRenderedHeightFountain * 0.2; } else if (entity instanceof EyeballMonster) { var eyeballWorldZ = entity.baseWorldYOffset; var screenY_offset_due_to_worldZ = eyeballWorldZ * z_to_screen_pixels_factor / dist; entity.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ; } else if (entity instanceof Gate) { var gateSpriteOriginalHeight = entity.gateSprite.height; var actualRenderedHeightGate = gateSpriteOriginalHeight * scale; entity.y = screenY_for_floor_at_dist - actualRenderedHeightGate * 0.2; } else if (entity instanceof Monster || entity instanceof ImpMonster || entity instanceof BossMonster) { var entitySizeMultiplier = entity.sizeMultiplier || 1.0; var screenY_horizon = SCREEN_HEIGHT_HALF; if (entitySizeMultiplier < 1.0) { var sizeReduction = (1 - entitySizeMultiplier) * height * 0.5; entity.y = screenY_horizon + sizeReduction; } else { entity.y = screenY_horizon; } } else { entity.y = screenY_horizon; } if (entity instanceof PowerUp) { var finalScale = scale * entity.spawnScaleMultiplier; entity.scale.set(finalScale, finalScale); } else if (entity.sizeMultiplier !== undefined) { var customScale = scale * entity.sizeMultiplier; entity.scale.set(customScale, customScale); } else { entity.scale.set(scale, scale); } var SHADE_START_FACTOR_ENTITIES = 0.3; var MIN_SHADE_ENTITIES = 0.2; var distanceRatioEntities = dist / MAX_RENDER_DISTANCE; var entityShadeFactor = Math.max(MIN_SHADE_ENTITIES, 1 - distanceRatioEntities / SHADE_START_FACTOR_ENTITIES); var totalProjectileLightInfluenceEntity = 0; for (var k = 0; k < projectiles.length; k++) { var proj = projectiles[k]; if (proj.active) { var dxProjEntity = proj.worldX - entity.mapX; var dyProjEntity = proj.worldY - entity.mapY; var distToProjectileEntitySq = dxProjEntity * dxProjEntity + dyProjEntity * dyProjEntity; if (distToProjectileEntitySq < PROJECTILE_LIGHT_RADIUS * PROJECTILE_LIGHT_RADIUS) { var distToProjectileEntity = Math.sqrt(distToProjectileEntitySq); var distRatioProjEntity = distToProjectileEntity / PROJECTILE_LIGHT_RADIUS; var falloffEntity = Math.pow(Math.max(0, 1 - distRatioProjEntity), PROJECTILE_LIGHT_FALLOFF_EXPONENT); totalProjectileLightInfluenceEntity += PROJECTILE_LIGHT_MAX_CONTRIBUTION * falloffEntity; } } } entityShadeFactor = Math.min(1.0, entityShadeFactor + totalProjectileLightInfluenceEntity); var finalEntityTint; if (entity instanceof Monster || entity instanceof EyeballMonster || entity instanceof ImpMonster || entity instanceof BossMonster) { var r_ge = fastFloor(0xFF * entityShadeFactor); var g_ge = fastFloor(0xFF * entityShadeFactor); var b_ge = fastFloor(0xFF * entityShadeFactor); finalEntityTint = r_ge << 16 | g_ge << 8 | b_ge; entity.tint = finalEntityTint; } else if (entity instanceof Treasure) { var baseTreasureTint = 0xdddddd; var r_t = baseTreasureTint >> 16 & 0xFF; var g_t = baseTreasureTint >> 8 & 0xFF; var b_t = baseTreasureTint & 0xFF; r_t = fastFloor(r_t * entityShadeFactor); g_t = fastFloor(g_t * entityShadeFactor); b_t = fastFloor(b_t * entityShadeFactor); finalEntityTint = r_t << 16 | g_t << 8 | b_t; var targetSprite = entity.isOpen ? entity.treasureOpenSprite : entity.treasureSprite; targetSprite.tint = finalEntityTint; } else if (entity instanceof PowerUp) { var r_p = fastFloor(0xFF * entityShadeFactor); var g_p = fastFloor(0xFF * entityShadeFactor); var b_p = fastFloor(0xFF * entityShadeFactor); finalEntityTint = r_p << 16 | g_p << 8 | b_p; entity.powerUpSprite.tint = finalEntityTint; } else if (entity instanceof Fountain) { var baseFountainTint = entity.state === 'full' ? 0xFFFFFF : 0x707070; var r_ft = baseFountainTint >> 16 & 0xFF; var g_ft = baseFountainTint >> 8 & 0xFF; var b_ft = baseFountainTint & 0xFF; r_ft = fastFloor(r_ft * entityShadeFactor); g_ft = fastFloor(g_ft * entityShadeFactor); b_ft = fastFloor(b_ft * entityShadeFactor); finalEntityTint = r_ft << 16 | g_ft << 8 | b_ft; entity.sprite.tint = finalEntityTint; } else if (entity instanceof Gate) { var baseGateTint = 0x00FF00; var r_ga = baseGateTint >> 16 & 0xFF; var g_ga = baseGateTint >> 8 & 0xFF; var b_ga = baseGateTint & 0xFF; r_ga = fastFloor(r_ga * entityShadeFactor); g_ga = fastFloor(g_ga * entityShadeFactor); b_ga = fastFloor(b_ga * entityShadeFactor); finalEntityTint = r_ga << 16 | g_ga << 8 | b_ga; entity.gateSprite.tint = finalEntityTint; } renderableObjects.push({ distance: entity.renderDist, object: entity, type: 'entity' }); } } } } 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 = fastFloor(startX); var mapCheckY = fastFloor(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; } 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; } var showInteract = false; var interactableObject = null; if (gate) { var dx_g = gate.mapX - player.x; var dy_g = gate.mapY - player.y; var dist_g = Math.sqrt(dx_g * dx_g + dy_g * dy_g); if (dist_g < 0.7) { var angleToGate = Math.atan2(dy_g, dx_g); var angleDiff_g = Math.abs((player.dir - angleToGate + Math.PI * 3) % MATH_PI_2 - Math.PI); if (angleDiff_g < Math.PI / 3) { showInteract = true; interactableObject = gate; } } } if (!showInteract) { for (var i = 0; i < treasures.length; i++) { var t = treasures[i]; if (t.isOpen) { continue; } var dx_t = t.mapX - player.x; var dy_t = t.mapY - player.y; var dist_t = Math.sqrt(dx_t * dx_t + dy_t * dy_t); if (dist_t < 0.7) { var angleToTreasure = Math.atan2(dy_t, dx_t); var angleDiff_t = Math.abs((player.dir - angleToTreasure + Math.PI * 3) % MATH_PI_2 - Math.PI); if (angleDiff_t < Math.PI / 3) { showInteract = true; interactableObject = t; break; } } } } if (!showInteract) { for (var i = 0; i < powerUps.length; i++) { var p = powerUps[i]; if (p.collected) { continue; } var dx_p = p.mapX - player.x; var dy_p = p.mapY - player.y; var dist_p = Math.sqrt(dx_p * dx_p + dy_p * dy_p); if (dist_p < 0.7) { var angleToPowerUp = Math.atan2(dy_p, dx_p); var angleDiff_p = Math.abs((player.dir - angleToPowerUp + Math.PI * 3) % MATH_PI_2 - Math.PI); if (angleDiff_p < Math.PI / 3) { showInteract = true; interactableObject = p; break; } } } } if (!showInteract) { for (var i = 0; i < fountains.length; i++) { var f = fountains[i]; if (!f.isInteractable || f.state !== 'full') { continue; } var dx_f = f.mapX - player.x; var dy_f = f.mapY - player.y; var dist_f = Math.sqrt(dx_f * dx_f + dy_f * dy_f); if (dist_f < 0.7) { var angleToFountain = Math.atan2(dy_f, dx_f); var angleDiff_f = Math.abs((player.dir - angleToFountain + Math.PI * 3) % MATH_PI_2 - Math.PI); if (angleDiff_f < Math.PI / 3) { showInteract = true; interactableObject = f; break; } } } } controlButtons.attack.isInteract = showInteract; controlButtons.attack.interactTarget = interactableObject; if (showInteract && interactableObject) { var mainMessageStr = ""; var showParentheticalPrompt = true; if (interactableObject instanceof Treasure && !interactableObject.isOpen) { mainMessageStr = "Open"; } else if (interactableObject instanceof PowerUp && !interactableObject.collected) { mainMessageStr = "Pick up"; } else if (interactableObject instanceof Fountain && interactableObject.isInteractable && interactableObject.state === 'full') { mainMessageStr = "Drink"; } else if (interactableObject instanceof Gate) { if (monsters.length > 0) { mainMessageStr = "Defeat all monsters\nbefore exiting!"; showParentheticalPrompt = false; } else { mainMessageStr = "Exit"; } } else { showInteract = false; } if (showInteract) { interactionPromptMainText.setText(mainMessageStr); interactionPromptParentheticalGroup.visible = showParentheticalPrompt; interactionPromptContainer.visible = true; if (showParentheticalPrompt) { var spacing = 10; var buttonScaledWidth = interactionPromptButtonImage.width * interactionPromptButtonImage.scale.x; interactionPromptPressOpenText.x = 0; interactionPromptButtonImage.x = interactionPromptPressOpenText.x + interactionPromptPressOpenText.width + buttonScaledWidth / 2 + spacing; interactionPromptPressCloseText.x = interactionPromptButtonImage.x + buttonScaledWidth / 2 + spacing; var parentheticalGroupWidth = interactionPromptPressCloseText.x + interactionPromptPressCloseText.width; var firstElementEffectiveX = interactionPromptPressOpenText.x - interactionPromptPressOpenText.anchor.x * interactionPromptPressOpenText.width; var shiftToCenterGroup = -(firstElementEffectiveX + parentheticalGroupWidth / 2); interactionPromptPressOpenText.x += shiftToCenterGroup; interactionPromptButtonImage.x += shiftToCenterGroup; interactionPromptPressCloseText.x += shiftToCenterGroup; var mainTextWidth = interactionPromptMainText.width; var combinedSpacing = 20; var totalCombinedWidth = mainTextWidth + combinedSpacing + parentheticalGroupWidth; interactionPromptMainText.x = -totalCombinedWidth / 2 + mainTextWidth / 2; interactionPromptParentheticalGroup.x = interactionPromptMainText.x + mainTextWidth / 2 + combinedSpacing + parentheticalGroupWidth / 2; } else { interactionPromptMainText.x = 0; } } else { interactionPromptContainer.visible = false; } } else { interactionPromptContainer.visible = false; } controls.attack = controlButtons.attack.pressed; } function getDistanceToNearestWall(x, y) { var minDist = Infinity; var checkRadius = 2; var startX = Math.max(0, fastFloor(x) - checkRadius); var endX = Math.min(MAP_SIZE - 1, fastFloor(x) + checkRadius); var startY = Math.max(0, fastFloor(y) - checkRadius); var endY = Math.min(MAP_SIZE - 1, fastFloor(y) + checkRadius); for (var cellY = startY; cellY <= endY; cellY++) { for (var cellX = startX; cellX <= endX; cellX++) { if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].type === 1) { var wallLeft = cellX; var wallRight = cellX + 1; var wallTop = cellY; var wallBottom = cellY + 1; var distX = Math.max(0, Math.max(wallLeft - x, x - wallRight)); var distY = Math.max(0, Math.max(wallTop - y, y - wallBottom)); var distToWall = Math.sqrt(distX * distX + distY * distY); minDist = Math.min(minDist, distToWall); } } } return minDist; } function pushPlayerAwayFromWalls() { var currentDist = getDistanceToNearestWall(player.x, player.y); if (currentDist < WALL_BUFFER) { var pushDistance = WALL_BUFFER - currentDist + 0.05; var gradStep = 0.01; var distRight = getDistanceToNearestWall(player.x + gradStep, player.y); var distLeft = getDistanceToNearestWall(player.x - gradStep, player.y); var distUp = getDistanceToNearestWall(player.x, player.y - gradStep); var distDown = getDistanceToNearestWall(player.x, player.y + gradStep); var gradX = (distRight - distLeft) / (2 * gradStep); var gradY = (distDown - distUp) / (2 * gradStep); var gradMag = Math.sqrt(gradX * gradX + gradY * gradY); if (gradMag > 0) { gradX /= gradMag; gradY /= gradMag; var newX = player.x + gradX * pushDistance; var newY = player.y + gradY * pushDistance; if (canMoveTo(newX, newY)) { player.x = newX; player.y = newY; } else { var angles = [0, Math.PI / 4, MATH_PI_HALF, 3 * Math.PI / 4, Math.PI, 5 * Math.PI / 4, 3 * Math.PI / 2, 7 * Math.PI / 4]; for (var i = 0; i < angles.length; i++) { var testX = player.x + Math.cos(angles[i]) * pushDistance; var testY = player.y + Math.sin(angles[i]) * pushDistance; if (canMoveTo(testX, testY)) { player.x = testX; player.y = testY; break; } } } } } } function canMoveTo(targetX, targetY) { var cellX = fastFloor(targetX); var cellY = fastFloor(targetY); if (cellX < 0 || cellX >= MAP_SIZE || cellY < 0 || cellY >= MAP_SIZE || !map[cellY] || !map[cellY][cellX]) { return false; } if (map[cellY][cellX].type === 1) { return false; } for (var i = 0; i < treasures.length; i++) { var t = treasures[i]; if (fastFloor(t.mapX) === cellX && fastFloor(t.mapY) === cellY && !t.isOpen) { return false; } } var distToWall = getDistanceToNearestWall(targetX, targetY); return distToWall >= WALL_BUFFER; } function canMonsterMoveTo(targetX, targetY, monster) { var targetCellX = fastFloor(targetX); var targetCellY = fastFloor(targetY); if (targetCellX < 0 || targetCellX >= MAP_SIZE || targetCellY < 0 || targetCellY >= MAP_SIZE || !map[targetCellY] || !map[targetCellY][targetCellX]) { return false; } if (map[targetCellY][targetCellX].type === 1) { return false; } for (var i = 0; i < monsters.length; i++) { var otherMonster = monsters[i]; if (otherMonster !== monster && fastFloor(otherMonster.mapX) === targetCellX && fastFloor(otherMonster.mapY) === targetCellY) { return false; } } var distToActualWall = getDistanceToNearestWall(targetX, targetY); return distToActualWall >= MONSTER_WALL_BUFFER; } function updateMonsterPosition(monster, newMapX, newMapY) { var oldCellX = fastFloor(monster.mapX); var oldCellY = fastFloor(monster.mapY); var newCellX = fastFloor(newMapX); var newCellY = fastFloor(newMapY); if (oldCellX !== newCellX || oldCellY !== newCellY) { if (map[oldCellY] && map[oldCellY][oldCellX]) { map[oldCellY][oldCellX].removeMonster(); } if (map[newCellY] && map[newCellY][newCellX]) { map[newCellY][newCellX].addMonster(); } else { if (map[oldCellY] && map[oldCellY][oldCellX]) { map[oldCellY][oldCellX].addMonster(); } return; } } tween(monster, { mapX: newMapX, mapY: newMapY }, { duration: 300, easing: function easing(t) { return 1 - Math.pow(1 - t, 4); } }); } function updatePlayerMovement(deltaTime) { var moveSpeed = PLAYER_MOVE_SPEED * deltaTime; var turnSpeed = PLAYER_TURN_SPEED * deltaTime; var didMove = false; if (player.lastX === undefined) { player.lastX = player.x; } if (player.lastY === undefined) { player.lastY = player.y; } 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; } if (isJoystickConsideredActiveForMovement) { var joystickMagnitudeX = Math.abs(currentJoystickX); var rampedTurnValue = 0; var JOYSTICK_TURN_DEAD_ZONE = 0.15; var JOYSTICK_TURN_RAMP_EXPONENT = 2.5; if (joystickMagnitudeX > JOYSTICK_TURN_DEAD_ZONE) { var normalizedInputX = (joystickMagnitudeX - JOYSTICK_TURN_DEAD_ZONE) / (1.0 - JOYSTICK_TURN_DEAD_ZONE); rampedTurnValue = Math.pow(normalizedInputX, JOYSTICK_TURN_RAMP_EXPONENT); } var rampedTurnAmount = Math.sign(currentJoystickX) * rampedTurnValue; turnAmount = rampedTurnAmount * turnSpeed * 1.4; player.dir += turnAmount; while (player.dir < 0) { player.dir += MATH_PI_2; } while (player.dir >= MATH_PI_2) { player.dir -= MATH_PI_2; } } 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; } } var dx = 0, dy = 0; var actualMoveSpeed = moveSpeed * player.moveSpeedMultiplier; if (isJoystickConsideredActiveForMovement) { moveAmount = -currentJoystickY * actualMoveSpeed; if (Math.abs(moveAmount) > 0.01) { dx += Math.cos(player.dir) * moveAmount; dy += Math.sin(player.dir) * moveAmount; didMove = true; } } else if (controls.forward) { dx += Math.cos(player.dir) * actualMoveSpeed; dy += Math.sin(player.dir) * actualMoveSpeed; didMove = true; } else if (controls.backward) { dx -= Math.cos(player.dir) * actualMoveSpeed; dy -= Math.sin(player.dir) * actualMoveSpeed; didMove = true; } var inputMoveIntent = didMove; didMove = false; if (dx !== 0 || dy !== 0) { var currentX = player.x; var currentY = player.y; var targetX = currentX + dx; var targetY = currentY + dy; if (canMoveTo(targetX, targetY)) { player.x = targetX; player.y = targetY; didMove = true; } else { var moved = false; if (dx !== 0 && canMoveTo(currentX + dx, currentY)) { player.x = currentX + dx; moved = true; } if (dy !== 0 && canMoveTo(currentX, currentY + dy)) { player.y = currentY + dy; moved = true; } if (!moved) { var maxSteps = 10; for (var step = 1; step <= maxSteps; step++) { var fraction = step / maxSteps; if (dx !== 0) { var testX = currentX + dx * fraction; if (canMoveTo(testX, currentY)) { player.x = testX; moved = true; break; } } if (dy !== 0) { var testY = currentY + dy * fraction; if (canMoveTo(currentX, testY)) { player.y = testY; moved = true; break; } } } } didMove = moved; } } pushPlayerAwayFromWalls(); if (didMove && inputMoveIntent && LK.ticks % 20 === 0) { LK.getSound('walk').play(); } if (controls.attack && canAttack && !controlButtons.attack.isInteract) { attackAction(); } checkMonsterCollisions(); checkTreasureCollisions(); checkPowerUpCollisions(); updateMiniMap(); player.lastX = player.x; player.lastY = player.y; } function checkPowerUpCollisions() { for (var i = powerUps.length - 1; i >= 0; i--) { var powerUp = powerUps[i]; if (powerUp.collected) { continue; } var dx = powerUp.mapX - player.x; var dy = powerUp.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.6) {} } } function attackAction() { if (!canAttack) { return; } canAttack = false; var currentAttackCooldown = attackCooldown; if (player.attackBuffActive) { currentAttackCooldown /= 2; } var projectile = new Projectile(); projectile.fire(globalRightHand.x, globalRightHand.y - 400); LK.getSound('playerprojectile').play(); tween(globalRightHand, { y: 2732 - 50, rotation: -0.1 }, { duration: 100, onFinish: function onFinish() { tween(globalRightHand, { y: 2732, rotation: 0 }, { duration: 300 }); } }); projectiles.push(projectile); projectileLayer.addChild(projectile); var attackButton = controlButtons.attack; attackButton.updateCooldown(0); var _cooldownTick = function cooldownTick(progress) { attackButton.updateCooldown(progress); if (progress < 1) { LK.setTimeout(function () { _cooldownTick(progress + 0.05); }, currentAttackCooldown / 20); } }; _cooldownTick(0); LK.setTimeout(function () { canAttack = true; }, currentAttackCooldown); } function updateProjectiles(deltaTime) { for (var i = projectiles.length - 1; i >= 0; i--) { var projectile = projectiles[i]; var remove = projectile.update(deltaTime); if (remove) { projectile.destroy(); projectiles.splice(i, 1); continue; } if (!remove && projectile instanceof FireballProjectile) { var dx_player_fireball = projectile.worldX - player.x; var dy_player_fireball = projectile.worldY - player.y; var dz_player_fireball = projectile.worldZ - 0.5; var dist_player_sq_fireball = dx_player_fireball * dx_player_fireball + dy_player_fireball * dy_player_fireball + dz_player_fireball * dz_player_fireball; var PLAYER_HITBOX_RADIUS_FOR_FIREBALL = 0.45; if (dist_player_sq_fireball < PLAYER_HITBOX_RADIUS_FOR_FIREBALL * PLAYER_HITBOX_RADIUS_FOR_FIREBALL) { var baseFireballDamage = projectile.damage; var damageReductionFireball = player.defense * 0.5; var damageTakenByFireball = Math.max(0.5, baseFireballDamage - damageReductionFireball); player.health -= damageTakenByFireball; updateUI(); LK.effects.flashScreen(0xffaa00, 200); LK.getSound('hit').play(); createParticleExplosion(projectile.worldX, projectile.worldY, 15, 0xFF8C00, false); if (player.health <= 0) { handlePlayerDeathSequence(); } projectile.destroy(); projectiles.splice(i, 1); continue; } } var hitMonster = false; var hitMonsterIndex = -1; for (var j = 0; j < monsters.length; j++) { var monster = monsters[j]; if (monster.visible) { var monsterDx = monster.mapX - player.x; var monsterDy = monster.mapY - player.y; var monsterDist = Math.sqrt(monsterDx * monsterDx + monsterDy * monsterDy); var projectileWorldDist = projectile.distance; var distanceDifference = Math.abs(projectileWorldDist - monsterDist); var depthHit = distanceDifference < 0.5; var horizontalDifference = Math.abs(projectile.x - monster.x); var COLLISION_WIDTH_FACTOR = 0.6; var horizontalHit = horizontalDifference < monster.width * COLLISION_WIDTH_FACTOR; if (depthHit && horizontalHit) { if (projectile.hitMonsters.indexOf(monster) !== -1) { continue; } projectile.hitMonsters.push(monster); hitMonster = true; hitMonsterIndex = j; break; } } } if (hitMonster && hitMonsterIndex !== -1) { var monster = monsters[hitMonsterIndex]; var killed = monster.takeDamage(player.attackPower); createParticleExplosion(monster.mapX, monster.mapY, 8, 0xFF0000, false); if (killed) { var explosionParticleCount = 150; var explosionTintColor = 0xFF0000; if (monster instanceof ImpMonster) { explosionParticleCount = 75; } else if (monster instanceof EyeballMonster) { explosionParticleCount = 112; } createParticleExplosion(monster.mapX, monster.mapY, explosionParticleCount, explosionTintColor, false); LK.getSound('enemyexplosion').play(); LK.setTimeout(function () { LK.getSound('pixeldrop').play(); }, 250); map[fastFloor(monster.mapY)][fastFloor(monster.mapX)].removeMonster(); monster.destroy(); monsters.splice(hitMonsterIndex, 1); // player.score += 10; // Score removed updateUI(); checkLevelCompletion(); } projectile.destroy(); projectiles.splice(i, 1); } } } function checkMonsterCollisions() { var currentTime = Date.now(); for (var i = 0; i < monsters.length; i++) { var monster = monsters[i]; var dx = monster.mapX - player.x; var dy = monster.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); var collisionDistance = 0.5; if (monster instanceof ImpMonster) { collisionDistance = 0.35; } if (dist < collisionDistance && monster.canAttack) { // TRIGGER ANIMATION BEFORE DEALING DAMAGE if ((monster instanceof EyeballMonster || monster instanceof ImpMonster) && !monster.isAttacking) { if (typeof monster.performAttackAnimation === 'function') { monster.performAttackAnimation(); } } // Then deal damage... var baseDamage = monster.baseAttack || 1; var damageReduction = player.defense * 0.5; var damageTaken = Math.max(0.5, baseDamage - damageReduction); player.health -= damageTaken; updateUI(); LK.effects.flashScreen(0xff0000, 300); LK.getSound('playerhurt').play(); // player.x -= dx * 0.3; // Player pushback removed // player.y -= dy * 0.3; // Player pushback removed monster.canAttack = false; monster.lastAttackTime = currentTime; // Ensure monster.attackCooldown is a valid number, default if not var monsterCooldown = typeof monster.attackCooldown === 'number' && monster.attackCooldown > 0 ? monster.attackCooldown : 2000; LK.setTimeout(function () { // Check if monster still exists and is part of the game if (monster && monster.parent) { monster.canAttack = true; } }, monsterCooldown); // Monster flash on hit (existing tween logic) if (monster && monster.parent) { // Ensure monster exists before tweening tween(monster, { alpha: 0.5 }, { duration: 200, onFinish: function onFinish() { if (monster && monster.parent) { // Check again in onFinish tween(monster, { alpha: 1 }, { duration: 200 }); } } }); } if (player.health <= 0) { handlePlayerDeathSequence(); } break; // Only one monster attacks per frame } } } 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); } } function checkLevelCompletion() { if (monsters.length === 0) { if (gate) { tween(gate, { alpha: 0.2 }, { duration: 300, onFinish: function onFinish() { tween(gate, { alpha: 1 }, { duration: 300 }); } }); var gateHintText = new Text2('Find the exit', { size: 80, // Increased size to match upgrade messages fill: 0x00FF55, stroke: 0x000000, // Added stroke for better visibility strokeThickness: 7, // Added stroke thickness align: 'center' // Ensure text is centered }); gateHintText.anchor.set(0.5, 0.5); gateHintText.x = SCREEN_WIDTH_HALF; gateHintText.y = SCREEN_HEIGHT_HALF; // Centered vertically like other important messages gameLayer.addChild(gateHintText); LK.setTimeout(function () { gateHintText.destroy(); }, 3000); } } else { 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 = SCREEN_WIDTH_HALF; monsterHintText.y = 200; gameLayer.addChild(monsterHintText); LK.setTimeout(function () { monsterHintText.destroy(); }, 3000); } } } function updateMiniMap() { // Update player marker position (existing logic) playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE; playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE; // Player's current cell coordinates var playerCellX = fastFloor(player.x); var playerCellY = fastFloor(player.y); // Iterate through all map cells to update visibility for (var y = 0; y < MAP_SIZE; y++) { for (var x = 0; x < MAP_SIZE; x++) { if (map[y] && map[y][x]) { // Ensure cell object exists var cell = map[y][x]; // Check if it's an outer wall var isOuterWall = x === 0 || x === MAP_SIZE - 1 || y === 0 || y === MAP_SIZE - 1; // Check if cell is near the player var dxPlayer = Math.abs(x - playerCellX); var dyPlayer = Math.abs(y - playerCellY); var isNearPlayer = dxPlayer <= MINI_MAP_REVEAL_RADIUS && dyPlayer <= MINI_MAP_REVEAL_RADIUS; // If near player, mark as revealed if (isNearPlayer) { if (revealedCells[y]) { // Ensure row exists revealedCells[y][x] = true; } } var isRevealed = false; if (revealedCells[y] && revealedCells[y][x] !== undefined) { isRevealed = revealedCells[y][x]; } // Cell is visible if it's an outer wall OR if it has been revealed cell.visible = isOuterWall || isRevealed; } } } // The direction indicator part was commented out in the original, // if it needs to be drawn, that logic would go here. // var dirX = Math.cos(player.dir) * 15; // var dirY = Math.sin(player.dir) * 15; } game.update = function () { if (isGameOverAnimating) { return; } if (gameState === 'title') { return; } var currentTime = Date.now(); var deltaTime = currentTime - lastTime; lastTime = currentTime; updateControls(); updatePlayerMovement(deltaTime); renderableObjects = []; rayCasting(); renderEntities(); EntityRenderer.updateEntities(renderableObjects); if (LK.ticks % 15 === 0) { for (var i = 0; i < monsters.length; i++) { MonsterAI.moveTowardsPlayer(monsters[i], player.x, player.y, map); } } for (var i = 0; i < monsters.length; i++) { var monster = monsters[i]; if (monster.visible && (monster.mapX !== monster.lastMoveX || monster.mapY !== monster.lastMoveY)) { monster.updateAnimation(); monster.lastMoveX = monster.mapX; monster.lastMoveY = monster.mapY; } if (monster instanceof ImpMonster) { monster.updateSoundBehavior(); } if (monster instanceof EyeballMonster) { monster.updateSoundBehavior(); } if (monster instanceof Monster) { if (typeof monster.updateSoundBehavior === 'function') { monster.updateSoundBehavior(); } } } for (var i = 0; i < fountains.length; i++) { var fountainInstance = fountains[i]; if (fountainInstance.updateSoundBehavior) { fountainInstance.updateSoundBehavior(); } } updateProjectiles(deltaTime); for (var i = 0; i < fountains.length; i++) { if (fountains[i].updateParticles) { fountains[i].updateParticles(); } } particlePoolManager.updateActiveParticles(); MemoryManager.performCleanup(); }; var powerUpEffectTextDisplaying = null; function showPowerUpEffectText(message) { if (powerUpEffectTextDisplaying && powerUpEffectTextDisplaying.parent) { tween.stop(powerUpEffectTextDisplaying); tween.stop(powerUpEffectTextDisplaying); powerUpEffectTextDisplaying.parent.removeChild(powerUpEffectTextDisplaying); powerUpEffectTextDisplaying.destroy(); powerUpEffectTextDisplaying = null; } var effectText = new Text2(message, { size: 80, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 7, align: 'center', wordWrap: true, wordWrapWidth: 1800 }); effectText.anchor.set(0.5, 0.5); effectText.x = SCREEN_WIDTH_HALF; effectText.y = SCREEN_HEIGHT_HALF; effectText.alpha = 1.0; gameLayer.addChild(effectText); powerUpEffectTextDisplaying = effectText; LK.setTimeout(function () { if (effectText && effectText.parent) { tween(effectText, { alpha: 0 }, { duration: 500, easing: tween.linear, onFinish: function onFinish() { if (effectText.parent) { effectText.parent.removeChild(effectText); } effectText.destroy(); if (powerUpEffectTextDisplaying === effectText) { powerUpEffectTextDisplaying = null; } } }); } }, 2500); } setupGame(); game.down = function (x, y, obj) { if (gameState === 'title') { return; } activeControlForGlobalHandlers = null; var eventObjForAttack = Object.assign({}, obj); eventObjForAttack.stopPropagation = false; var attackBtn = controlButtons.attack; var attackBtnLocalX = x - attackBtn.x; var attackBtnLocalY = y - attackBtn.y; var attackBtnHitRadius = 150 * 3.5; var attackBtnDistSq = attackBtnLocalX * attackBtnLocalX + attackBtnLocalY * attackBtnLocalY; if (attackBtnDistSq <= attackBtnHitRadius * attackBtnHitRadius) { activeControlForGlobalHandlers = attackBtn; attackBtn.down(attackBtnLocalX, attackBtnLocalY, eventObjForAttack); return; } var joystick = controlButtons.joystick; if (x < 800 && y > 2732 - 800) { activeControlForGlobalHandlers = joystick; var eventObjForJoystick = Object.assign({}, obj); eventObjForJoystick.stopPropagation = false; joystick.down(x - joystick.x, y - joystick.y, eventObjForJoystick); } }; game.up = function (x, y, obj) { if (gameState === 'title') { return; } 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) { if (controlButtons.joystick.active) { var joystickLocalX = x - controlButtons.joystick.x; var joystickLocalY = y - controlButtons.joystick.y; controlButtons.joystick.up(joystickLocalX, joystickLocalY, obj); } } activeControlForGlobalHandlers = null; }; game.move = function (x, y, obj) { if (gameState === 'title') { return; } if (activeControlForGlobalHandlers === controlButtons.joystick) { if (controlButtons.joystick.active) { var joystickLocalX = x - controlButtons.joystick.x; var joystickLocalY = y - controlButtons.joystick.y; controlButtons.joystick.move(joystickLocalX, joystickLocalY, obj); } } }; var MonsterAI = { moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) { var monsterX = monster.mapX; var monsterY = monster.mapY; var dxToPlayer = playerX - monsterX; var dyToPlayer = playerY - monsterY; var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer); var hasClearLOS = false; if (distToPlayer <= MONSTER_AGGRO_RANGE) { var rayHit = castRayToPoint(monsterX, monsterY, playerX, playerY); hasClearLOS = !rayHit.hit || rayHit.dist >= distToPlayer - 0.1; } monster.canSeePlayer = hasClearLOS; var currentTimeForAI = Date.now(); // Boss logic remains the same... if (monster instanceof BossMonster) { var effectiveMoveSpeedBoss = typeof monster.moveSpeed !== 'undefined' ? monster.moveSpeed : 0.15; var moveDirXBoss = 0; var moveDirYBoss = 0; var bossShouldChase = false; if (distToPlayer <= MONSTER_AGGRO_RANGE * 1.5) { bossShouldChase = true; } if (monster.canSeePlayer && distToPlayer <= MONSTER_AGGRO_RANGE) { if (!monster.canShootFireball && currentTimeForAI - monster.lastFireballTime > monster.fireballCooldown + 1000) { monster.canShootFireball = true; } if (monster.canShootFireball) { monster.performFireballAttack(playerX, playerY); } } if (!bossShouldChase) { return; } if (distToPlayer < BOSS_TOO_CLOSE_DISTANCE) { if (distToPlayer > 0.1) { moveDirXBoss = -dxToPlayer / distToPlayer; moveDirYBoss = -dyToPlayer / distToPlayer; } } else if (distToPlayer > BOSS_PREFERRED_DISTANCE) { if (distToPlayer > 0.1) { moveDirXBoss = dxToPlayer / distToPlayer; moveDirYBoss = dyToPlayer / distToPlayer; } } else { if (monster.canSeePlayer) { var perpendicularAngle = Math.atan2(dyToPlayer, dxToPlayer) + (Math.random() > 0.5 ? MATH_PI_HALF : -MATH_PI_HALF); moveDirXBoss = Math.cos(perpendicularAngle); moveDirYBoss = Math.sin(perpendicularAngle); } else { moveDirXBoss = dxToPlayer / distToPlayer; moveDirYBoss = dyToPlayer / distToPlayer; } } if (moveDirXBoss !== 0 || moveDirYBoss !== 0) { var potentialNewXBoss = monsterX + moveDirXBoss * effectiveMoveSpeedBoss; var potentialNewYBoss = monsterY + moveDirYBoss * effectiveMoveSpeedBoss; if (canMonsterMoveTo(potentialNewXBoss, potentialNewYBoss, monster)) { updateMonsterPosition(monster, potentialNewXBoss, potentialNewYBoss); } else { if (moveDirXBoss !== 0 && canMonsterMoveTo(monsterX + moveDirXBoss * effectiveMoveSpeedBoss, monsterY, monster)) { updateMonsterPosition(monster, monsterX + moveDirXBoss * effectiveMoveSpeedBoss, monsterY); } else if (moveDirYBoss !== 0 && canMonsterMoveTo(monsterX, monsterY + moveDirYBoss * effectiveMoveSpeedBoss, monster)) { updateMonsterPosition(monster, monsterX, monsterY + moveDirYBoss * effectiveMoveSpeedBoss); } else { var alternativeAngles = [0, Math.PI / 4, MATH_PI_HALF, 3 * Math.PI / 4, Math.PI, 5 * Math.PI / 4, 3 * Math.PI / 2, 7 * Math.PI / 4]; for (var a = 0; a < alternativeAngles.length; a++) { var altX = monsterX + Math.cos(alternativeAngles[a]) * effectiveMoveSpeedBoss * 0.5; var altY = monsterY + Math.sin(alternativeAngles[a]) * effectiveMoveSpeedBoss * 0.5; if (canMonsterMoveTo(altX, altY, monster)) { updateMonsterPosition(monster, altX, altY); break; } } } } } return; } var shouldChase = false; if (distToPlayer <= MONSTER_AGGRO_RANGE) { if (monster.canSeePlayer) { shouldChase = true; } else if (distToPlayer < MONSTER_CLOSE_PROXIMITY_RANGE) { shouldChase = true; } } if (monster instanceof ImpMonster) { monster.isAggroed = shouldChase; } if (!shouldChase) { monster.pathToPlayer = []; return; } monster.pathToPlayer = [{ x: playerX, y: playerY }]; var engagementDistance = MAX_MONSTER_PLAYER_ENGAGEMENT_DISTANCE; if (monster instanceof ImpMonster) { engagementDistance = 0.4; } // TRIGGER ATTACK ANIMATION when in engagement range if (distToPlayer <= engagementDistance) { if (monster.canAttack) { if ((monster instanceof EyeballMonster || monster instanceof ImpMonster) && !monster.isAttacking) { if (typeof monster.performAttackAnimation === 'function') { monster.performAttackAnimation(); } } } } // ALWAYS TRY TO GET CLOSER until in collision range var collisionDistance = 0.5; if (monster instanceof ImpMonster) { collisionDistance = 0.35; } // Only stop moving when actually in collision range if (distToPlayer <= collisionDistance) { return; // Close enough for collision detection to handle } // Continue moving toward player var effectiveMoveSpeed = typeof monster.moveSpeed !== 'undefined' ? monster.moveSpeed : 0.2; var moveDirX = 0; var moveDirY = 0; if (distToPlayer > 0) { moveDirX = dxToPlayer / distToPlayer; moveDirY = dyToPlayer / distToPlayer; } var potentialNewX = monsterX + moveDirX * effectiveMoveSpeed; var potentialNewY = monsterY + moveDirY * effectiveMoveSpeed; if (canMonsterMoveTo(potentialNewX, potentialNewY, monster)) { updateMonsterPosition(monster, potentialNewX, potentialNewY); } else { var slid = false; if (moveDirX !== 0 && canMonsterMoveTo(monsterX + moveDirX * effectiveMoveSpeed, monsterY, monster)) { updateMonsterPosition(monster, monsterX + moveDirX * effectiveMoveSpeed, monsterY); slid = true; } if (!slid && moveDirY !== 0 && canMonsterMoveTo(monsterX, monsterY + moveDirY * effectiveMoveSpeed, monster)) { updateMonsterPosition(monster, monsterX, monsterY + moveDirY * effectiveMoveSpeed); slid = true; } } } }; function ensureMapConnectivity() { 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 = []; queue.push({ x: 1, y: 1 }); visited[1][1] = true; while (queue.length > 0) { var current = queue.shift(); var x = current.x; var y = current.y; 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; 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 }); } } } for (var y = 1; y < MAP_SIZE - 1; y++) { for (var x = 1; x < MAP_SIZE - 1; x++) { if (map[y][x].type === 0 && !visited[y][x]) { 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; } } } } if (nearestX !== -1) { createPath(x, y, nearestX, nearestY); 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) { startX = Math.max(1, Math.min(MAP_SIZE - 2, startX)); startY = Math.max(1, Math.min(MAP_SIZE - 2, startY)); endX = Math.max(1, Math.min(MAP_SIZE - 2, endX)); endY = Math.max(1, Math.min(MAP_SIZE - 2, endY)); if (Math.random() < 0.5) { var x = startX; while (x !== endX) { x += x < endX ? 1 : -1; if (x >= 1 && x < MAP_SIZE - 1 && map[startY][x].type === 1) { map[startY][x].setType(0); } } var y = startY; while (y !== endY) { y += y < endY ? 1 : -1; if (y >= 1 && y < MAP_SIZE - 1 && map[y][endX].type === 1) { map[y][endX].setType(0); } } } else { var y = startY; while (y !== endY) { y += y < endY ? 1 : -1; if (y >= 1 && y < MAP_SIZE - 1 && map[y][startX].type === 1) { map[y][startX].setType(0); } } var x = startX; while (x !== endX) { x += x < endX ? 1 : -1; if (x >= 1 && x < MAP_SIZE - 1 && map[endY][x].type === 1) { map[endY][x].setType(0); } } } }
===================================================================
--- original.js
+++ change.js
@@ -1958,9 +1958,9 @@
var MONSTER_WALL_BUFFER = 0.22;
var MONSTER_COUNT = 8;
var TREASURE_COUNT = 10;
var BOSS_FIREBALL_COOLDOWN = 3000;
-var FIREBALL_SPEED = 0.04;
+var FIREBALL_SPEED = 0.042;
var FIREBALL_DAMAGE = 2;
var FIREBALL_MAX_DISTANCE = 12;
var BOSS_PREFERRED_DISTANCE = 6.0;
var BOSS_TOO_CLOSE_DISTANCE = 3.5;
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