User prompt
update with: if (graphicsQuality === 'Low') { // Add slight texture offset based on wall position to break up banding var textureOffset = (wallMapX + wallMapY * 3) % 4; activeWallSprite.x = textureOffset * 0.5; // Slight horizontal offset }
Code edit (7 edits merged)
Please save this source code
User prompt
Reduce starting monster number to 5 monsters for level 1. Increase by 1 each non boss level. From level 6 onwards, increase by 2 each non boss level.
User prompt
When the player loses their last health, instead of calling game over right away, animate the righthand and have it start sinking below the screen. Also play the demon laugh sound effect and fade the screen to red over 2 seconds. Put the game over call in a time out to trigger after that 2 seconds. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the title screen fade to black take 2 seconds. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a slight scale shrink and return to regular scale feedback for start button when pressed. When pressed also play demon laugh and fade the screen to black before starting the game. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
On game over, stop the dungeon music.
User prompt
On game over stop dungeon music.
User prompt
update with: var MonsterAI = { moveTowardsPlayer: function(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) { // ... existing boss code 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) { 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; } } } };
User prompt
update as needed with: 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) { 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.y -= dy * 0.3; monster.canAttack = false; monster.lastAttackTime = currentTime; LK.setTimeout(function() { monster.canAttack = true; }, monster.attackCooldown); // Rest of existing code... break; } } }
Code edit (5 edits merged)
Please save this source code
User prompt
Increase minimap size by 25%
User prompt
update with: function createUI() { // Create stats panel in top right - moved further left to account for larger text const 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(`❤️ Health: ${player.health}/${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(`⚔️ Attack: ${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(`🛡️ Defense: ${player.defense}`, { 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(`💨 Speed: ${player.moveSpeedMultiplier.toFixed(1)}x`, { size: 54, // Increased from 36 fill: 0x55FF55, stroke: 0x000000, s
User prompt
update with: function createUI() { // Create stats panel in top right const statsPanel = new Container(); LK.gui.topRight.addChild(statsPanel); statsPanel.x = -280; statsPanel.y = 20; // Health with current/max format healthText = new Text2(`❤️ Health: ${player.health}/${player.maxHealth}`, { size: 36, fill: 0xFF5555, stroke: 0x000000, strokeThickness: 2 }); healthText.anchor.set(0, 0); statsPanel.addChild(healthText); healthText.y = 0; // Attack stat attackText = new Text2(`⚔️ Attack: ${player.attackPower}`, { size: 36, fill: 0xFFAA55, stroke: 0x000000, strokeThickness: 2 }); attackText.anchor.set(0, 0); statsPanel.addChild(attackText); attackText.y = 50; // Defense stat defenseText = new Text2(`🛡️ Defense: ${player.defense}`, { size: 36, fill: 0x5555FF, stroke: 0x000000, strokeThickness: 2 }); defenseText.anchor.set(0, 0); statsPanel.addChild(defenseText); defenseText.y = 100; // Speed stat speedText = new Text2(`💨 Speed: ${player.moveSpeedMultiplier.toFixed(1)}x`, { size: 36, fill: 0x55FF55, stroke: 0x000000, strokeThickness: 2 }); speedText.anchor.set(0, 0); statsPanel.addChild(speedText); speedText.y = 150; // Level and Monsters under mini map levelText = new Text2(`Level: ${player.level}`, { size: 32, fill: 0x55FF55, stroke: 0x000000, strokeThickness: 2 }); levelText.anchor.set(0, 0); gameLayer.addChild(levelText); levelText.x = miniMap.x; levelText.y = miniMap.y + miniMap.height + 10; monsterText = new Text2('Monsters: 0', { size: 32, fill: 0xFF9955, stroke: 0x000000, strokeThickness: 2 }); monsterText.anchor.set(0, 0); gameLayer.addChild(monsterText); monsterText.x = miniMap.x; monsterText.y = levelText.y + 40; // Remove score-related code entirely } function updateUI() { healthText.setText(`❤️ Health: ${player.health}/${player.maxHealth}`); attackText.setText(`⚔️ Attack: ${player.attackPower}`); defenseText.setText(`🛡️ Defense: ${player.defense}`); speedText.setText(`💨 Speed: ${player.moveSpeedMultiplier.toFixed(1)}x`); levelText.setText(`Level: ${player.level}`); monsterText.setText(`Monsters: ${monsters.length}`); // Remove LK.setScore(player.score); line }
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
create a title screen when the game loads that uses the titlebackground with the logo centered and the startbutton underneath to start the game
User prompt
create a title screen when the game loads that uses the titlebackground with the logo centered and the startbutton underneath to start the game
User prompt
After every 5 levels are complete, raise all monster attacks by 1 and health by 3. Raise boss attack by 2 and health by 10.
User prompt
Play bosschant sound effect when the boss starts charging his fireball.
User prompt
Play fireball sound effect when the boss fires a fireball.
User prompt
Make sure there’s always at least one treasure in a level even there’s no dead end.
User prompt
Play the ogre sound effect on interval when the player is in proximity of the monster enemy.
Code edit (1 edits merged)
Please save this source code
User prompt
Play powerup sound effect when power up is collected.
/**** * 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.update = function (playerX, playerY, playerDir) { floorContainer.removeChildren(); ceilingContainer.removeChildren(); 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; var stripIndex = 0; for (var y = HORIZON_Y; y < SCREEN_HEIGHT; y += stripHeight) { var distanceFromHorizon = y - HORIZON_Y; if (distanceFromHorizon <= 0) { continue; } var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; if (worldDistance > MAX_RENDER_DISTANCE) { continue; } 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; } var floorStrip; if (stripIndex < floorStrips.length) { floorStrip = floorStrips[stripIndex]; } else { floorStrip = LK.getAsset('mapFloor', { anchorX: 0, anchorY: 0 }); floorStrips.push(floorStrip); } floorStrip.x = 0; floorStrip.y = y; floorStrip.width = SCREEN_WIDTH; floorStrip.height = stripHeight; floorStrip.alpha = shadeFactor; floorContainer.addChild(floorStrip); stripIndex++; } stripIndex = 0; for (var y = HORIZON_Y - stripHeight; y >= 0; y -= stripHeight) { var distanceFromHorizon = HORIZON_Y - y; if (distanceFromHorizon <= 0) { continue; } var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon; if (worldDistance > MAX_RENDER_DISTANCE) { 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; if (stripIndex < ceilingStrips.length) { ceilingStrip = ceilingStrips[stripIndex]; } else { ceilingStrip = LK.getAsset('ceiling', { anchorX: 0, anchorY: 0 }); ceilingStrips.push(ceilingStrip); } ceilingStrip.x = 0; ceilingStrip.y = y; ceilingStrip.width = SCREEN_WIDTH; ceilingStrip.height = stripHeight; ceilingStrip.alpha = shadeFactor; ceilingContainer.addChild(ceilingStrip); stripIndex++; } }; 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.type === 1) { self.attachAsset('mapWall', { anchorX: 0, anchorY: 0 }); } else { self.attachAsset('mapFloor', { anchorX: 0, anchorY: 0 }); } }; self.addMonster = function () { if (self.type === 0 && !self.monster && !self.treasure && !self.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) { 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; } 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; if (graphicsQuality === 'Low') { // Add slight texture offset based on wall position to break up banding var textureOffset = (wallMapX + wallMapY * 3) % 4; activeWallSprite.x = textureOffset * 0.5; // Slight horizontal offset } else { 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' ? 15 : 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.18; var MONSTER_WALL_BUFFER = 0.25; var MONSTER_COUNT = 8; var TREASURE_COUNT = 10; var BOSS_FIREBALL_COOLDOWN = 3000; var FIREBALL_SPEED = 0.04; 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 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 = 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() { miniMap.removeChildren(); map = []; for (var i = 0; i < monsters.length; i++) { 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 = 5; } 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(); 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); } for (var i = 0; i < allDynamicEntities.length; i++) { SpatialGrid.insert(allDynamicEntities[i]); } var nearbyEntities = SpatialGrid.getNearby(player.x, player.y, MAX_RENDER_DISTANCE); for (var i = 0; i < nearbyEntities.length; i++) { var entity = nearbyEntities[i]; entity.visible = false; var dx = entity.mapX - player.x; var dy = entity.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); entity.renderDist = dist; if (dist >= MAX_RENDER_DISTANCE) { continue; } 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.y -= dy * 0.3; 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 gate!', { size: 60, fill: 0x00FF55 }); gateHintText.anchor.set(0.5, 0.5); gateHintText.x = SCREEN_WIDTH_HALF; gateHintText.y = 200; 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() { playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE; playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE; 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
@@ -1475,9 +1475,15 @@
}
}
activeWallSprite.width = stripWidth;
activeWallSprite.height = wallHeight;
- activeWallSprite.x = 0;
+ if (graphicsQuality === 'Low') {
+ // Add slight texture offset based on wall position to break up banding
+ var textureOffset = (wallMapX + wallMapY * 3) % 4;
+ activeWallSprite.x = textureOffset * 0.5; // Slight horizontal offset
+ } else {
+ 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;
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