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; 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; 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; var gateInteractionInProgress = false; 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 () { gameState = 'game'; if (titleScreenContainer) { game.removeChild(titleScreenContainer); titleScreenContainer.destroy(); titleScreenContainer = null; } initializeGamePlayElements(); }; 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)) { var monstersToPlace = MONSTER_COUNT + fastFloor(player.level * 0.5); 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 var statsPanel = new Container(); LK.gui.topRight.addChild(statsPanel); statsPanel.x = -280; // Adjusted for potentially wider text statsPanel.y = 20; // Health with current/max format healthText = new Text2("\u2764\uFE0F Health: ".concat(player.health, "/").concat(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("\u2694\uFE0F Attack: ".concat(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("\uD83D\uDEE1\uFE0F Defense: ".concat(player.defense), { // Used shield emoji size: 36, fill: 0x5555FF, stroke: 0x000000, strokeThickness: 2 }); defenseText.anchor.set(0, 0); statsPanel.addChild(defenseText); defenseText.y = 100; // Speed stat speedText = new Text2("\uD83D\uDC5F Speed: ".concat(player.moveSpeedMultiplier.toFixed(1), "x"), { // Used shoe emoji 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: ".concat(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 + MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE + 10; // Calculate miniMap height monsterText = new Text2("Monsters: ".concat(monsters.length), { // Initialized with monsters.length 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; // 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 || Math.abs(player.dir - lastPlayerDir) > 0.001; if (playerMoved || 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) { LK.showGameOver(); } 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) { 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); tween(monster, { alpha: 0.5 }, { duration: 200, onFinish: function onFinish() { tween(monster, { alpha: 1 }, { duration: 200 }); } }); if (player.health <= 0) { LK.showGameOver(); } break; } } } function checkTreasureCollisions() { for (var i = treasures.length - 1; i >= 0; i--) { var treasure = treasures[i]; var dx = treasure.mapX - player.x; var dy = treasure.mapY - player.y; var dist = Math.sqrt(dx * dx + dy * dy); } } 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 (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(); 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; } if (distToPlayer <= engagementDistance) { if (monster.canAttack) { if ((monster instanceof EyeballMonster || monster instanceof ImpMonster) && !monster.isAttacking) { monster.performAttackAnimation(); } } return; } 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
@@ -10,9 +10,8 @@
var BossMonster = Container.expand(function () {
var self = Container.call(this);
var frameContainer = new Container();
self.addChild(frameContainer);
- // Boss assets are larger, let's use a size multiplier to make it imposing
var walkFrame1 = LK.getAsset('bosswalk1', {
anchorX: 0.5,
anchorY: 0.5
});
@@ -29,123 +28,103 @@
frameContainer.addChild(walkFrame2);
frameContainer.addChild(attackFrameAsset);
walkFrame1.alpha = 1;
walkFrame2.alpha = 0;
- self.currentFrame = 1; // 1 for walkFrame1, 2 for walkFrame2
+ self.currentFrame = 1;
self.animationTick = 0;
self.currentFireballTimer = null;
self.mapX = 0;
self.mapY = 0;
- self.health = BOSS_CURRENT_BASE_HEALTH; // Boss health
- self.moveSpeed = 0.15; // Slower than Ogre (0.2)
- // Properties for MonsterAI compatibility
+ self.health = BOSS_CURRENT_BASE_HEALTH;
+ self.moveSpeed = 0.15;
self.lastMoveTime = 0;
self.canSeePlayer = false;
self.pathToPlayer = [];
- // Attack properties
- self.attackCooldown = 3000; // ms, Boss attacks a bit slower
+ self.attackCooldown = 3000;
self.lastAttackTime = 0;
- self.canAttack = true; // Cooldown flag for dealing damage
- self.isAttacking = false; // Animation state flag
- self.attackDuration = 800; // ms for the attack visual
- // Fireball attack properties
+ 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; // Number of particles in the charging effect
- var chargingRadius = 2.0; // How far around the boss to spawn particles
- var chargingDuration = 950; // Slightly less than fireball delay for timing
+ var chargingParticleCount = 25;
+ var chargingRadius = 2.0;
+ var chargingDuration = 950;
for (var i = 0; i < chargingParticleCount; i++) {
var particle = particlePoolManager.getParticle();
if (!particle) {
continue;
}
- // Spawn particles in a circle around the boss
- var angle = i / chargingParticleCount * Math.PI * 2;
- var spawnRadius = chargingRadius * (0.7 + Math.random() * 0.6); // Vary distance
+ 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; // Vary height slightly, using targetZ
- // Calculate velocity to pull particle toward target over time
+ 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);
- // Velocity needs to cover the distance in chargingDuration milliseconds
- // Convert milliseconds to ticks (assuming 60fps = 16.67ms per tick)
var durationInTicks = chargingDuration / 16.67;
var vx = dx / durationInTicks;
var vy = dy / durationInTicks;
var vz = dz / durationInTicks;
- // Add some spiraling motion for visual effect
var spiralSpeed = 0.002;
- var spiralAngle = angle + Math.random() * Math.PI * 0.5;
+ var spiralAngle = angle + Math.random() * MATH_PI_HALF;
vx += Math.cos(spiralAngle) * spiralSpeed;
vy += Math.sin(spiralAngle) * spiralSpeed;
- var life = Math.floor(durationInTicks) + 10; // Live slightly longer than duration
- var chargingColor = 0xFF8C00; // Dark orange color for charging effect
+ var life = Math.floor(durationInTicks) + 10;
+ var chargingColor = 0xFF8C00;
particle.init(startX, startY, startZ, vx, vy, vz, life, chargingColor);
- particle.collisionImmunity = life; // Don't bounce off walls during charging
- particle.isChargingParticle = true; // Flag to identify charging particles
+ particle.collisionImmunity = life;
+ particle.isChargingParticle = true;
particleExplosionContainer.addChild(particle);
}
};
self.performFireballAttack = function (playerWorldX, playerWorldY) {
if (!self.canShootFireball || !self.parent) {
return;
}
- console.log("Boss starting fireball attack!"); // Debug output
self.canShootFireball = false;
self.lastFireballTime = Date.now();
- // ALWAYS trigger attack animation as warning
if (!self.isAttacking) {
self.performAttackAnimation();
}
- // Store target position when attack starts
var targetX = playerWorldX;
var targetY = playerWorldY;
- // Calculate fireball spawn position
var fireballStartX = self.mapX + Math.cos(player.dir) * 0.5;
var fireballStartY = self.mapY + Math.sin(player.dir) * 0.5;
var fireballStartZ = 0.8;
- // CREATE CHARGING PARTICLE EFFECT
self.createChargingEffect(fireballStartX, fireballStartY, fireballStartZ);
- // Play boss chant sound effect
LK.getSound('bosschant').play();
- // Delay fireball spawn to give player warning time
var fireballTimer = LK.setTimeout(function () {
if (!self.parent) {
- return; // Boss was destroyed
+ return;
}
- console.log("Boss firing fireball after delay!"); // Debug output
var fireball = new FireballProjectile();
- // Fire at the position where player WAS when attack started
fireball.fire(fireballStartX, fireballStartY, fireballStartZ, targetX, targetY);
projectiles.push(fireball);
projectileLayer.addChild(fireball);
LK.getSound('fireball').play();
- }, 1000); // 600ms warning time
+ }, 1000);
self.currentFireballTimer = fireballTimer;
var resetCooldown = function resetCooldown() {
if (self.parent) {
self.canShootFireball = true;
self.currentFireballTimer = null;
- console.log("Boss fireball cooldown reset"); // Debug output
}
};
LK.setTimeout(resetCooldown, self.fireballCooldown);
};
self.updateAnimation = function () {
if (self.isAttacking) {
- // If currently in attack animation, don't update walk animation
return;
}
self.animationTick++;
- // Change animation frame every 20 ticks (slower than Ogre/Imp's 15)
if (self.animationTick >= 20) {
self.animationTick = 0;
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
if (self.currentFrame === 1) {
@@ -158,28 +137,23 @@
}
};
self.performAttackAnimation = function () {
if (self.isAttacking || !self.parent) {
- // Already visually attacking or destroyed
return;
}
self.isAttacking = true;
- // Hide walking frames, show attack frame
walkFrame1.alpha = 0;
walkFrame2.alpha = 0;
attackFrameAsset.alpha = 1;
LK.setTimeout(function () {
if (!self.parent) {
- // Check if still exists
return;
}
- attackFrameAsset.alpha = 0; // Hide attack frame
- // Revert to the current walking frame
+ attackFrameAsset.alpha = 0;
if (self.currentFrame === 1) {
walkFrame1.alpha = 1;
walkFrame2.alpha = 0;
} else {
- // currentFrame is 2
walkFrame1.alpha = 0;
walkFrame2.alpha = 1;
}
self.isAttacking = false;
@@ -188,9 +162,8 @@
self.takeDamage = function (damageAmount) {
self.health -= damageAmount;
LK.getSound('hit').play();
LK.effects.flashObject(frameContainer, 0xff0000, 400);
- // Clean up any pending fireball timers if boss dies
if (self.health <= 0) {
if (self.currentFireballTimer) {
LK.clearTimeout(self.currentFireballTimer);
self.currentFireballTimer = null;
@@ -199,11 +172,11 @@
walkFrame2.alpha = 0;
if (attackFrameAsset) {
attackFrameAsset.alpha = 0;
}
- return true; // Monster is killed
+ return true;
}
- return false; // Monster survived
+ return false;
};
return self;
});
var ControlButton = Container.expand(function (direction) {
@@ -219,13 +192,12 @@
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
- // Set arrow direction based on button type
if (direction === 'up') {
arrowSprite.rotation = 0;
} else if (direction === 'right') {
- arrowSprite.rotation = Math.PI / 2;
+ arrowSprite.rotation = MATH_PI_HALF;
} else if (direction === 'down') {
arrowSprite.rotation = Math.PI;
} else if (direction === 'left') {
arrowSprite.rotation = Math.PI * 1.5;
@@ -241,83 +213,64 @@
self.direction = direction;
self.pressed = false;
self.updateCooldown = function (ratio) {
if (self.direction === 'attack') {
- // If in interact mode, change color
if (self.isInteract) {
- buttonSprite.tint = 0x33aa33; // Green for interact
+ buttonSprite.tint = 0x33aa33;
buttonSprite.alpha = 1;
} else {
- buttonSprite.tint = 0xaa3333; // Default attack color
- // Update the button alpha based on cooldown ratio (0 to 1)
+ 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') {
- // Stop propagation to prevent the event from affecting the joystick
obj.stopPropagation = true;
- // If joystick is active, store its current state for override
if (controlButtons && controlButtons.joystick && controlButtons.joystick.active) {
joystickOverrideActive = true;
joystickOverrideX = controlButtons.joystick.normalizedX;
joystickOverrideY = controlButtons.joystick.normalizedY;
} else {
- // Ensure override is not active if joystick wasn't active at press time
joystickOverrideActive = false;
}
- // If interact mode, handle based on interactTarget type
if (self.isInteract && self.interactTarget) {
if (self.interactTarget instanceof Treasure && !self.interactTarget.isOpen) {
var treasureToOpen = self.interactTarget;
treasureToOpen.openTreasure();
- // Play sound
LK.getSound('treasureopen').play();
- // Award points
- player.score += treasureToOpen.value * 5;
+ // player.score += treasureToOpen.value * 5; // Score removed
updateUI();
- // Spawn a random powerup at the treasure's location, with animation
- var t = treasureToOpen; // Use the correct treasure object
+ var t = treasureToOpen;
var powerUpTypes = ['speed', 'defense', 'health', 'attack'];
- var randomType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
- var powerUpFromChest = new PowerUp(randomType); //{A} // Renamed to avoid conflict
- // Place at the center of the chest (mapX/Y are world coords, will be projected in renderEntities)
+ var randomType = powerUpTypes[fastFloor(Math.random() * powerUpTypes.length)];
+ var powerUpFromChest = new PowerUp(randomType);
powerUpFromChest.mapX = t.mapX;
powerUpFromChest.mapY = t.mapY;
powerUpFromChest.collected = false;
- // Set initial multiplier for spawn animation. Scale itself will be handled by renderEntities.
powerUpFromChest.spawnScaleMultiplier = 0.1;
- // Initialize new animation properties
powerUpFromChest.verticalOffset = 0;
powerUpFromChest.currentBobOffset = 0;
powerUpFromChest.hasReachedBobHeight = false;
powerUps.push(powerUpFromChest);
if (typeof dynamicEntitiesContainer !== "undefined") {
dynamicEntitiesContainer.addChild(powerUpFromChest);
}
- // Combined Animation Sequence: Scale up and Lift
tween(powerUpFromChest, {
spawnScaleMultiplier: 1.0,
- verticalOffset: 0.10 // Lift by 0.34 world units (reduced by 15%)
+ verticalOffset: 0.10
}, {
duration: 1200,
- // Slower combined animation (1200ms)
easing: tween.elasticOut,
- // Retains a nice "pop"
onFinish: function onFinish() {
- // Start bobbing after combined animation
powerUpFromChest.hasReachedBobHeight = true;
powerUpFromChest.startBobbingAnimation();
}
});
- // Remove treasure from map after a short delay (for open animation)
LK.setTimeout(function () {
- map[Math.floor(t.mapY)][Math.floor(t.mapX)].removeTreasure();
- // Do NOT destroy the treasure object, just leave it open and non-collidable
- // Remove from treasures array so it doesn't show interact again
+ map[fastFloor(t.mapY)][fastFloor(t.mapX)].removeTreasure();
var idx = treasures.indexOf(t);
if (idx !== -1) {
treasures.splice(idx, 1);
}
@@ -325,49 +278,43 @@
}, 500);
} else if (self.interactTarget instanceof PowerUp && !self.interactTarget.collected) {
var powerUpToCollect = self.interactTarget;
powerUpToCollect.applyAndRemove();
- // No specific score or immediate level completion for collecting power-ups.
} else if (self.interactTarget instanceof Fountain) {
var fountainToUse = self.interactTarget;
fountainToUse.interact();
- // Sound, message, and health refill are handled within fountain.interact()
} else if (self.interactTarget instanceof Gate) {
- var currentGateInstance = self.interactTarget; // Use the specific gate instance from interactTarget
+ var currentGateInstance = self.interactTarget;
if (monsters.length > 0) {
- // Display message that monsters need to be defeated
var warningText = new Text2('Defeat all monsters\nbefore exiting!', {
size: 60,
fill: 0xFF5555,
stroke: 0x000000,
strokeThickness: 6,
align: 'center'
});
warningText.anchor.set(0.5, 0.5);
- warningText.x = 2048 / 2;
- warningText.y = 2732 / 2;
+ warningText.x = SCREEN_WIDTH_HALF;
+ warningText.y = SCREEN_HEIGHT_HALF;
gameLayer.addChild(warningText);
- // Remove text after a few seconds
LK.setTimeout(function () {
if (warningText.parent) {
warningText.parent.removeChild(warningText);
}
warningText.destroy();
}, 2000);
} else {
- // Player reached the gate and monsters are defeated - complete level
if (gateInteractionInProgress) {
return;
- } // Prevent re-entry
- gateInteractionInProgress = true; // Set flag
+ }
+ gateInteractionInProgress = true;
LK.getSound('collect').play();
- player.score += 50 * player.level;
- // Use currentGateInstance for map coordinates
- if (map[Math.floor(currentGateInstance.mapY)] && map[Math.floor(currentGateInstance.mapY)][Math.floor(currentGateInstance.mapX)]) {
- map[Math.floor(currentGateInstance.mapY)][Math.floor(currentGateInstance.mapX)].removeGate();
+ // 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(); // Destroy the specific instance
- gate = null; // Clear the global gate variable
+ currentGateInstance.destroy();
+ gate = null;
player.level++;
storage.level = player.level;
player.health = Math.min(player.health + 2, player.maxHealth);
updateUI();
@@ -378,18 +325,18 @@
strokeThickness: 7,
align: 'center'
});
levelCompleteText.anchor.set(0.5, 0.5);
- levelCompleteText.x = 2048 / 2;
- levelCompleteText.y = 2732 / 2;
+ 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; // Reset flag after generation
+ gateInteractionInProgress = false;
}, 2000);
}
}
} else if (canAttack) {
@@ -399,12 +346,11 @@
};
self.up = function (x, y, obj) {
self.pressed = false;
if (self.direction === 'attack') {
- // Clear joystick override when attack button is released
joystickOverrideActive = false;
if (!canAttack) {
- buttonSprite.alpha = 0.3; // Show as disabled
+ buttonSprite.alpha = 0.3;
} else {
buttonSprite.alpha = 1;
}
} else {
@@ -427,75 +373,53 @@
self.health = EYEBALL_CURRENT_BASE_HEALTH;
self.baseAttack = EYEBALL_CURRENT_BASE_ATTACK;
self.mapX = 0;
self.mapY = 0;
- self.moveSpeed = 0.25; // Eyeballs move a bit faster than default monsters
- // Properties for attack visuals and game logic compatibility
- self.isAttacking = false; // True if currently in attack animation
- self.attackDuration = 600; // Duration of the attack visual animation in ms
- self.attackCooldown = 2000; // Cooldown for dealing damage (used by checkMonsterCollisions)
- self.lastAttackTime = 0; // Timestamp of last damage dealt (used by checkMonsterCollisions)
- self.canAttack = true; // Whether this monster can deal damage (used by checkMonsterCollisions)
+ 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; // Initial interval: 2 to 3 seconds
- self.soundAudibleDistance = 10.0; // World units for eyeball sound audibility
- // Bobbing properties
- self.baseWorldYOffset = 0.4; // Base height above ground (world units)
- // currentBobbingYOffset, bobAmplitude, and bobSpeed removed as bobbing is disabled.
- // For MonsterAI compatibility (canSeePlayer, pathToPlayer might be set by AI)
+ self.soundInterval = 2000 + Math.random() * 1000;
+ self.soundAudibleDistance = 10.0;
+ self.baseWorldYOffset = 0.4;
self.canSeePlayer = false;
self.pathToPlayer = [];
- self.lastMoveTime = 0; // For AI movement frequency or other logic
- // startBobbing method removed as bobbing animation is disabled.
- // This function is called by MonsterAI to trigger the attack *animation*
+ self.lastMoveTime = 0;
self.performAttackAnimation = function () {
if (self.isAttacking || !self.parent) {
- // Already visually attacking or destroyed
return;
}
self.isAttacking = true;
- // tween.stop for currentBobbingYOffset removed as bobbing is disabled.
- // Switch to attack sprite by directly setting alpha
self.attackSprite.alpha = 1;
self.flySprite.alpha = 0;
- // After attack visual duration, revert to fly sprite
LK.setTimeout(function () {
if (!self.parent) {
return;
- } // Check if still exists
- // Revert to fly sprite by directly setting alpha
+ }
self.attackSprite.alpha = 0;
self.flySprite.alpha = 1;
self.isAttacking = false;
- // self.startBobbing() call removed as bobbing is disabled.
}, self.attackDuration);
};
self.takeDamage = function (damageAmount) {
self.health -= damageAmount;
LK.getSound('hit').play();
- // Flash the whole container (both sprites are children)
LK.effects.flashObject(self, 0xff0000, 400);
if (self.health <= 0) {
- // tween.stop for currentBobbingYOffset removed as bobbing is disabled.
- // Ensure attack sprite is hidden if dying mid-attack animation
if (self.attackSprite) {
self.attackSprite.alpha = 0;
}
if (self.flySprite) {
self.flySprite.alpha = 0;
- } // Or make it fade out
- return true; // Monster is killed
+ }
+ return true;
}
- return false; // Monster survived
+ return false;
};
- // This is called by the main game loop's monster iteration.
- // For EyeballMonster, its primary "idle" animation (bobbing) is tween-based and self-managed.
- // The attack animation is triggered by performAttackAnimation.
- // So, this can be empty or used for other per-tick logic if needed.
- self.updateAnimation = function () {
- // Bobbing is handled by tweens started in startBobbing.
- // Attack sprite switching is handled by performAttackAnimation.
- };
+ self.updateAnimation = function () {};
self.updateSoundBehavior = function () {
if (!self.visible) {
return;
}
@@ -514,11 +438,11 @@
volume: volume
});
}
self.lastSoundTime = currentTime;
- self.soundInterval = 2000 + Math.random() * 1000; // Randomize next interval (2-3 seconds)
+ self.soundInterval = 2000 + Math.random() * 1000;
} else {
- self.lastSoundTime = currentTime - Math.random() * self.soundInterval * 0.5; // Stagger if out of range
+ self.lastSoundTime = currentTime - Math.random() * self.soundInterval * 0.5;
}
}
};
return self;
@@ -546,27 +470,23 @@
self.worldY = startY;
self.worldZ = startZ;
var dx = targetX - startX;
var dy = targetY - startY;
- var targetPlayerZ = 0.5; // Assume player's center for aiming
+ 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 {
- // Fallback if target is at origin (e.g. aim forward based on boss facing if available)
- // For now, default to a generic forward if this edge case occurs.
self.dirX = 1;
self.dirY = 0;
self.dirZ = 0;
}
self.active = true;
self.distanceTraveled = 0;
self.visible = true;
- // Rotate sprite to face 2D direction of travel.
- // 3D rotation based on dirZ could be added if sprite is not billboarded.
sprite.rotation = Math.atan2(self.dirY, self.dirX);
};
self.update = function () {
if (!self.active) {
@@ -576,24 +496,23 @@
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); // Fizzle particles
- return true; // Remove
+ createParticleExplosion(self.worldX, self.worldY, 10, 0xFF6347, false);
+ return true;
}
- var mapX = Math.floor(self.worldX);
- var mapY = Math.floor(self.worldY);
+ 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; // Remove
+ return true;
}
if (self.worldZ < 0.1) {
- // Hit floor
createParticleExplosion(self.worldX, self.worldY, 10, 0xFF6347, false);
- return true; // Remove
+ return true;
}
self.updateScreenPosition();
- return false; // Keep active
+ return false;
};
self.updateScreenPosition = function () {
if (!self.visible || !self.active) {
return;
@@ -605,37 +524,33 @@
distToPlayerPlane = 0.1;
}
var angle = Math.atan2(dy, dx) - player.dir;
while (angle < -Math.PI) {
- angle += Math.PI * 2;
+ angle += MATH_PI_2;
}
while (angle > Math.PI) {
- angle -= Math.PI * 2;
+ angle -= MATH_PI_2;
}
if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE) {
- self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2);
- var screenY_horizon = 2732 / 2;
+ 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; // Fallback if height not immediately available
- var visualScaleFactor = 0.6; // Make fireball appear reasonably sized
+ 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);
- // Fireballs are bright, so less distance tinting or a bright base color.
- var baseColor = 0xFFFFAA; // Bright yellow/orange
- var shadeFactor = 1.0; // Full brightness, or apply minimal distance fade if desired
- // Example with slight fade:
- // var distanceRatio = distToPlayerPlane / MAX_RENDER_DISTANCE;
- // shadeFactor = Math.max(0.7, 1 - (distanceRatio / 0.5) * 0.3); // Start fading at 50% distance, down to 70% brightness
+ 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 = Math.floor(r_base * shadeFactor);
- var g = Math.floor(g_base * shadeFactor);
- var b = Math.floor(b_base * shadeFactor);
+ 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;
@@ -644,55 +559,42 @@
return self;
});
var FloorCaster = Container.expand(function () {
var self = Container.call(this);
- // Screen dimensions
var SCREEN_WIDTH = 2048;
var SCREEN_HEIGHT = 2732;
var HORIZON_Y = SCREEN_HEIGHT / 2;
- // Create containers for floor and ceiling strips
var floorContainer = new Container();
var ceilingContainer = new Container();
self.addChild(floorContainer);
self.addChild(ceilingContainer);
- // Store strip pools to avoid creating/destroying objects
var floorStrips = [];
var ceilingStrips = [];
- var stripHeight = 4; // Height of each horizontal strip
+ var stripHeight = 4;
var numStrips = Math.ceil(HORIZON_Y / stripHeight);
- // Initialize floor and ceiling rendering
self.update = function (playerX, playerY, playerDir) {
- // Clear existing strips
floorContainer.removeChildren();
ceilingContainer.removeChildren();
- // Enhanced shading parameters
- var SHADE_START_FACTOR = 0.3; // Start shading even sooner (0.5 instead of 0.7)
- var FLOOR_MIN_SHADE = 0.28; // Slightly darker minimum for floor
- var CEILING_MIN_SHADE = 0.18; // Darker minimum for ceiling
- var WALL_BASE_GRADIENT = 0.20; // Add gradient near walls
- // Render floor (bottom half of screen)
+ 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) {
- // Calculate distance from horizon
var distanceFromHorizon = y - HORIZON_Y;
if (distanceFromHorizon <= 0) {
continue;
}
- // Calculate world distance for perspective
var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon;
- // Skip if too far away
if (worldDistance > MAX_RENDER_DISTANCE) {
continue;
}
- // Calculate enhanced shade based on distance (darker = further)
var distanceRatio = worldDistance / MAX_RENDER_DISTANCE;
- var shadeFactor = Math.max(FLOOR_MIN_SHADE, 1 - distanceRatio / SHADE_START_FACTOR);
- // Wall base gradient - darker near the horizon/walls
+ 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;
}
- // Create or reuse floor strip
var floorStrip;
if (stripIndex < floorStrips.length) {
floorStrip = floorStrips[stripIndex];
} else {
@@ -701,41 +603,32 @@
anchorY: 0
});
floorStrips.push(floorStrip);
}
- // Position and scale the strip
floorStrip.x = 0;
floorStrip.y = y;
floorStrip.width = SCREEN_WIDTH;
floorStrip.height = stripHeight;
floorStrip.alpha = shadeFactor;
- // Add to floor container
floorContainer.addChild(floorStrip);
stripIndex++;
}
- // Render ceiling (top half of screen)
stripIndex = 0;
for (var y = HORIZON_Y - stripHeight; y >= 0; y -= stripHeight) {
- // Calculate distance from horizon
var distanceFromHorizon = HORIZON_Y - y;
if (distanceFromHorizon <= 0) {
continue;
}
- // Calculate world distance for perspective
var worldDistance = WALL_HEIGHT_FACTOR / distanceFromHorizon;
- // Skip if too far away
if (worldDistance > MAX_RENDER_DISTANCE) {
continue;
}
- // Calculate enhanced shade for ceiling (darker than floor)
var distanceRatio = worldDistance / MAX_RENDER_DISTANCE;
- var shadeFactor = Math.max(CEILING_MIN_SHADE, 1 - distanceRatio / SHADE_START_FACTOR);
- // Add subtle gradient near ceiling/walls
+ 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;
}
- // Create or reuse ceiling strip
var ceilingStrip;
if (stripIndex < ceilingStrips.length) {
ceilingStrip = ceilingStrips[stripIndex];
} else {
@@ -744,15 +637,13 @@
anchorY: 0
});
ceilingStrips.push(ceilingStrip);
}
- // Position and scale the strip
ceilingStrip.x = 0;
ceilingStrip.y = y;
ceilingStrip.width = SCREEN_WIDTH;
ceilingStrip.height = stripHeight;
ceilingStrip.alpha = shadeFactor;
- // Add to ceiling container
ceilingContainer.addChild(ceilingStrip);
stripIndex++;
}
};
@@ -765,43 +656,41 @@
anchorY: 0.5
});
self.mapX = 0;
self.mapY = 0;
- self.state = 'full'; // 'full' or 'empty'
+ self.state = 'full';
self.isInteractable = true;
self.particleTimer = 0;
- self.particleInterval = 4; // Spawn particles a bit frequently for a nice effect
+ self.particleInterval = 4;
self.soundAudibleDistance = FOUNTAIN_SOUND_AUDIBLE_DISTANCE;
self.soundPlaybackInterval = FOUNTAIN_SOUND_INTERVAL;
- self.lastSoundPlayTime = 0; // Timestamp of the last time the sound was played
+ self.lastSoundPlayTime = 0;
self.init = function (mapX, mapY) {
self.mapX = mapX;
self.mapY = mapY;
- self.sprite.tint = 0xFFFFFF; // Start with a bright tint
+ 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 + Math.floor(Math.random() * 2); // 1 or 2 particles per emission
+ 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;
- // Assuming the fountain model's "nozzle" is around 1.0 world unit high from its base.
var emissionWorldZ = 1.0;
- // Simulate a world-relative radial splash
- var splashAngle = Math.random() * Math.PI * 2; // Full circle for radial splash
- var horizontalSpeed = Math.random() * 0.0020 + 0.0005; // Small horizontal speed for gentle splash
+ 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; // Gentle upward velocity
- var life = 50 + Math.random() * 40; // Shorter life for fountain particles
- var tintColor = 0x66B2FF; // Light blue, slightly less intense
+ 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);
}
@@ -811,9 +700,9 @@
self.interact = function () {
if (self.state === 'full' && self.isInteractable) {
self.state = 'empty';
self.isInteractable = false;
- self.sprite.tint = 0x707070; // Darken the fountain significantly
+ self.sprite.tint = 0x707070;
player.health = player.maxHealth;
updateUI();
showPowerUpEffectText("Your health has been fully restored!");
LK.getSound('collect').play();
@@ -823,43 +712,38 @@
if (!self.visible || self.state !== 'full') {
return;
}
var currentTime = Date.now();
- // Check if enough time has passed since the last sound play
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);
- // Ensure volume is a valid number
if (isNaN(volume) || volume < 0) {
volume = 0;
}
if (volume > 0) {
LK.getSound('fountainsplash').play({
volume: volume
});
- self.lastSoundPlayTime = currentTime; // Update only when sound is actually played
+ self.lastSoundPlayTime = currentTime;
}
}
}
};
return self;
});
var Gate = Container.expand(function () {
var self = Container.call(this);
- // Create gate visual using existing wall asset but with a different color
var gateSprite = self.attachAsset('staircase', {
anchorX: 0.5,
anchorY: 0.5
});
- // Make the gate distinct with a green tint
gateSprite.tint = 0x00FF00;
self.gateSprite = gateSprite;
self.mapX = 0;
self.mapY = 0;
- // Pulse animation to make gate more visible
var _animateGate = function animateGate() {
tween(gateSprite, {
alpha: 0.7,
scaleX: 1.1,
@@ -877,18 +761,15 @@
});
}
});
};
- // Start the pulsing animation
_animateGate();
return self;
});
var ImpMonster = Container.expand(function () {
var self = Container.call(this);
var frameContainer = new Container();
self.addChild(frameContainer);
- // Scale imp assets to be visually as tall as the Ogre monster (approx 170px effective height)
- // impwalk1 original height: 200px. Scale: 170/200 = 0.85
var impScale = 0.85;
self.sizeMultiplier = 0.7;
var walkFrame1 = LK.getAsset('impwalk1', {
anchorX: 0.5,
@@ -913,34 +794,31 @@
frameContainer.addChild(walkFrame2);
frameContainer.addChild(attackFrameAsset);
walkFrame1.alpha = 1;
walkFrame2.alpha = 0;
- self.currentFrame = 1; // 1 for walkFrame1, 2 for walkFrame2
+ 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; // Ogre default 0.2, Eyeball 0.25. Imp is faster.
+ self.moveSpeed = 0.3;
self.isAggroed = false;
self.lastCryTime = 0;
- self.cryInterval = 3000 + Math.random() * 2000; // Initial interval: 3 to 5 seconds
+ self.cryInterval = 3000 + Math.random() * 2000;
self.lastMoveTime = 0;
self.canSeePlayer = false;
self.pathToPlayer = [];
- // Attack properties
- self.attackCooldown = 1500; // ms (Ogre/Eyeball 2000ms). Imp attacks faster.
+ self.attackCooldown = 1500;
self.lastAttackTime = 0;
- self.canAttack = true; // Cooldown flag for dealing damage
- self.isAttacking = false; // Animation state flag
- self.attackDuration = 500; // ms for the attack visual (Eyeball 600ms)
+ self.canAttack = true;
+ self.isAttacking = false;
+ self.attackDuration = 500;
self.updateAnimation = function () {
if (self.isAttacking) {
- // If currently in attack animation, don't update walk animation
return;
}
self.animationTick++;
- // Change animation frame every 15 ticks (matches Ogre monster animation speed)
if (self.animationTick >= 15) {
self.animationTick = 0;
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
if (self.currentFrame === 1) {
@@ -953,29 +831,24 @@
}
};
self.performAttackAnimation = function () {
if (self.isAttacking || !self.parent) {
- // Already visually attacking or destroyed
return;
}
self.isAttacking = true;
- // Hide walking frames, show attack frame
walkFrame1.alpha = 0;
walkFrame2.alpha = 0;
attackFrameAsset.alpha = 1;
LK.setTimeout(function () {
if (!self.parent) {
- // Check if still exists
return;
}
- attackFrameAsset.alpha = 0; // Hide attack frame
- // Revert to the current walking frame (don't reset walk animation cycle)
+ attackFrameAsset.alpha = 0;
if (self.currentFrame === 1) {
walkFrame1.alpha = 1;
- walkFrame2.alpha = 0; // Ensure other walk frame is hidden
+ walkFrame2.alpha = 0;
} else {
- // currentFrame is 2
- walkFrame1.alpha = 0; // Ensure other walk frame is hidden
+ walkFrame1.alpha = 0;
walkFrame2.alpha = 1;
}
self.isAttacking = false;
}, self.attackDuration);
@@ -992,122 +865,102 @@
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;
- } // Ensure volume is valid
+ }
if (volume > 0) {
- // Changed condition from volume > 0.01
- // Play sound if volume is any positive value
LK.getSound('impCry').play({
volume: volume
});
}
self.lastCryTime = currentTime;
- self.cryInterval = 3000 + Math.random() * 2000; // Randomize next interval (3-5 seconds)
+ self.cryInterval = 3000 + Math.random() * 2000;
} else {
- // If player is out of audible range but imp is still aggroed,
- // reset lastCryTime to avoid immediate cry if player comes back in range.
- // Optionally, also slightly randomize interval to prevent synchronized cries.
- self.lastCryTime = currentTime - Math.random() * self.cryInterval * 0.5; // Stagger it a bit
+ self.lastCryTime = currentTime - Math.random() * self.cryInterval * 0.5;
}
}
};
self.takeDamage = function (damageAmount) {
self.health -= damageAmount;
LK.getSound('hit').play();
- // Flash the frameContainer which holds all visual sprites for the imp
LK.effects.flashObject(frameContainer, 0xff0000, 400);
if (self.health <= 0) {
- // Ensure all sprites are hidden if dying to prevent lingering visuals
walkFrame1.alpha = 0;
walkFrame2.alpha = 0;
if (attackFrameAsset) {
attackFrameAsset.alpha = 0;
}
- return true; // Monster is killed
+ return true;
}
- return false; // Monster survived
+ return false;
};
return self;
});
var JoystickController = Container.expand(function () {
var self = Container.call(this);
- // Create joystick base
var baseRadius = 150;
var baseSprite = self.attachAsset('controlButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 5.0,
scaleY: 5.0,
alpha: 0.4
});
- // Create joystick handle
var handleRadius = 100;
var handleSprite = self.attachAsset('controlButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.0,
scaleY: 3.0
});
- // Initialize variables
self.active = false;
self.startX = 0;
self.startY = 0;
self.maxDistance = baseRadius;
self.normalizedX = 0;
self.normalizedY = 0;
- // Reset the joystick handle position
self.resetHandle = function () {
handleSprite.x = 0;
handleSprite.y = 0;
self.normalizedX = 0;
self.normalizedY = 0;
self.active = false;
};
- // Handle touch down event
self.down = function (x, y, obj) {
self.active = true;
self.startX = x;
self.startY = y;
};
- // Handle touch move event
self.move = function (x, y, obj) {
if (!self.active) {
return;
}
- // Calculate distance from start position
var dx = x - self.startX;
var dy = y - self.startY;
var distance = Math.sqrt(dx * dx + dy * dy);
- // Normalize the distance to get direction vector
if (distance > 0) {
- // Clamp to max distance
if (distance > self.maxDistance) {
dx = dx * self.maxDistance / distance;
dy = dy * self.maxDistance / distance;
distance = self.maxDistance;
}
- // Set handle position
handleSprite.x = dx;
handleSprite.y = dy;
- // Calculate normalized values (-1 to 1)
self.normalizedX = dx / self.maxDistance;
self.normalizedY = dy / self.maxDistance;
} else {
self.resetHandle();
}
};
- // Handle touch up event
self.up = function (x, y, obj) {
self.resetHandle();
};
- // Initialize with handle at center
self.resetHandle();
return self;
});
var MapCell = Container.expand(function () {
var self = Container.call(this);
- self.type = 0; // 0 = floor, 1 = wall
+ self.type = 0;
self.monster = null;
self.treasure = null;
self.gate = null;
self.setType = function (type) {
@@ -1149,9 +1002,8 @@
self.treasure = null;
};
self.addGate = function () {
if (self.type === 0 && !self.monster && !self.treasure && !self.gate) {
- // Gate check is already there, good.
self.gate = true;
return true;
}
return false;
@@ -1162,27 +1014,22 @@
return self;
});
var Monster = Container.expand(function () {
var self = Container.call(this);
- // Create animation frame container
var frameContainer = new Container();
self.addChild(frameContainer);
- // Create monster animation frames
var monsterFrame1 = LK.getAsset('demonOgreWalk1', {
anchorX: 0.5,
anchorY: 0.5
});
var monsterFrame2 = LK.getAsset('demonOgreWalk2', {
anchorX: 0.5,
anchorY: 0.5
});
- // Add frames to container
frameContainer.addChild(monsterFrame1);
frameContainer.addChild(monsterFrame2);
- // Set initial visibility
monsterFrame1.alpha = 1;
monsterFrame2.alpha = 0;
- // Initialize animation state
self.currentFrame = 1;
self.animationTick = 0;
self.mapX = 0;
self.mapY = 0;
@@ -1190,14 +1037,14 @@
self.baseAttack = OGRE_CURRENT_BASE_ATTACK;
self.lastMoveTime = 0;
self.canSeePlayer = false;
self.pathToPlayer = [];
- self.attackCooldown = 2000; // 2 seconds between attacks
- self.lastAttackTime = 0; // When monster last attacked
- self.canAttack = true; // Whether monster can attack
+ 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; // Timestamp of the last time the sound was played
+ self.lastSoundTime = 0;
self.updateSoundBehavior = function () {
if (!self.visible || !self.parent) {
return;
}
@@ -1216,24 +1063,19 @@
volume: volume
});
}
self.lastSoundTime = currentTime;
- // Randomize next interval
self.soundPlaybackInterval = OGRE_SOUND_INTERVAL + Math.random() * OGRE_SOUND_INTERVAL_RANDOMNESS;
} else {
- // If out of range, slightly stagger the next check to avoid synchronized sounds if player moves in/out quickly
self.lastSoundTime = currentTime - Math.random() * self.soundPlaybackInterval * 0.5;
}
}
};
- // Update animation frames - will be called from game loop
self.updateAnimation = function () {
self.animationTick++;
- // Change animation frame every 30 ticks (half second at 60fps)
if (self.animationTick >= 15) {
self.animationTick = 0;
self.currentFrame = self.currentFrame === 1 ? 2 : 1;
- // Directly change alpha values without tween
if (self.currentFrame === 1) {
monsterFrame1.alpha = 1;
monsterFrame2.alpha = 0;
} else {
@@ -1244,9 +1086,8 @@
};
self.takeDamage = function (damageAmount) {
self.health -= damageAmount;
LK.getSound('hit').play();
- // Visual feedback for hit - flash the monster red
LK.effects.flashObject(frameContainer, 0xff0000, 400);
return self.health <= 0;
};
return self;
@@ -1256,27 +1097,25 @@
var graphics = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5
});
- // World coordinates and velocities
self.worldX = 0;
self.worldY = 0;
- self.worldZ = 0; // Height above the dungeon floor
- self.vx = 0; // World velocity X
- self.vy = 0; // World velocity Y
- self.vz = 0; // World velocity Z (vertical)
+ self.worldZ = 0;
+ self.vx = 0;
+ self.vy = 0;
+ self.vz = 0;
self.life = 0;
- self.maxLife = 480; // Approx 8 seconds at 60 FPS (Doubled)
+ self.maxLife = 480;
self.bounces = 0;
- self.tintColor = 0xFFFFFF; // Default tint
- // Particle physics constants (world-based)
- var PARTICLE_WORLD_GRAVITY_EFFECT = 0.0035; // Gravity pulling worldZ down (Reduced by half)
- var PARTICLE_Z_DAMPING = 0.6; // Energy retained vertically after floor bounce (Increased for higher bounce)
- var PARTICLE_XY_DAMPING = 0.4; // Energy retained horizontally after wall bounce (Reduced from 0.6)
- var PARTICLE_GROUND_FRICTION = 0.8; // Friction for vx, vy when hitting floor (Reduced from 0.9)
+ 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; // Visual size (in world units) when 1 unit away
- var MAX_RENDER_DISTANCE_PARTICLES = 12; // Max distance to render particles
+ 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;
@@ -1284,151 +1123,128 @@
self.vy = velY;
self.vz = velZ;
self.life = particleLife || self.maxLife;
self.bounces = 0;
- self.collisionImmunity = 0; // No immunity by default
- self.isChargingParticle = false; // Reset charging flag
- self.tintColor = tintColor || 0xFFFFFF; // Store and apply tint
+ self.collisionImmunity = 0;
+ self.isChargingParticle = false;
+ self.tintColor = tintColor || 0xFFFFFF;
graphics.tint = self.tintColor;
graphics.alpha = 1;
- // Initial scale will be set in update based on distance
- self.visible = false; // Will be set true if in FOV during update
+ self.visible = false;
};
self.update = function () {
if (self.life <= 0 || self.bounces >= PARTICLE_MAX_BOUNCES) {
self.visible = false;
- return false; // Indicate inactive
+ return false;
}
var prevWorldX, prevWorldY;
- // Special handling for charging particles - no gravity
if (self.isChargingParticle) {
- // Store pre-move position for collision response (if needed by subsequent common code)
prevWorldX = self.worldX;
prevWorldY = self.worldY;
- // Apply world velocities (no gravity for charging particles)
self.worldX += self.vx;
self.worldY += self.vy;
self.worldZ += self.vz;
- // Skip wall collision for charging particles (they have immunity)
- // Floor bounce is also implicitly skipped.
} else {
- // Regular particle physics (existing code)
- // Store pre-move position for collision response
prevWorldX = self.worldX;
prevWorldY = self.worldY;
- // Apply world velocities
self.worldX += self.vx;
self.worldY += self.vy;
self.worldZ += self.vz;
- // Apply gravity to vertical velocity
self.vz -= PARTICLE_WORLD_GRAVITY_EFFECT;
- // Floor bounce logic (worldZ = 0 is the floor)
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)) {
- // Come to rest
self.vz = 0;
}
}
- // Wall bounce logic - skip if particle has collision immunity
if (self.collisionImmunity > 0) {
self.collisionImmunity--;
} else {
- var currentMapX = Math.floor(self.worldX);
- var currentMapY = Math.floor(self.worldY);
+ 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; // Revert to position before entering wall
+ self.worldX = prevWorldX;
self.worldY = prevWorldY;
- var pt_map_prev_x = Math.floor(prevWorldX);
- var pt_map_prev_y = Math.floor(prevWorldY);
+ var pt_map_prev_x = fastFloor(prevWorldX);
+ var pt_map_prev_y = fastFloor(prevWorldY);
var reflectedX = false;
var reflectedY = false;
- // Check if collision was primarily due to X movement into a wall
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;
}
- // Check if collision was primarily due to Y movement into a wall
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 hit a corner or exact cause is ambiguous (e.g. started in wall), reflect based on dominant velocity or both
if (!reflectedX && !reflectedY) {
- // This case implies it was already in a wall or hit a corner perfectly.
- // A simple heuristic: reflect the larger velocity component, or both if similar.
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 {
- // Similar magnitude or started in wall, reflect both
self.vx *= -PARTICLE_XY_DAMPING;
self.vy *= -PARTICLE_XY_DAMPING;
}
}
self.bounces++;
}
}
}
- // Calculate screen position and scale (common for both particle types)
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;
- } // Avoid division by zero / extreme scales
+ }
var angle = Math.atan2(dy, dx) - player.dir;
while (angle < -Math.PI) {
- angle += Math.PI * 2;
+ angle += MATH_PI_2;
}
while (angle > Math.PI) {
- angle -= Math.PI * 2;
+ angle -= MATH_PI_2;
}
if (Math.abs(angle) < HALF_FOV && distToPlayerPlane < MAX_RENDER_DISTANCE_PARTICLES) {
self.visible = true;
- // Screen X
- self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2);
- // Screen Y: based on horizon, distance, and particle's worldZ
- var screenY_horizon = 2732 / 2;
+ 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;
- // Scale based on distance and particle's inherent visual size
- var particleAssetBaseSize = 100.0; // Updated for 'particle' asset (100x100)
+ 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; // Fade out but maintain some visibility
+ 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; // Indicate inactive
+ return false;
}
- return true; // Indicate active
+ return true;
};
return self;
});
var PowerUp = Container.expand(function (type) {
var self = Container.call(this);
- self.powerUpType = type; // 'speed', 'defense', 'health', 'attack'
- self.spawnScaleMultiplier = 1.0; // Used for spawn animation, defaults to full size
+ self.powerUpType = type;
+ self.spawnScaleMultiplier = 1.0;
self.mapX = 0;
self.mapY = 0;
self.collected = false;
- self.verticalOffset = 0; // World units for lift from chest
- self.currentBobOffset = 0; // Screen pixels for bobbing motion
- self.hasReachedBobHeight = false; // Flag to start bobbing
+ self.verticalOffset = 0;
+ self.currentBobOffset = 0;
+ self.hasReachedBobHeight = false;
var assetId = '';
switch (self.powerUpType) {
case 'speed':
assetId = 'speedup';
@@ -1442,27 +1258,26 @@
case 'attack':
assetId = 'attackup';
break;
default:
- console.error("Unknown power-up type: " + self.powerUpType);
- assetId = 'treasure'; // Fallback, should not happen
+ assetId = 'treasure';
break;
}
var powerUpSprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
- self.powerUpSprite = powerUpSprite; // Expose the sprite
+ self.powerUpSprite = powerUpSprite;
self.startBobbingAnimation = function () {
if (self.collected || !self.parent || !self.hasReachedBobHeight) {
return;
}
- var bobAmplitude = 15; // Screen pixels for bobbing up/down
- var bobSpeed = 1800; // Milliseconds for a full bob cycle ( थोड़ा धीमा )
+ var bobAmplitude = 15;
+ var bobSpeed = 1800;
function animateUpInternal() {
if (self.collected || !self.parent) {
return;
- } // Check again before starting tween
+ }
tween(self, {
currentBobOffset: -bobAmplitude
}, {
duration: bobSpeed / 2,
@@ -1477,13 +1292,12 @@
}
function animateDownInternal() {
if (self.collected || !self.parent) {
return;
- } // Check again
+ }
tween(self, {
currentBobOffset: 0
}, {
- // Bob back to its lifted base
duration: bobSpeed / 2,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (self.collected || !self.parent) {
@@ -1492,61 +1306,56 @@
animateUpInternal();
}
});
}
- animateUpInternal(); // Start the bobbing cycle
+ animateUpInternal();
};
- // Note: _animateBob() call removed, startBobbingAnimation will be triggered after lift.
self.applyAndRemove = function () {
if (self.collected) {
return;
}
self.collected = true;
var effectMessage = "";
- // Apply power-up effect (now permanent)
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; // Smaller, incremental speed boost
+ player.moveSpeedMultiplier += 0.15;
effectMessage = "Movement Speed Increased!";
break;
case 'defense':
- player.defense += 1; // Increment numerical defense stat
- effectMessage = "Defense Stat Increased!"; // Updated message
+ player.defense += 1;
+ effectMessage = "Defense Stat Increased!";
break;
case 'attack':
- player.attackPower += 1; // Increase attack power
- // player.attackBuffActive is no longer set by this power-up, as it affects power not speed/cooldown.
+ player.attackPower += 1;
effectMessage = "Attack Power Increased!";
break;
}
if (effectMessage) {
showPowerUpEffectText(effectMessage);
}
- LK.getSound('powerup').play(); // Play powerup sound effect
- // Visual feedback for collection (quick scale out and fade)
+ 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,
- // Make it disappear inwards
onFinish: function onFinish() {
if (self.parent) {
self.parent.removeChild(self);
}
var index = powerUps.indexOf(self);
if (index !== -1) {
powerUps.splice(index, 1);
}
- self.destroy(); // Explicitly destroy after removal
+ self.destroy();
}
});
};
return self;
@@ -1556,112 +1365,81 @@
var projectileSprite = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
- // World position and movement properties
self.worldX = 0;
self.worldY = 0;
self.dirX = 0;
self.dirY = 0;
- self.speed = 0.0375; // World units per tick (reduced to 1/4 of original)
+ self.speed = 0.0375;
self.active = false;
self.distance = 0;
- self.maxDistance = 10; // Maximum travel distance in world units
- // Initialize projectile
+ self.maxDistance = 10;
self.fire = function (screenX, screenY) {
- // Start at player position, slightly offset in the firing direction
self.worldX = player.x + Math.cos(player.dir) * 0.3;
self.worldY = player.y + Math.sin(player.dir) * 0.3;
- // Direction is player's current facing direction
self.dirX = Math.cos(player.dir);
self.dirY = Math.sin(player.dir);
self.active = true;
self.distance = 0;
- self.hitMonsters = []; // Initialize list to track hit monsters
- self.wallHitSoundPlayed = false; // Flag to ensure wall hit sound plays only once
- // Play attack sound
+ self.hitMonsters = [];
+ self.wallHitSoundPlayed = false;
LK.getSound('attack').play();
- // Scale for visual effect
projectileSprite.scale.set(0.2, 0.2);
- // Store initial screen coordinates from hand
- self.initialScreenX = screenX || 2048 / 2;
+ self.initialScreenX = screenX || SCREEN_WIDTH_HALF;
self.initialScreenY = screenY || 2732 - 300;
- // Set initial position to hand location
self.x = self.initialScreenX;
self.y = self.initialScreenY;
self.visible = true;
};
- // Update projectile position and check for collisions
self.update = function (deltaTime) {
if (!self.active) {
return false;
}
- // Move projectile in world space
self.worldX += self.dirX * self.speed;
self.worldY += self.dirY * self.speed;
- // Track distance traveled
self.distance += self.speed;
- // Check for wall collision
- var mapX = Math.floor(self.worldX);
- var mapY = Math.floor(self.worldY);
+ 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) {
- // Projectile went out of bounds
shouldRemove = true;
- // We could optionally treat out-of-bounds as a wall hit for particles,
- // but typically particles are for hitting actual geometry.
} else if (map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 1) {
- // Projectile hit an actual map wall
shouldRemove = true;
hitActualMapWall = true;
}
if (shouldRemove) {
if (hitActualMapWall) {
- // Spawn particle feedback at the point of impact
- createParticleExplosion(self.worldX, self.worldY, PROJECTILE_WALL_HIT_PARTICLE_COUNT, 0x87CEFA, true); // Light Blue Tint, isWallHit = true
- // Play wall hit sound effect only once
+ createParticleExplosion(self.worldX, self.worldY, PROJECTILE_WALL_HIT_PARTICLE_COUNT, 0x87CEFA, true);
if (!self.wallHitSoundPlayed) {
LK.getSound('wallhit').play();
self.wallHitSoundPlayed = true;
}
}
- return true; // Remove projectile
+ return true;
}
- // Position on screen based on player view (raycasting principles)
self.updateScreenPosition();
- return false; // Keep projectile
+ return false;
};
- // Calculate screen position from world position
self.updateScreenPosition = function () {
- // Vector from player to projectile
var dx = self.worldX - player.x;
var dy = self.worldY - player.y;
- // Distance from player to projectile
var dist = Math.sqrt(dx * dx + dy * dy);
- // Angle from player to projectile
var angle = Math.atan2(dy, dx) - player.dir;
- // Normalize angle (-PI to PI)
while (angle < -Math.PI) {
- angle += Math.PI * 2;
+ angle += MATH_PI_2;
}
while (angle > Math.PI) {
- angle -= Math.PI * 2;
+ angle -= MATH_PI_2;
}
- // Check if projectile is in field of view
if (Math.abs(angle) < HALF_FOV) {
- // Calculate screen X based on angle in FOV
- self.x = 2048 / 2 + angle / HALF_FOV * (2048 / 2);
- // Calculate target Y position (center of screen with slight adjustment)
- var targetY = 2732 / 2 + 40 - WALL_HEIGHT_FACTOR / dist * 0.1; // Adjusted +20 to +40 to lower target height
- // Calculate transition factor based on distance
- // As distance increases, move closer to target Y
+ 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);
- // Interpolate between initial hand Y and target Y
self.y = self.initialScreenY * (1 - transitionFactor) + targetY * transitionFactor;
- // Scale based on distance
var scale = Math.max(0.1, 2 / dist);
projectileSprite.scale.set(scale, scale);
self.visible = true;
} else {
@@ -1671,64 +1449,49 @@
return self;
});
var RaycastStrip = Container.expand(function () {
var self = Container.call(this);
- var activeWallSprite = null; // The currently displayed wall sprite instance
- var currentTileIndex = null; // Stores the numeric index of activeWallSprite, e.g., 5
+ var activeWallSprite = null;
+ var currentTileIndex = null;
self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance, textureX, side, tileIndex, wallMapX, wallMapY) {
- // self.x is now set in rayCasting for correct centering; do not set here
- // Validate tileIndex (already a number)
if (tileIndex < 1 || tileIndex > 64 || !Number.isInteger(tileIndex)) {
- console.error("RaycastStrip: Invalid tileIndex received: " + tileIndex + ". Clearing strip.");
- self.clearStrip(); // Clear the strip if tileIndex is invalid
+ self.clearStrip();
return;
}
- // Use direct lookup instead of string concatenation
var newTileAssetName = wallTextureLookup[tileIndex];
- // If the current sprite is active but not the correct type for the new tileIndex, release it.
- // Note: We still use currentTileIndex (numeric) for the pool key, but use newTileAssetName for LK.getAsset if needed.
if (activeWallSprite && currentTileIndex !== tileIndex) {
- // Compare numeric indices
- wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex); // Pass numeric index
- self.removeChild(activeWallSprite); // Detach from this strip's container
+ wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex);
+ self.removeChild(activeWallSprite);
activeWallSprite = null;
- currentTileIndex = null; // Clear current numeric index
+ currentTileIndex = null;
}
- // If no sprite is active (either first time, or after releasing/clearing), get one from the pool.
if (!activeWallSprite) {
- // wallSpritePoolManager.getSprite now expects the numeric tileIndex and internally uses the lookup for asset name if creating new.
- activeWallSprite = wallSpritePoolManager.getSprite(tileIndex); // Pass numeric index
+ activeWallSprite = wallSpritePoolManager.getSprite(tileIndex);
if (activeWallSprite) {
- self.addChild(activeWallSprite); // Add the new/reused sprite to this strip's container
- currentTileIndex = tileIndex; // Store numeric index
+ self.addChild(activeWallSprite);
+ currentTileIndex = tileIndex;
} else {
- // This can happen if wallSpritePoolManager.getSprite returns null
- console.error("RaycastStrip: Failed to get sprite for tileIndex " + tileIndex + " from pool using asset name: " + newTileAssetName);
- self.clearStrip(); // Ensure strip is clean if sprite acquisition fails
- return; // Cannot render this strip without a sprite
+ self.clearStrip();
+ return;
}
}
- // Configure the active sprite's properties (position, size, appearance)
activeWallSprite.width = stripWidth;
activeWallSprite.height = wallHeight;
- // Position sprite relative to the RaycastStrip container's origin
activeWallSprite.x = 0;
- activeWallSprite.y = (2732 - wallHeight) / 2; // Vertically center the wall segment
- // Apply distance-based tinting
- var SHADE_START_FACTOR_WALLS = 0.3; // Match floor/ceiling behavior
- var MIN_SHADE_WALLS = 0.2; // General minimum shade for distant walls
+ 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);
- // --- Apply projectile light ---
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; // Use squared distance for check
+ 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);
@@ -1736,48 +1499,40 @@
}
}
}
shadeFactor = Math.min(1.0, shadeFactor + totalProjectileLightInfluence);
- // --- End projectile light ---
if (side === 1) {
- // Conventionally, side 1 walls (e.g., horizontal, further from light) are darker
- shadeFactor *= 0.7; // Apply side shadow multiplicatively
- // Ensure side shadow doesn't make it darker than a very low bound
- shadeFactor = Math.max(MIN_SHADE_WALLS * 0.5, shadeFactor); // e.g. max(0.1, shadeFactor)
+ shadeFactor *= 0.7;
+ shadeFactor = Math.max(MIN_SHADE_WALLS * 0.5, shadeFactor);
}
- // Calculate tint based on shadeFactor
- var baseWallTint = 0xFFFFFF; // Assuming walls are not pre-tinted by default in assets
+ var baseWallTint = 0xFFFFFF;
var r_wall = baseWallTint >> 16 & 0xFF;
var g_wall = baseWallTint >> 8 & 0xFF;
var b_wall = baseWallTint & 0xFF;
- r_wall = Math.floor(r_wall * shadeFactor);
- g_wall = Math.floor(g_wall * shadeFactor);
- b_wall = Math.floor(b_wall * shadeFactor);
+ 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; // Use tint for darkness, keep alpha full for wall segments
- activeWallSprite.visible = true; // Ensure the sprite is visible
+ activeWallSprite.alpha = 1.0;
+ activeWallSprite.visible = true;
};
- // Clears the strip by releasing its active sprite back to the pool.
self.clearStrip = function () {
if (activeWallSprite && typeof currentTileIndex === 'number') {
- // Check if currentTileIndex is valid
- wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex); // Pass numeric index
- self.removeChild(activeWallSprite); // Detach from container
+ wallSpritePoolManager.releaseSprite(activeWallSprite, currentTileIndex);
+ self.removeChild(activeWallSprite);
activeWallSprite = null;
currentTileIndex = null;
}
};
return self;
});
var Treasure = Container.expand(function () {
var self = Container.call(this);
- // Closed treasure sprite
var treasureSprite = self.attachAsset('treasure', {
anchorX: 0.5,
anchorY: 0.5
});
- treasureSprite.tint = 0xdddddd; // Light grey tint
- // Open treasure sprite (initially hidden)
+ treasureSprite.tint = 0xdddddd;
var treasureOpenSprite = self.attachAsset('treasureopen', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
@@ -1807,21 +1562,160 @@
/****
* Game Code
****/
-var gameState = 'title'; // 'title' or 'game'
+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;
-// Base stats for monsters - these will be incremented for difficulty scaling
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;
-// FIREBALL_DAMAGE (defined below) will serve as the boss's current attack power.
-// At start of game, create a texture lookup table
var wallTextureLookup = new Array(65);
for (var i = 1; i <= 64; i++) {
wallTextureLookup[i] = 'walltile' + i;
}
@@ -1829,13 +1723,11 @@
pools: function () {
var p = {};
var PREALLOCATE_PER_TILE = 48;
if (graphicsQuality === 'Low') {
- // Low quality mode: only create pools for lowqualitywall1 and lowqualitywall2
- // Map them to indices 1 and 2 for simplicity
var lowQualityAssets = ['lowqualitywall1', 'lowqualitywall2'];
for (var i = 0; i < lowQualityAssets.length; i++) {
- var poolIndex = i + 1; // Use indices 1 and 2
+ var poolIndex = i + 1;
var assetName = lowQualityAssets[i];
var tilePool = [];
p[poolIndex] = tilePool;
for (var j = 0; j < PREALLOCATE_PER_TILE; j++) {
@@ -1847,13 +1739,11 @@
tilePool.push(sprite);
}
}
} else {
- // High quality mode: create pools for all 64 wall tiles
for (var i = 1; i <= 64; i++) {
var assetNameForLK = wallTextureLookup[i];
if (!assetNameForLK) {
- console.error("WallSpritePoolManager (init pools): No asset name found in lookup for tileIndex: " + i);
continue;
}
var tilePool = [];
p[i] = tilePool;
@@ -1872,9 +1762,8 @@
getSprite: function getSprite(tileIndex) {
var actualIndex = tileIndex;
var assetNameForLK;
if (graphicsQuality === 'Low') {
- // Map all tile indices to either lowqualitywall1 (1) or lowqualitywall2 (2)
actualIndex = (tileIndex - 1) % 2 + 1;
assetNameForLK = actualIndex === 1 ? 'lowqualitywall1' : 'lowqualitywall2';
} else {
assetNameForLK = wallTextureLookup[tileIndex];
@@ -1882,11 +1771,9 @@
if (!this.pools[actualIndex]) {
var isValidTile = Number.isInteger(actualIndex) && actualIndex >= 1;
if (isValidTile) {
this.pools[actualIndex] = [];
- console.warn("WallSpritePoolManager: Created pool on-the-fly for tile index: " + actualIndex);
} else {
- console.error("WallSpritePoolManager: Attempted to get sprite for invalid tile index: " + actualIndex);
return null;
}
}
var pool = this.pools[actualIndex];
@@ -1894,9 +1781,8 @@
var sprite = pool.pop();
return sprite;
} else {
if (!assetNameForLK) {
- console.error("WallSpritePoolManager: No asset name found for tileIndex: " + tileIndex);
return null;
}
var newSprite = LK.getAsset(assetNameForLK, {
anchorX: 0,
@@ -1908,22 +1794,18 @@
},
releaseSprite: function releaseSprite(sprite, tileIndex) {
var actualIndex = tileIndex;
if (graphicsQuality === 'Low') {
- // Map to the actual pool index used in low quality mode
actualIndex = (tileIndex - 1) % 2 + 1;
}
if (!sprite || typeof actualIndex !== 'number') {
- console.error("WallSpritePoolManager: Invalid sprite or tileIndex for release. tileIndex: " + actualIndex);
return;
}
if (!this.pools[actualIndex]) {
var isValidTile = Number.isInteger(actualIndex) && actualIndex >= 1;
if (isValidTile) {
this.pools[actualIndex] = [];
- console.warn("WallSpritePoolManager: Releasing sprite to a newly created pool for tile index: " + actualIndex);
} else {
- console.error("WallSpritePoolManager: Attempted to release sprite for invalid tile index: " + actualIndex);
return;
}
}
sprite.visible = false;
@@ -1935,100 +1817,90 @@
sprite.tint = 0xFFFFFF;
this.pools[actualIndex].push(sprite);
}
};
-// Game constants
-var PROJECTILE_LIGHT_RADIUS = 5.0; // World units for how far projectile light reaches
-var PROJECTILE_LIGHT_MAX_CONTRIBUTION = 0.6; // Max brightness projectile can add (0 to 1)
-var PROJECTILE_LIGHT_FALLOFF_EXPONENT = 2.0; // Power for light falloff (e.g., 2 for quadratic)
+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; // Number of particles for projectile wall impact
+var PROJECTILE_WALL_HIT_PARTICLE_COUNT = 8;
var particlePoolManager;
-var particleExplosionContainer; // Container for all active particles from explosions
+var particleExplosionContainer;
var CELL_SIZE = 20;
var MINI_MAP_SCALE = 1;
-var STRIP_WIDTH = graphicsQuality === 'Low' ? 12 : 5; // Half the rays in low quality
-var NUM_RAYS = Math.ceil(2048 / STRIP_WIDTH);
-// Pre-allocate ray result objects to avoid garbage collection
-var rayResults = new Array(NUM_RAYS);
-for (var i = 0; i < NUM_RAYS; i++) {
- rayResults[i] = {
- wallHit: false,
- distance: 0,
- // perpWallDist from DDA
- actualDistance: 0,
- // distance corrected for FOV, used for shading and strip height
- side: 0,
- wallType: 0,
- mapX: 0,
- // from DDA
- mapY: 0,
- // from DDA
- screenX: 0,
- // strip's screen X position
- wallHeight: 0,
- // calculated wall height
- textureIndex: 0 // calculated texture/tile index
- };
+function getStripWidth() {
+ return graphicsQuality === 'Low' ? 15 : 5;
}
-var FOV = Math.PI / 3; // 60 degrees field of view
-// Variables for raycasting optimization
+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; // Ensure first frame does a full raycast
+var shouldRaycast = true;
var HALF_FOV = FOV / 2;
-var PLAYER_MOVE_SPEED = 0.002; // Reduced from 0.005 to make movement slower
-var PLAYER_TURN_SPEED = 0.002; // Reduced from 0.005 to make turning slower
+var 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; // Max distance at which monsters will notice and start chasing the player.
-var MONSTER_CLOSE_PROXIMITY_RANGE = 4.0; // Max distance for non-LOS chase if within AGGRO_RANGE.
-var MIN_MONSTER_PLAYER_DISTANCE = 0.6; // Monsters stop if they get closer than this (e.g., overshoot); this is within attack range.
+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; // Minimum distance from player center to wall cell's effective edge
-var MONSTER_WALL_BUFFER = 0.25; // Minimum distance from monster center to wall cell's effective edge
-var MONSTER_COUNT = 8; // Increased by 50% from 5, rounded up
+var WALL_BUFFER = 0.18;
+var MONSTER_WALL_BUFFER = 0.25;
+var MONSTER_COUNT = 8;
var TREASURE_COUNT = 10;
-var BOSS_FIREBALL_COOLDOWN = 3000; // milliseconds
-var FIREBALL_SPEED = 0.04; // world units per tick
+var BOSS_FIREBALL_COOLDOWN = 3000;
+var FIREBALL_SPEED = 0.04;
var FIREBALL_DAMAGE = 2;
-var FIREBALL_MAX_DISTANCE = 12; // world units
-var BOSS_PREFERRED_DISTANCE = 6.0; // Preferred distance boss tries to keep from player
-var BOSS_TOO_CLOSE_DISTANCE = 3.5; // If player is closer than this, boss backs up
-var BOSS_SHOOT_RANGE_MAX = 10.0; // Max range for shooting
-var BOSS_SHOOT_RANGE_MIN = 2.0; // Min range for shooting (don't shoot if player is too close)
-var MAX_AUDIBLE_DISTANCE_IMP_CRY = 20.0; // World units for imp cry audibility (Increased from 12.0)
-var FOUNTAIN_SOUND_AUDIBLE_DISTANCE = 6.0; // How close player needs to be to hear fountain
-var FOUNTAIN_SOUND_INTERVAL = 2500; // Milliseconds between fountain sound plays
-var OGRE_SOUND_AUDIBLE_DISTANCE = 10.0; // How close player needs to be to hear the ogre
-var OGRE_SOUND_INTERVAL = 3000; // Milliseconds between ogre sound plays (base)
-var OGRE_SOUND_INTERVAL_RANDOMNESS = 1500; // Random additional ms for ogre sound interval
-// Game state
+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,
- // Current health
maxHealth: 10,
- // Maximum health, starts at 5
score: 0,
level: 1,
- // TEMPORARY: Start at level 5 for boss testing
- // Permanent Stats Enhanced by Power-ups
moveSpeedMultiplier: 1.0,
- // Base is 1.0, increases with speed power-ups
defense: 0,
- // Numerical defense stat, starts at 0
- attackPower: 1 // Base attack power is 1, increases with attack power-ups
- // Buff timeouts are removed as effects are permanent
+ attackPower: 1
};
var powerUps = [];
-var POWERUP_COUNT = 3; // Number of power-ups to spawn per level
-// POWERUP_DURATION is no longer needed as buffs are permanent until new game
+var POWERUP_COUNT = 3;
var controls = {
forward: false,
backward: false,
left: false,
@@ -2044,77 +1916,88 @@
var wallSegments = [];
var lastWallCheck = [];
var lastTime = Date.now();
var canAttack = true;
-var attackCooldown = 500; // 500 millisecond cooldown
-// Joystick override state
+var attackCooldown = 500;
var joystickOverrideActive = false;
var joystickOverrideX = 0;
var joystickOverrideY = 0;
-var gateInteractionInProgress = false; // Flag to prevent double level advancement via gate
-// UI elements
+var gateInteractionInProgress = false;
var miniMap;
var rayCastView;
var healthText;
-var scoreText;
+var attackText;
+var defenseText;
+var speedText;
var levelText;
var monsterText;
var controlButtons = {};
var playerMarker;
-var globalRightHand; // Stores the right hand game object
-var activeControlForGlobalHandlers = null; // Tracks which control is active for game.down/up/move
-var renderableObjects = []; // Array to hold all objects (walls, entities) to be sorted by depth
+var globalRightHand;
+var activeControlForGlobalHandlers = null;
+var renderableObjects = [];
var graphicsQuality = 'High';
-// Interaction Prompt UI Elements
var interactionPromptContainer;
var interactionPromptMainText;
-var interactionPromptParentheticalGroup; // Container for "(Press [img] )"
+var interactionPromptParentheticalGroup;
var interactionPromptPressOpenText;
var interactionPromptButtonImage;
var interactionPromptPressCloseText;
-// Create layer variables at the global scope
var gameLayer = new Container();
var projectileLayer = new Container();
var handLayer = new Container();
particlePoolManager = {
pool: [],
activeParticles: [],
maxParticles: 750,
- // Increased from 300 to support more particles
- // Increased max particles
+ 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 {
+ } else if (this.activeParticles.length < this.maxParticles) {
particle = new Particle();
+ } else {
+ particle = this.activeParticles.shift();
+ if (particle.parent) {
+ particle.parent.removeChild(particle);
+ }
}
- this.activeParticles.push(particle);
- // The particle will be added to particleExplosionContainer by createParticleExplosion
+ if (particle) {
+ this.activeParticles.push(particle);
+ }
return particle;
},
releaseParticle: function releaseParticle(particle) {
- // Remove from activeParticles
var index = this.activeParticles.indexOf(particle);
if (index !== -1) {
this.activeParticles.splice(index, 1);
}
- // Remove from its parent container (particleExplosionContainer)
if (particle.parent) {
particle.parent.removeChild(particle);
}
- if (this.pool.length < this.maxParticles) {
+ 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);
- } else {
- // If pool is full, LK will garbage collect if it has no parent.
- // Explicitly calling destroy isn't standard in LK for pooled objects unless necessary.
}
},
updateActiveParticles: function updateActiveParticles() {
for (var i = this.activeParticles.length - 1; i >= 0; i--) {
var particle = this.activeParticles[i];
if (!particle.update()) {
- // Particle.update returns false when life is over
this.releaseParticle(particle);
}
}
}
@@ -2127,25 +2010,22 @@
var finalExplosionX = worldExplosionX;
var finalExplosionY = worldExplosionY;
var wallNormalX = 0;
var wallNormalY = 0;
- var applyWallBias = isWallHit === true; // Explicitly check for true
+ var applyWallBias = isWallHit === true;
if (applyWallBias) {
- // Determine the wall normal direction to offset the particles away from the wall
- var cellX = Math.floor(worldExplosionX);
- var cellY = Math.floor(worldExplosionY);
- // Check adjacent cells to determine wall direction
+ var cellX = fastFloor(worldExplosionX);
+ var cellY = fastFloor(worldExplosionY);
if (cellX + 1 < MAP_SIZE && map[cellY][cellX + 1].type === 1) {
- wallNormalX = -1; // Wall is to the right, push left
+ wallNormalX = -1;
} else if (cellX - 1 >= 0 && map[cellY][cellX - 1].type === 1) {
- wallNormalX = 1; // Wall is to the left, push right
+ wallNormalX = 1;
}
if (cellY + 1 < MAP_SIZE && map[cellY + 1][cellX].type === 1) {
- wallNormalY = -1; // Wall is below, push up
+ wallNormalY = -1;
} else if (cellY - 1 >= 0 && map[cellY - 1][cellX].type === 1) {
- wallNormalY = 1; // Wall is above, push down
+ wallNormalY = 1;
}
- // If we're at a corner or couldn't determine direction, use distance from cell center
if (wallNormalX === 0 && wallNormalY === 0) {
wallNormalX = worldExplosionX - (cellX + 0.5);
wallNormalY = worldExplosionY - (cellY + 0.5);
var normalLength = Math.sqrt(wallNormalX * wallNormalX + wallNormalY * wallNormalY);
@@ -2159,13 +2039,13 @@
}
var offsetDistance = 0.1;
finalExplosionX = worldExplosionX + wallNormalX * offsetDistance;
finalExplosionY = worldExplosionY + wallNormalY * offsetDistance;
- } // else, finalExplosionX/Y remain worldExplosionX/Y
- var worldExplosionZ = 0.5; // Assume explosion originates roughly half a unit above the floor
+ }
+ var worldExplosionZ = 0.5;
for (var i = 0; i < count; i++) {
var particle = particlePoolManager.getParticle();
- var phi = Math.random() * Math.PI * 2;
+ 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;
@@ -2175,23 +2055,21 @@
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; // Only give immunity for wall hits
+ particle.collisionImmunity = applyWallBias ? 10 : 0;
particleExplosionContainer.addChild(particle);
}
}
function reinitializeWallSpritePoolManager() {
- // Clear existing pools
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();
}
}
}
- // Reinitialize pools based on current graphics quality
wallSpritePoolManager.pools = function () {
var p = {};
var PREALLOCATE_PER_TILE = 48;
if (graphicsQuality === 'Low') {
@@ -2213,9 +2091,8 @@
} else {
for (var i = 1; i <= 64; i++) {
var assetNameForLK = wallTextureLookup[i];
if (!assetNameForLK) {
- console.error("WallSpritePoolManager (reinit): No asset name found in lookup for tileIndex: " + i);
continue;
}
var tilePool = [];
p[i] = tilePool;
@@ -2242,21 +2119,21 @@
var bgAsset = titleScreenContainer.attachAsset('titlebackground', {
anchorX: 0.5,
anchorY: 0.5
});
- bgAsset.x = 2048 / 2;
- bgAsset.y = 2732 / 2;
+ bgAsset.x = SCREEN_WIDTH_HALF;
+ bgAsset.y = SCREEN_HEIGHT_HALF;
var logoAsset = titleScreenContainer.attachAsset('logo', {
anchorX: 0.5,
anchorY: 0.5
});
- logoAsset.x = 2048 / 2;
- logoAsset.y = 2732 / 2 - logoAsset.height / 2 + 300;
+ 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 = 2048 / 2;
+ startButtonAsset.x = SCREEN_WIDTH_HALF;
startButtonAsset.y = logoAsset.y + logoAsset.height / 2 + startButtonAsset.height / 2 + 150;
startButtonAsset.down = function () {
gameState = 'game';
if (titleScreenContainer) {
@@ -2265,92 +2142,75 @@
titleScreenContainer = null;
}
initializeGamePlayElements();
};
- // Graphics Quality Selector
- var qualityText = new Text2('Graphics quality: ' + graphicsQuality, {
+ 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 = 2048 / 2;
- qualityText.y = startButtonAsset.y + startButtonAsset.height / 2 + 200; // Position under start button
+ qualityText.x = SCREEN_WIDTH_HALF;
+ qualityText.y = startButtonAsset.y + startButtonAsset.height / 2 + 200;
qualityText.down = function () {
- // Toggle between High and Low
graphicsQuality = graphicsQuality === 'High' ? 'Low' : 'High';
- qualityText.setText('Graphics quality: ' + graphicsQuality);
- // Reinitialize wall sprite pool manager for the new quality setting
+ qualityText.setText("Graphics quality: ".concat(graphicsQuality));
reinitializeWallSpritePoolManager();
- // Visual feedback
+ if (gameState === 'game') {
+ reinitializeRaycastSystem();
+ }
qualityText.tint = 0xAAFFAA;
LK.setTimeout(function () {
qualityText.tint = 0xFFFFFF;
}, 150);
};
titleScreenContainer.addChild(qualityText);
}
function initializeGamePlayElements() {
- // Set up the layer system using the globally defined variables
- // Add layers in the correct order (projectiles below hand)
game.addChild(gameLayer);
game.addChild(projectileLayer);
game.addChild(handLayer);
- // Create the floor/ceiling caster first (should be below walls)
floorCaster = new FloorCaster();
gameLayer.addChild(floorCaster);
- // Initialize wallSegments array to hold RaycastStrip instances
wallSegments = [];
- // Create raycast strips
- for (var i = 0; i < NUM_RAYS; i++) {
+ initializeRayResults();
+ var numRays = getNumRays();
+ for (var i = 0; i < numRays; i++) {
var strip = new RaycastStrip();
- wallSegments.push(strip); // Store strips in an array, not in a display container yet
+ wallSegments.push(strip);
}
- // Initialize container for depth-sorted dynamic entities (walls and entities)
dynamicEntitiesContainer = new Container();
gameLayer.addChild(dynamicEntitiesContainer);
- // Create minimap container
miniMap = new Container();
- // Move mini map left by 25% of its original centered position
miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2 * 0.40;
- miniMap.y = 40; // Keep at top
+ miniMap.y = 40;
gameLayer.addChild(miniMap);
- // Generate map
generateMap();
- // Create player marker
playerMarker = gameLayer.addChild(LK.getAsset('player', {
anchorX: 0.5,
anchorY: 0.5
}));
- // Create UI elements
createUI();
- // Create control buttons
createControlButtons();
- // Start background music
LK.playMusic('dungeon');
- // Ensure raycasting variables are reset for the first frame of gameplay
shouldRaycast = true;
lastPlayerX = -999;
lastPlayerY = -999;
lastPlayerDir = -999;
+ particlePoolManager.initialize();
}
-// Setup game
function setupGame() {
if (gameState === 'title') {
showTitleScreen();
} else {
- // This case should ideally not be hit on initial load if gameState defaults to 'title'
- // but good for potential future states.
initializeGamePlayElements();
}
}
function generateMap() {
- // Clear existing map visuals, data structures
miniMap.removeChildren();
map = [];
- // Remove existing monsters and treasures
for (var i = 0; i < monsters.length; i++) {
if (monsters[i] && typeof monsters[i].destroy === 'function') {
monsters[i].destroy();
}
@@ -2361,43 +2221,30 @@
treasures[i].destroy();
}
}
treasures = [];
- // Clear projectiles
for (var i = 0; i < projectiles.length; i++) {
if (projectiles[i] && typeof projectiles[i].destroy === 'function') {
projectiles[i].destroy();
}
}
projectiles = [];
- // Clear existing powerups
for (var i = 0; i < powerUps.length; i++) {
if (powerUps[i] && typeof powerUps[i].destroy === 'function') {
powerUps[i].destroy();
}
}
powerUps = [];
- // Increase monster/boss difficulty every 5 completed levels
- // This check uses player.level, which is the *new* level number after advancing.
- // So, (player.level - 1) is the level just completed.
if ((player.level - 1) % 5 === 0 && player.level > 1) {
- // Regular monsters
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 monster
BOSS_CURRENT_BASE_HEALTH += 10;
- FIREBALL_DAMAGE += 2; // Boss attack power is tied to fireball damage
- console.log("Level " + (player.level - 1) + " complete. Difficulty Increased!");
- console.log("New Ogre: HP=" + OGRE_CURRENT_BASE_HEALTH + ", ATK=" + OGRE_CURRENT_BASE_ATTACK);
- console.log("New Eyeball: HP=" + EYEBALL_CURRENT_BASE_HEALTH + ", ATK=" + EYEBALL_CURRENT_BASE_ATTACK);
- console.log("New Imp: HP=" + IMP_CURRENT_BASE_HEALTH + ", ATK=" + IMP_CURRENT_BASE_ATTACK);
- console.log("New Boss: HP=" + BOSS_CURRENT_BASE_HEALTH + ", Fireball DMG=" + FIREBALL_DAMAGE);
+ FIREBALL_DAMAGE += 2;
}
- // Clear existing fountains
for (var i = 0; i < fountains.length; i++) {
if (fountains[i]) {
if (fountains[i].parent) {
fountains[i].parent.removeChild(fountains[i]);
@@ -2407,34 +2254,28 @@
}
}
}
fountains = [];
- fountainPlacedThisLevel = false; // Reset flag for the new level
- // --- Map type selection ---
+ fountainPlacedThisLevel = false;
var mapType;
if (player.level !== 0 && player.level % 5 === 0) {
- mapType = 1; // Arena map for boss level
+ mapType = 1;
} else {
- // Regular levels: select from maze, mixed, or spiral. Give more weight to maze type.
- // Maze (0) = 50% chance, Mixed (2) = 25% chance, Spiral (4) = 25% chance.
var availableMapTypes = [0, 0, 2, 4];
- mapType = availableMapTypes[Math.floor(Math.random() * availableMapTypes.length)];
+ mapType = availableMapTypes[fastFloor(Math.random() * availableMapTypes.length)];
}
- // 1. Initialize the map: all cells are walls
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); // Initialize as wall
+ cell.setType(1);
map[y][x] = cell;
miniMap.addChild(cell);
}
}
- // --- Maze type ---
if (mapType === 0) {
- // Implement Recursive Backtracker
var stack = [];
var startX = 1;
var startY = 1;
map[startY][startX].setType(0);
@@ -2464,11 +2305,10 @@
dy: 0,
wallXOffset: -1,
wallYOffset: 0
}];
- // Shuffle potential moves
for (var i = potentialMoves.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
+ var j = fastFloor(Math.random() * (i + 1));
var temp = potentialMoves[i];
potentialMoves[i] = potentialMoves[j];
potentialMoves[j] = temp;
}
@@ -2478,11 +2318,9 @@
var nextX = current.x + move.dx;
var nextY = current.y + move.dy;
var wallBetweenX = current.x + move.wallXOffset;
var wallBetweenY = current.y + move.wallYOffset;
- // Allow generation up to the edges for maximum space
if (nextX >= 0 && nextX < MAP_SIZE && nextY >= 0 && nextY < MAP_SIZE && map[nextY][nextX].type === 1) {
- // Critical check: Ensure wallBetweenX/Y are within valid map boundaries BEFORE accessing map[wallBetweenY][wallBetweenX]
if (wallBetweenX >= 0 && wallBetweenX < MAP_SIZE && wallBetweenY >= 0 && wallBetweenY < MAP_SIZE) {
map[wallBetweenY][wallBetweenX].setType(0);
map[nextY][nextX].setType(0);
stack.push({
@@ -2498,117 +2336,88 @@
stack.pop();
}
}
} else if (mapType === 1) {
- // --- Arena with pillars ---
- // Use margin of 1 to maximize space but ensure walls at edges
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);
}
}
- // Place pillars in a grid pattern
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) {
- // Ensure pillar is within map
map[py][px].setType(1);
}
}
}
} else if (mapType === 4) {
- // Spiral Maze
- // Create a spiral from outside to center
var spiralSteps = [];
var x = 1,
- y = 1; // Start position (top-left corner)
+ y = 1;
var dx = 1,
- dy = 0; // Start moving right
- var segmentLength = MAP_SIZE - 3; // Initial segment length, ensuring it fits within map boundaries (MAP_SIZE - 1 for outer wall, -1 for inner path, -1 for turn space)
+ dy = 0;
+ var segmentLength = MAP_SIZE - 3;
var segmentPassed = 0;
var turnsCompleted = 0;
- // Mark starting position
if (y >= 0 && y < MAP_SIZE && x >= 0 && x < MAP_SIZE) {
- // Bounds check for initial mark
map[y][x].setType(0);
spiralSteps.push({
x: x,
y: y
});
}
- // Generate main spiral path
while (segmentLength > 0) {
- // Move forward
x += dx;
y += dy;
- // Check bounds before marking path
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 {
- // If we hit a boundary, stop this segment or adjust
break;
}
segmentPassed++;
- // Time to turn?
if (segmentPassed >= segmentLength) {
segmentPassed = 0;
- // Turn 90 degrees clockwise
var temp = dx;
dx = -dy;
dy = temp;
turnsCompleted++;
- // Every 2 turns, reduce segment length
if (turnsCompleted % 2 === 0) {
- segmentLength -= 2; // Reduce by 2 to account for walls on both sides
+ segmentLength -= 2;
}
}
}
- // Add some strategic shortcuts/branches
- var numShortcuts = 2 + Math.floor(Math.random() * 3); // 2-4 shortcuts
+ var numShortcuts = 2 + fastFloor(Math.random() * 3);
for (var s = 0; s < numShortcuts; s++) {
if (spiralSteps.length <= 10) {
continue;
- } // Not enough steps to pick a safe random point
- // Pick a random point on the spiral
- var pointIndex = Math.floor(Math.random() * (spiralSteps.length - 10)) + 5;
+ }
+ var pointIndex = fastFloor(Math.random() * (spiralSteps.length - 10)) + 5;
var point = spiralSteps[pointIndex];
- // Try to create a small branch or shortcut
- var branchDir = Math.floor(Math.random() * 4);
+ var branchDir = fastFloor(Math.random() * 4);
var branchDirs = [{
dx: 0,
dy: -2
- },
- // Up
- {
+ }, {
dx: 2,
dy: 0
- },
- // Right
- {
+ }, {
dx: 0,
dy: 2
- },
- // Down
- {
+ }, {
dx: -2,
dy: 0
- } // Left
- ];
+ }];
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;
- // Check if we can make this branch (within inner map boundaries)
- if (branchX >= 1 && branchX < MAP_SIZE - 1 && branchY >= 1 && branchY < MAP_SIZE - 1 && connectX >= 1 && connectX < MAP_SIZE - 1 &&
- // also check connector cell
- connectY >= 1 && connectY < MAP_SIZE - 1) {
- // Only create branch if it connects to another corridor or with a chance for dead-ends
+ 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
@@ -2624,52 +2433,39 @@
}];
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 &&
- // Check within inner map boundaries
- checkY >= 1 && checkY < MAP_SIZE - 1 && map[checkY][checkX].type === 0 && (Math.abs(checkX - point.x) > 1 || Math.abs(checkY - point.y) > 1)) {
- // Ensure it's not connecting back to itself immediately
+ 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) {
- // 30% chance for dead-end branches
map[connectY][connectX].setType(0);
map[branchY][branchX].setType(0);
- // Sometimes extend the branch further
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 &&
- // check connector
- extConnectY >= 1 && extConnectY < MAP_SIZE - 1 &&
- // check connector
- map[extY][extX].type === 1) {
- // Ensure target is a wall
+ 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);
}
}
}
}
}
- // Add a few small rooms/wider areas along the spiral
- var numRooms = 1 + Math.floor(Math.random() * 3); // 1-3 small rooms
+ var numRooms = 1 + fastFloor(Math.random() * 3);
for (var r = 0; r < numRooms; r++) {
if (spiralSteps.length === 0) {
continue;
- } // No points to create rooms from
- var roomPointIndex = Math.floor(Math.random() * spiralSteps.length);
+ }
+ var roomPointIndex = fastFloor(Math.random() * spiralSteps.length);
var roomPoint = spiralSteps[roomPointIndex];
- // Create a small 2x2 or 3x3 room
var roomSize = Math.random() < 0.5 ? 2 : 3;
- var roomStartX = roomPoint.x - Math.floor(roomSize / 2);
- var roomStartY = roomPoint.y - Math.floor(roomSize / 2);
- // Carve room if it fits within inner map boundaries
+ 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;
@@ -2691,11 +2487,8 @@
}
}
}
} else {
- // mapType === 2 (Mixed)
- // --- Mixed: maze with open arena center ---
- // First, generate a maze
var stack = [];
var startX = 1;
var startY = 1;
map[startY][startX].setType(0);
@@ -2726,9 +2519,9 @@
wallXOffset: -1,
wallYOffset: 0
}];
for (var i = potentialMoves.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
+ var j = fastFloor(Math.random() * (i + 1));
var temp = potentialMoves[i];
potentialMoves[i] = potentialMoves[j];
potentialMoves[j] = temp;
}
@@ -2738,11 +2531,9 @@
var nextX = current.x + move.dx;
var nextY = current.y + move.dy;
var wallBetweenX = current.x + move.wallXOffset;
var wallBetweenY = current.y + move.wallYOffset;
- // Allow generation up to edges
if (nextX >= 0 && nextX < MAP_SIZE && nextY >= 0 && nextY < MAP_SIZE && map[nextY][nextX].type === 1) {
- // Critical check for wallBetween
if (wallBetweenX >= 0 && wallBetweenX < MAP_SIZE && wallBetweenY >= 0 && wallBetweenY < MAP_SIZE) {
map[wallBetweenY][wallBetweenX].setType(0);
map[nextY][nextX].setType(0);
stack.push({
@@ -2757,78 +2548,63 @@
if (!moved) {
stack.pop();
}
}
- // Open up a central arena - use smaller margin to maximize space
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) {
- // Ensure within map bounds
map[y][x].setType(0);
}
}
}
- // Add random pillars
for (var p = 0; p < 8; p++) {
- var px = Math.floor(Math.random() * (MAP_SIZE - 2 * arenaMargin - 2)) + arenaMargin + 1;
- var py = Math.floor(Math.random() * (MAP_SIZE - 2 * arenaMargin - 2)) + arenaMargin + 1;
+ 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) {
- // Ensure pillar is within map bounds
map[py][px].setType(1);
}
}
}
- // CRITICAL FIX: Ensure outer border is ALWAYS walls
- // This prevents any floor tiles from being at the very edge
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);
}
- // 4. Ensure all parts of the map are connected
ensureMapConnectivity();
- // --- Player Spawn Logic --- (Copied from original, assuming it's still desired)
var intendedPlayerX, intendedPlayerY;
if (mapType === 0) {
- // Maze
intendedPlayerX = 1.5;
intendedPlayerY = 1.5;
} else if (mapType === 1) {
- // Arena
- var arenaMarginForSpawn = 2; // Use the margin defined for arena
- intendedPlayerX = arenaMarginForSpawn + 0.5; // Spawn just inside the open area
+ var arenaMarginForSpawn = 2;
+ intendedPlayerX = arenaMarginForSpawn + 0.5;
intendedPlayerY = arenaMarginForSpawn + 0.5;
} else if (mapType === 2) {
- // Mixed
- var mixedArenaMarginForSpawn = 3; // Use the new arenaMargin for mixed maps
+ var mixedArenaMarginForSpawn = 3;
intendedPlayerX = mixedArenaMarginForSpawn + 0.5;
intendedPlayerY = mixedArenaMarginForSpawn + 0.5;
} else if (mapType === 4) {
- // Spiral Maze
- // For spiral, start near the beginning of the spiral
intendedPlayerX = 1.5;
intendedPlayerY = 1.5;
} else if (mapType === 3) {
- // Grid (This case should ideally not be hit if mapType 3 is removed from availableMapTypes, but keep for robustness or if it's reintroduced)
- intendedPlayerX = Math.floor(MAP_SIZE / 2) + 0.5;
- intendedPlayerY = Math.floor(MAP_SIZE / 2) + 0.5;
+ intendedPlayerX = fastFloor(MAP_SIZE / 2) + 0.5;
+ intendedPlayerY = fastFloor(MAP_SIZE / 2) + 0.5;
}
var spawnIsValid = false;
- var intendedCellX = Math.floor(intendedPlayerX);
- var intendedCellY = Math.floor(intendedPlayerY);
+ 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) {
- console.warn("Intended player spawn point (" + intendedPlayerX + "," + intendedPlayerY + ") was invalid for mapType " + mapType + ". Searching for an alternative...");
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;
@@ -2841,53 +2617,47 @@
}
}
}
if (alternativeSpawns.length > 0) {
- var randomIndex = Math.floor(Math.random() * alternativeSpawns.length);
+ var randomIndex = fastFloor(Math.random() * alternativeSpawns.length);
player.x = alternativeSpawns[randomIndex].x;
player.y = alternativeSpawns[randomIndex].y;
- console.log("Player spawn adjusted to a valid alternative: (" + player.x + ", " + player.y + ")");
} else {
- console.error("CRITICAL: No valid player spawn point found anywhere. Defaulting to (1.5, 1.5).");
- player.x = 1.5; // Fallback, try to ensure this is a floor if possible
+ player.x = 1.5;
player.y = 1.5;
if (map[1] && map[1][1] && map[1][1].type === 1) {
map[1][1].setType(0);
- } // Force (1,1) to be floor
+ }
}
}
player.dir = 0;
var initialPlayerDirForRotationCheck = player.dir;
var playerRotationAttempts = 0;
while (playerRotationAttempts < 4) {
var lookAheadDist = 1.0;
- var cellInFrontMapX = Math.floor(player.x + Math.cos(player.dir) * lookAheadDist);
- var cellInFrontMapY = Math.floor(player.y + Math.sin(player.dir) * lookAheadDist);
+ 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 / 2;
- if (player.dir >= Math.PI * 2) {
- player.dir -= Math.PI * 2;
+ player.dir += MATH_PI_HALF;
+ if (player.dir >= MATH_PI_2) {
+ player.dir -= MATH_PI_2;
}
playerRotationAttempts++;
}
if (playerRotationAttempts === 4) {
- console.warn("Player could not be rotated to face an open space. Reverting to: " + initialPlayerDirForRotationCheck);
player.dir = initialPlayerDirForRotationCheck;
}
- // --- Monster, Treasure, Gate, Fountain Placement (Copied, assuming still desired) ---
if (!(player.level !== 0 && player.level % 5 === 0)) {
- // Non-boss levels
- var monstersToPlace = MONSTER_COUNT + Math.floor(player.level * 0.5);
+ var monstersToPlace = MONSTER_COUNT + fastFloor(player.level * 0.5);
for (var i = 0; i < monstersToPlace; i++) {
placeMonster();
}
} else {
- // Boss level
- monsters = []; // Ensure no regular monsters
+ monsters = [];
var boss = new BossMonster();
- var arenaMarginBoss = 1; // Using new arena margin
+ var arenaMarginBoss = 1;
var bossSpawnX, bossSpawnY;
var foundBossSpawn = false;
var attempts = 0;
var preferredSpawns = [{
@@ -2898,10 +2668,10 @@
y: MAP_SIZE / 2 + 0.5
}];
for (var s = 0; s < preferredSpawns.length; s++) {
var spawn = preferredSpawns[s];
- var cellX = Math.floor(spawn.x);
- var cellY = Math.floor(spawn.y);
+ 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;
@@ -2909,53 +2679,49 @@
}
}
if (!foundBossSpawn) {
do {
- bossSpawnX = Math.floor(Math.random() * (MAP_SIZE - 2 * arenaMarginBoss - 2)) + arenaMarginBoss + 1 + 0.5;
- bossSpawnY = Math.floor(Math.random() * (MAP_SIZE - 2 * arenaMarginBoss - 2)) + arenaMarginBoss + 1 + 0.5;
+ 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;
- console.warn("Boss spawn fallback to center.");
break;
}
- var cellX = Math.floor(bossSpawnX);
- var cellY = Math.floor(bossSpawnY);
+ 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[Math.floor(boss.mapY)] && map[Math.floor(boss.mapY)][Math.floor(boss.mapX)]) {
- map[Math.floor(boss.mapY)][Math.floor(boss.mapX)].addMonster();
- } else {
- console.error("Failed to register boss on map cell: " + Math.floor(boss.mapX) + "," + Math.floor(boss.mapY));
+ if (map[fastFloor(boss.mapY)] && map[fastFloor(boss.mapY)][fastFloor(boss.mapX)]) {
+ map[fastFloor(boss.mapY)][fastFloor(boss.mapX)].addMonster();
}
}
placeTreasure();
gate = placeGate();
placeFountain();
- // --- End Placement Logic ---
}
function placePowerUp() {
var x, y;
var attempts = 0;
var powerUpTypes = ['speed', 'defense', 'health', 'attack'];
- var randomType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
+ var randomType = powerUpTypes[fastFloor(Math.random() * powerUpTypes.length)];
do {
- x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
- y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
+ 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; // Center in cell
- powerUp.mapY = y + 0.5; // Center in cell
+ powerUp.mapX = x + 0.5;
+ powerUp.mapY = y + 0.5;
powerUps.push(powerUp);
dynamicEntitiesContainer.addChild(powerUp);
}
}
@@ -2967,12 +2733,11 @@
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) {
- // Check if already a fountain here by iterating fountains array
var alreadyFountain = false;
for (var fIdx = 0; fIdx < fountains.length; fIdx++) {
- if (Math.floor(fountains[fIdx].mapX) === x && Math.floor(fountains[fIdx].mapY) === y) {
+ if (fastFloor(fountains[fIdx].mapX) === x && fastFloor(fountains[fIdx].mapY) === y) {
alreadyFountain = true;
break;
}
}
@@ -3017,11 +2782,10 @@
}
if (deadEndSpots.length === 0) {
return;
}
- // Shuffle deadEndSpots
for (var i = deadEndSpots.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
+ var j = fastFloor(Math.random() * (i + 1));
var temp = deadEndSpots[i];
deadEndSpots[i] = deadEndSpots[j];
deadEndSpots[j] = temp;
}
@@ -3029,73 +2793,58 @@
for (var k = 0; k < deadEndSpots.length; k++) {
var spotToPlace = deadEndSpots[k];
var cellX = spotToPlace.x;
var cellY = spotToPlace.y;
- var isPlayerSpot = cellX === Math.floor(player.x) && cellY === Math.floor(player.y);
- // Gate might not be placed yet if this is called before placeGate, so check gate object directly
- var isGateSpot = gate && cellX === Math.floor(gate.mapX) && cellY === Math.floor(gate.mapY);
- // Re-check map cell properties directly, as they are the source of truth for monsters/treasures placed by their respective functions
+ 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); // Add to the same container as monsters/treasures
+ dynamicEntitiesContainer.addChild(fountain);
fountainPlacedThisLevel = true;
placed = true;
break;
}
}
}
function mapCellHasPowerUp(cellX, cellY) {
for (var i = 0; i < powerUps.length; i++) {
- if (Math.floor(powerUps[i].mapX) === cellX && Math.floor(powerUps[i].mapY) === cellY) {
+ if (fastFloor(powerUps[i].mapX) === cellX && fastFloor(powerUps[i].mapY) === cellY) {
return true;
}
}
return false;
}
function placeMonster() {
- // Find a random empty cell
var x, y;
var attempts = 0;
do {
- x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
- y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
+ x = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1;
+ y = fastFloor(Math.random() * (MAP_SIZE - 2)) + 1;
attempts++;
- // Make sure it's not too close to the player
- var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
+ var distToPlayer = getDistanceToPoint(x, player.x, y, player.y);
if (attempts > 100) {
break;
- } // Prevent infinite loop
+ }
} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || distToPlayer < 3);
if (attempts <= 100) {
map[y][x].addMonster();
var monster;
- // Randomly decide to spawn a regular Monster, an EyeballMonster, or an ImpMonster
var randMonsterType = Math.random();
if (randMonsterType < 0.1) {
- // 50% chance for Ogre (Monster)
monster = new Monster();
- // Monster health is set in its constructor
} else if (randMonsterType < 0.4) {
- // 30% chance for EyeballMonster (0.5 to 0.799...)
monster = new EyeballMonster();
- // EyeballMonster health is set in its constructor
- // monster.startBobbing(); // Call removed as bobbing is disabled
} else {
- // 20% chance for ImpMonster (0.8 to 0.999...)
monster = new ImpMonster();
- // ImpMonster health and other stats are set in its constructor
}
- // Common monster setup
- monster.mapX = x + 0.5; // Center in cell
- monster.mapY = y + 0.5; // Center in cell
- // Initialize attack cooldown properties for all monster types
- // These might be defined in individual monster classes but ensuring they exist for MonsterAI
+ monster.mapX = x + 0.5;
+ monster.mapY = y + 0.5;
if (monster.attackCooldown === undefined) {
monster.attackCooldown = 2000;
- } // Default if not set
+ }
if (monster.lastAttackTime === undefined) {
monster.lastAttackTime = 0;
}
if (monster.canAttack === undefined) {
@@ -3107,53 +2856,40 @@
}
function placeTreasure() {
var potentialSpots = [];
var deadEndSpots = [];
- // Iterate through the map (excluding borders initially for simplicity in dead-end logic)
- // to find all valid floor cells for treasure.
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 &&
- // Check if cell already has a treasure (though addTreasure also checks)
- !map[y][x].gate) {
+ 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; // No suitable spots found
+ return;
}
- // Identify dead ends from the list of potential spots
for (var i = 0; i < potentialSpots.length; i++) {
var spot = potentialSpots[i];
var openNeighbors = 0;
var neighbors = [{
dx: 0,
dy: -1
- },
- // North
- {
+ }, {
dx: 1,
dy: 0
- },
- // East
- {
+ }, {
dx: 0,
dy: 1
- },
- // South
- {
+ }, {
dx: -1,
dy: 0
- } // West
- ];
+ }];
for (var j = 0; j < neighbors.length; j++) {
var nx = spot.x + neighbors[j].dx;
var ny = spot.y + neighbors[j].dy;
- // Check bounds and if neighbor is a floor cell
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny] && map[ny][nx] && map[ny][nx].type === 0) {
openNeighbors++;
}
}
@@ -3161,15 +2897,13 @@
deadEndSpots.push(spot);
}
}
var treasuresPlaced = 0;
- var maxTreasures = 1 + Math.floor(Math.random() * 3); // 1 to 3 treasures
- // Attempt to place treasures in dead ends if any exist
+ var maxTreasures = 1 + fastFloor(Math.random() * 3);
if (deadEndSpots.length > 0) {
if (deadEndSpots.length > 1) {
- // Shuffle deadEndSpots
for (var i = deadEndSpots.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
+ var j = fastFloor(Math.random() * (i + 1));
var temp = deadEndSpots[i];
deadEndSpots[i] = deadEndSpots[j];
deadEndSpots[j] = temp;
}
@@ -3177,565 +2911,523 @@
for (var i = 0; i < deadEndSpots.length && treasuresPlaced < maxTreasures; i++) {
var spot = deadEndSpots[i];
var cellX = spot.x;
var cellY = spot.y;
- // Check if the spot is the player's current location
- if (cellX === Math.floor(player.x) && cellY === Math.floor(player.y)) {
- continue; // Skip this spot if it's where the player is
+ if (cellX === fastFloor(player.x) && cellY === fastFloor(player.y)) {
+ continue;
}
- // Double-check if we can add treasure to this cell (MapCell's own logic)
if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].addTreasure()) {
var treasure = new Treasure();
- treasure.mapX = cellX + 0.5; // Center treasure in the cell
- treasure.mapY = cellY + 0.5; // Center treasure in the cell
- treasure.value = 1 + Math.floor(Math.random() * player.level);
+ 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++;
}
}
- } //{n6} // End of dead-end placement attempt
- // If no treasures were placed (either no dead ends or dead ends unsuitable),
- // and potentialSpots exist, try to place at least one treasure from general potential spots.
+ }
if (treasuresPlaced === 0 && potentialSpots.length > 0) {
- // Shuffle potentialSpots to get a random non-dead-end spot
for (var i = potentialSpots.length - 1; i > 0; i--) {
- var j = Math.floor(Math.random() * (i + 1));
+ 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;
- // Ensure spot is not player's location or gate location.
- // The map[cellY][cellX].addTreasure() call will handle other occupancy checks (monster, existing treasure).
- if (cellX === Math.floor(player.x) && cellY === Math.floor(player.y)) {
+ if (cellX === fastFloor(player.x) && cellY === fastFloor(player.y)) {
continue;
}
- if (gate && cellX === Math.floor(gate.mapX) && cellY === Math.floor(gate.mapY)) {
+ 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 + Math.floor(Math.random() * player.level); // Standard value for fallback treasure
+ treasure.value = 1 + fastFloor(Math.random() * player.level);
treasures.push(treasure);
dynamicEntitiesContainer.addChild(treasure);
- treasuresPlaced++; // This will become 1
- break; // Only place one fallback treasure
+ treasuresPlaced++;
+ break;
}
}
}
}
function placeGate() {
- // Place gate in a random valid location
var x, y;
var attempts = 0;
var validPositions = [];
- // Collect all valid positions first
for (var i = 0; i < 100; i++) {
- x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
- y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
- // Check if the cell is suitable
+ 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) {
- // Make sure it's far from the player's spawn point
- var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
+ var distToPlayer = getDistanceToPoint(x, player.x, y, player.y);
if (distToPlayer > MAP_SIZE / 2.5) {
- // Increased distance requirement, ensures it's far
- // Add to valid positions
validPositions.push({
x: x,
y: y
});
}
}
}
- // If we found valid spots, choose one randomly
if (validPositions.length > 0) {
- // Pick a random position from the valid ones
- var randomIndex = Math.floor(Math.random() * validPositions.length);
+ var randomIndex = fastFloor(Math.random() * validPositions.length);
var chosenPos = validPositions[randomIndex];
- var gateX = chosenPos.x; // This is the cell's integer X index
- var gateY = chosenPos.y; // This is the cell's integer Y index
- // Place the gate
- map[gateY][gateX].addGate(); // Use integer indices for map cell logic
+ var gateX = chosenPos.x;
+ var gateY = chosenPos.y;
+ map[gateY][gateX].addGate();
var gate = new Gate();
- // Set gate's world position to the center of the cell
gate.mapX = gateX + 0.5;
gate.mapY = gateY + 0.5;
dynamicEntitiesContainer.addChild(gate);
return gate;
}
return null;
}
function createUI() {
- // Health display
- healthText = new Text2('Health: ' + player.health, {
- size: 40,
- fill: 0xFF5555
+ // Create stats panel in top right
+ var statsPanel = new Container();
+ LK.gui.topRight.addChild(statsPanel);
+ statsPanel.x = -280; // Adjusted for potentially wider text
+ statsPanel.y = 20;
+ // Health with current/max format
+ healthText = new Text2("\u2764\uFE0F Health: ".concat(player.health, "/").concat(player.maxHealth), {
+ size: 36,
+ fill: 0xFF5555,
+ stroke: 0x000000,
+ strokeThickness: 2
});
healthText.anchor.set(0, 0);
- LK.gui.topRight.addChild(healthText);
- healthText.x = -200;
- healthText.y = 20;
- // Score display
- scoreText = new Text2('Score: ' + player.score, {
- size: 40,
- fill: 0xFFFF55
+ statsPanel.addChild(healthText);
+ healthText.y = 0;
+ // Attack stat
+ attackText = new Text2("\u2694\uFE0F Attack: ".concat(player.attackPower), {
+ size: 36,
+ fill: 0xFFAA55,
+ stroke: 0x000000,
+ strokeThickness: 2
});
- scoreText.anchor.set(0, 0);
- LK.gui.topRight.addChild(scoreText);
- scoreText.x = -200;
- scoreText.y = 80;
- // Level display
- levelText = new Text2('Level: ' + player.level, {
- size: 40,
- fill: 0x55FF55
+ attackText.anchor.set(0, 0);
+ statsPanel.addChild(attackText);
+ attackText.y = 50;
+ // Defense stat
+ defenseText = new Text2("\uD83D\uDEE1\uFE0F Defense: ".concat(player.defense), {
+ // Used shield emoji
+ size: 36,
+ fill: 0x5555FF,
+ stroke: 0x000000,
+ strokeThickness: 2
});
+ defenseText.anchor.set(0, 0);
+ statsPanel.addChild(defenseText);
+ defenseText.y = 100;
+ // Speed stat
+ speedText = new Text2("\uD83D\uDC5F Speed: ".concat(player.moveSpeedMultiplier.toFixed(1), "x"), {
+ // Used shoe emoji
+ 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: ".concat(player.level), {
+ size: 32,
+ fill: 0x55FF55,
+ stroke: 0x000000,
+ strokeThickness: 2
+ });
levelText.anchor.set(0, 0);
- LK.gui.topRight.addChild(levelText);
- levelText.x = -200;
- levelText.y = 140;
- // Monster counter display
- monsterText = new Text2('Monsters: 0', {
- size: 40,
- fill: 0xFF9955
+ 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: 32,
+ fill: 0xFF9955,
+ stroke: 0x000000,
+ strokeThickness: 2
});
monsterText.anchor.set(0, 0);
- LK.gui.topRight.addChild(monsterText);
- monsterText.x = -200;
- monsterText.y = 200;
- // Update UI displays
- updateUI();
- // Initialize Interaction Prompt UI
+ gameLayer.addChild(monsterText);
+ monsterText.x = miniMap.x;
+ monsterText.y = levelText.y + 40;
+ // 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); // Centered
+ interactionPromptMainText.anchor.set(0.5, 0.5);
interactionPromptContainer.addChild(interactionPromptMainText);
- interactionPromptParentheticalGroup = new Container(); // Anchor (0,0) by default
+ interactionPromptParentheticalGroup = new Container();
interactionPromptPressOpenText = new Text2('(Press ', {
size: 60,
- // Slightly smaller for parenthetical part
fill: 0xDDDDDD,
stroke: 0x000000,
strokeThickness: 5
});
- interactionPromptPressOpenText.anchor.set(0, 0.5); // Anchor left-middle for horizontal layout
- // Use 'controlButton' asset for the image, scaled appropriately
+ interactionPromptPressOpenText.anchor.set(0, 0.5);
interactionPromptButtonImage = LK.getAsset('attackButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
- // Scaled for prompt
scaleY: 0.6
});
interactionPromptPressCloseText = new Text2(')', {
size: 60,
- // Slightly smaller
fill: 0xDDDDDD,
stroke: 0x000000,
strokeThickness: 5
});
- interactionPromptPressCloseText.anchor.set(0, 0.5); // Anchor left-middle
+ 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() {
- healthText.setText('Health: ' + player.health);
- scoreText.setText('Score: ' + player.score);
- levelText.setText('Level: ' + player.level);
- monsterText.setText('Monsters: ' + monsters.length);
- // Update score in LK system
- LK.setScore(player.score);
+ 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() {
- // Create joystick control for movement
controlButtons.joystick = new JoystickController();
controlButtons.joystick.x = 400;
controlButtons.joystick.y = 2732 - 500;
gameLayer.addChild(controlButtons.joystick);
- // Using the global layer system - no need to recreate layers here
- // Create attack button more centered on the right side
controlButtons.attack = new ControlButton('attack');
controlButtons.attack.x = 2048 - 400;
controlButtons.attack.y = 2732 - 500;
gameLayer.addChild(controlButtons.attack);
- // Add right hand at the bottom right for projectile firing
globalRightHand = LK.getAsset('rightHand', {
anchorX: 0.5,
anchorY: 0.9,
scaleX: 1.2,
scaleY: 1.2
});
- globalRightHand.x = 2048 * 0.6; // Position just off right of middle
- globalRightHand.y = 2732; // Bottom of the screen
+ globalRightHand.x = 2048 * 0.6;
+ globalRightHand.y = 2732;
handLayer.addChild(globalRightHand);
- // Add pulse and float animation to give the hand some life
var _animateHand = function animateHand() {
- // Get a small random x offset between -40 and 40 pixels
var randomXOffset = Math.random() * 80 - 40;
- // Store original X position if not set yet
if (globalRightHand.originalX === undefined) {
globalRightHand.originalX = globalRightHand.x;
}
- // Slow scale pulse animation with random left/right movement
tween(globalRightHand, {
scaleX: 1.1,
scaleY: 1.1,
x: globalRightHand.originalX + randomXOffset,
- y: 2732 - 15 // Float up slightly from base position
+ y: 2732 - 15
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: function onFinish() {
- // Get another random x offset for the return animation
var returnRandomXOffset = Math.random() * 40 - 20;
tween(globalRightHand, {
scaleX: 1.1,
scaleY: 1.1,
x: globalRightHand.originalX + returnRandomXOffset,
- y: 2732 // Return to original position
+ y: 2732
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: _animateHand
});
}
});
};
- // Start the hand animation
_animateHand();
}
function castRayDDAInto(startX, startY, rayDirX, rayDirY, result) {
- // Pre-compute map coordinates only once
- var mapX = Math.floor(startX);
- var mapY = Math.floor(startY);
- // Early bounds check to avoid unnecessary computation
- if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) {
+ 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; // Default wall type if out of bounds
- result.mapX = mapX;
- result.mapY = mapY;
+ result.wallType = 1;
+ result.mapX = cache.mapX;
+ result.mapY = cache.mapY;
return;
}
- // Pre-compute these reciprocals once to avoid division in the loop
- var deltaDistX = Math.abs(rayDirX) < 0.0001 ? 9999999 : Math.abs(1 / rayDirX);
- var deltaDistY = Math.abs(rayDirY) < 0.0001 ? 9999999 : Math.abs(1 / rayDirY);
- var stepX, stepY, sideDistX, sideDistY;
- // Optimize direction calculations with ternary operators
- stepX = rayDirX < 0 ? -1 : 1;
- sideDistX = rayDirX < 0 ? (startX - mapX) * deltaDistX : (mapX + 1.0 - startX) * deltaDistX;
- stepY = rayDirY < 0 ? -1 : 1;
- sideDistY = rayDirY < 0 ? (startY - mapY) * deltaDistY : (mapY + 1.0 - startY) * deltaDistY;
- // Use a maximum iteration count to prevent potential infinite loops
+ 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; // Maximum map traversal
+ var maxIterations = MAP_SIZE * 2;
var iterations = 0;
- // Main DDA loop with iteration limit
while (!hit && iterations < maxIterations) {
- // Store the smaller of the two side distances for optimization
- if (sideDistX < sideDistY) {
- sideDistX += deltaDistX;
- mapX += stepX;
+ if (cache.sideDistX < cache.sideDistY) {
+ cache.sideDistX += cache.deltaDistX;
+ cache.mapX += cache.stepX;
side = 0;
} else {
- sideDistY += deltaDistY;
- mapY += stepY;
+ cache.sideDistY += cache.deltaDistY;
+ cache.mapY += cache.stepY;
side = 1;
}
- // Fast bounds check
- if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) {
- hit = true; // Ray went out of bounds
+ if (cache.mapX < 0 || cache.mapX >= MAP_SIZE || cache.mapY < 0 || cache.mapY >= MAP_SIZE) {
+ hit = true;
continue;
}
- // Fast wall check - avoid accessing map if possible
- if (map[mapY] && map[mapY][mapX] && map[mapY][mapX].type === 1) {
+ if (map[cache.mapY] && map[cache.mapY][cache.mapX] && map[cache.mapY][cache.mapX].type === 1) {
hit = true;
}
iterations++;
}
- // Calculate distance precisely for the current ray hit
var perpWallDist;
if (hit) {
- // Only calculate precise distance if a wall was hit (or went out of bounds)
if (side === 0) {
- perpWallDist = (mapX - startX + (1 - stepX) / 2) / rayDirX;
+ perpWallDist = (cache.mapX - startX + (1 - cache.stepX) / 2) / rayDirX;
} else {
- perpWallDist = (mapY - startY + (1 - stepY) / 2) / rayDirY;
+ perpWallDist = (cache.mapY - startY + (1 - cache.stepY) / 2) / rayDirY;
}
- // Clamp distance
perpWallDist = Math.max(0.001, Math.min(MAX_RENDER_DISTANCE, perpWallDist));
} else {
- // No wall hit within maxIterations, treat as max distance
perpWallDist = MAX_RENDER_DISTANCE;
}
result.wallHit = hit;
result.distance = perpWallDist;
result.side = side;
- // Determine wallType only if hit and within bounds
- if (hit && mapX >= 0 && mapX < MAP_SIZE && mapY >= 0 && mapY < MAP_SIZE && map[mapY] && map[mapY][mapX]) {
- result.wallType = map[mapY][mapX].type;
+ 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; // Default or out-of-bounds wall type
+ result.wallType = 1;
}
- result.mapX = mapX;
- result.mapY = mapY;
+ result.mapX = cache.mapX;
+ result.mapY = cache.mapY;
}
-// Separate the raycasting logic from the visual updates
function fullRayCasting() {
floorCaster.update(player.x, player.y, player.dir);
- // Pre-calculate values needed for all rays
+ var numRays = getNumRays();
+ var stripWidth = getStripWidth();
var playerX = player.x;
var playerY = player.y;
var playerDir = player.dir;
- // Process all rays at once
- for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
- var rayResult = rayResults[rayIdx]; // Use pre-allocated object
- // Calculate ray angle and direction
- var rayAngle = playerDir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
+ 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);
- // Get ray hit info - populates rayResult
castRayDDAInto(playerX, playerY, rayDirX, rayDirY, rayResult);
- // rayResult.distance is perpWallDist
if (rayResult.wallHit) {
- // Calculate final strip properties and store in rayResult
rayResult.actualDistance = rayResult.distance * Math.cos(rayAngle - playerDir);
- rayResult.wallHeight = Math.max(STRIP_WIDTH, Math.min(2732, WALL_HEIGHT_FACTOR / rayResult.actualDistance));
+ 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; // This scales wallX so one unit of wallX effectively spans 64 original texture slices.
- // Use all 64 wall tiles (1-64)
- // Original calculation provided an index from 0 to 63 for a 64-slice block.
- var baseIndexOriginal64 = Math.floor(texturePos) % 64; // This gives an index from 0 to 63.
- var tileIndex = baseIndexOriginal64 + 1; // Convert to 1-64 for walltile1 to walltile64
- // Apply reflection based on side and ray direction, mapping within the new range [1, 64].
+ 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) {
- // Reflects: e.g., if min=1, max=64: 1 maps to 64, 2 to 63, ..., 64 to 1.
- // Formula: new_min + (new_max - current_value_in_new_range)
tileIndex = minTexIdNewRange + (maxTexIdNewRange - tileIndex);
}
if (rayResult.side === 1 && rayDirY < 0) {
- // Same reflection logic for this case.
tileIndex = minTexIdNewRange + (maxTexIdNewRange - tileIndex);
}
rayResult.textureIndex = tileIndex;
- rayResult.screenX = Math.round(rayIdx * STRIP_WIDTH + (2048 - NUM_RAYS * STRIP_WIDTH) / 2);
- // Update strip with calculated values
+ rayResult.screenX = Math.round(rayIdx * stripWidth + (2048 - numRays * stripWidth) / 2);
var strip = wallSegments[rayIdx];
strip.x = rayResult.screenX;
- strip.updateStrip(STRIP_WIDTH, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY);
+ strip.updateStrip(stripWidth, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY);
strip.visible = true;
- // Add to renderableObjects (cleared each frame in game.update, so always push)
renderableObjects.push({
distance: rayResult.actualDistance,
object: strip,
type: 'wall',
originalSortOrder: rayIdx
});
} else {
- // Clear strip if no wall was hit
var strip = wallSegments[rayIdx];
if (strip && strip.clearStrip) {
strip.clearStrip();
strip.visible = false;
}
- // Ensure wallHit is false for updateRaycastVisuals
rayResult.wallHit = false;
}
}
}
function updateRaycastVisuals() {
- // floorCaster.update is handled by the main rayCasting function if this optimization path is taken.
- // Or, if floor/ceiling should update even on minor moves, it can be called here too.
- // For now, following prompt implies floor/ceiling only updates on fullRayCasting.
- // Process all rays using cached data
- for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
- var rayResult = rayResults[rayIdx]; // Use cached data
+ var numRays = getNumRays();
+ var stripWidth = getStripWidth();
+ for (var rayIdx = 0; rayIdx < numRays; rayIdx++) {
+ var rayResult = rayResults[rayIdx];
if (rayResult.wallHit) {
- // Check cached wallHit
- // Update strip with cached values
var strip = wallSegments[rayIdx];
strip.x = rayResult.screenX;
- strip.updateStrip(STRIP_WIDTH, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY);
+ strip.updateStrip(stripWidth, rayResult.wallHeight, rayIdx, rayResult.wallType, rayResult.actualDistance, 0, rayResult.side, rayResult.textureIndex, rayResult.mapX, rayResult.mapY);
strip.visible = true;
- // Add to renderableObjects using cached distance (cleared each frame in game.update, so always push)
renderableObjects.push({
distance: rayResult.actualDistance,
object: strip,
type: 'wall',
originalSortOrder: rayIdx
});
} else {
- // Clear strip if no wall was hit (based on cache)
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() {
- // Check if player has moved enough to require new raycasting
- // A small threshold helps prevent floating point inaccuracies from triggering constant raycasts.
var playerMoved = Math.abs(player.x - lastPlayerX) > 0.001 || Math.abs(player.y - lastPlayerY) > 0.001 || Math.abs(player.dir - lastPlayerDir) > 0.001;
- // Only perform full raycasting if needed
if (playerMoved || shouldRaycast) {
- // Perform full raycasting (this also updates floor/ceiling via floorCaster.update)
fullRayCasting();
- // Update cached player position and state
lastPlayerX = player.x;
lastPlayerY = player.y;
lastPlayerDir = player.dir;
- shouldRaycast = false; // Reset flag until next explicit request or significant move
+ shouldRaycast = false;
} else {
- // Just update the visual rendering of walls with already calculated ray data
- // Floor/ceiling are NOT updated in this path, as per optimization.
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);
}
- // visibleEntities array and local sorting are no longer needed here.
- // Entities will be added to the global renderableObjects array.
for (var i = 0; i < allDynamicEntities.length; i++) {
- var entity = allDynamicEntities[i];
- entity.visible = false; // Hide by default, will be set true if it should be rendered.
+ 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; // Store distance for sorting
+ entity.renderDist = dist;
if (dist >= MAX_RENDER_DISTANCE) {
- // Too far to render
continue;
}
var angle = Math.atan2(dy, dx) - player.dir;
- // Normalize angle to be within -PI to PI range
while (angle < -Math.PI) {
- angle += Math.PI * 2;
+ angle += MATH_PI_2;
}
while (angle > Math.PI) {
- angle -= Math.PI * 2;
+ angle -= MATH_PI_2;
}
if (Math.abs(angle) < HALF_FOV) {
- // Check if entity is within Field of View
var rayHit = castRayToPoint(player.x, player.y, entity.mapX, entity.mapY);
- // Check if there's a clear Line of Sight to the entity
- // (dist - 0.5 is a small tolerance, e.g. for entity's own depth/size)
if (!rayHit.hit || rayHit.dist > dist - 0.5) {
- entity.visible = true; // Mark as visible for rendering
- var screenX = (0.5 + angle / FOV) * 2048; // Project to screen X
- var height = WALL_HEIGHT_FACTOR / dist; // Calculate perceived height
- var scale = height / 100; // Assuming base asset height is 100 for scaling
+ entity.visible = true;
+ var screenX = (0.5 + angle / FOV) * 2048;
+ var height = WALL_HEIGHT_FACTOR / dist;
+ var scale = height / 100;
entity.x = screenX;
- // Y position calculation
- var screenY_horizon = 2732 / 2;
- var player_camera_height_projection_factor = WALL_HEIGHT_FACTOR * 0.5; // Simulates camera height
+ 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) {
- // Treasures are on the floor. Their anchor is 0.5 (center).
- // To place the bottom of the treasure on the 'screenY_for_floor_at_dist' line,
- // its center (entity.y) must be 'screenY_for_floor_at_dist - actualRenderedHeight / 2'.
- // 'scale' is (WALL_HEIGHT_FACTOR / dist) / 100, calculated a few lines above.
- // 'texture.height' is the original asset height.
var currentSprite = entity.isOpen ? entity.treasureOpenSprite : entity.treasureSprite;
- // Per LK guidelines, .width and .height on an asset provide its original dimensions.
- // The error indicates currentSprite.texture is undefined, so we use currentSprite.height instead.
var currentSpriteOriginalHeight = currentSprite.height;
- var actualRenderedHeight = currentSpriteOriginalHeight * scale; // 'scale' is the calculated scale for this frame
- // Adjusted from actualRenderedHeight / 2 (which is * 0.5) to * 0.45
- // This subtracts a slightly smaller amount, making entity.y larger (lower on screen).
+ var actualRenderedHeight = currentSpriteOriginalHeight * scale;
entity.y = screenY_for_floor_at_dist - actualRenderedHeight * 0.2;
} else if (entity instanceof PowerUp) {
- var powerUpWorldZ = entity.verticalOffset; // PowerUps have verticalOffset from chest
+ var powerUpWorldZ = entity.verticalOffset;
var screenY_offset_due_to_worldZ = powerUpWorldZ * z_to_screen_pixels_factor / dist;
- // currentBobOffset for PowerUp is in screen pixels.
entity.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ + entity.currentBobOffset - height / 2;
} else if (entity instanceof Fountain) {
- // Fountains are on the floor, similar to treasures.
var fountainSprite = entity.sprite;
var fountainSpriteOriginalHeight = fountainSprite.height;
var actualRenderedHeightFountain = fountainSpriteOriginalHeight * scale;
- // Using * 0.2 based on treasure logic to position bottom edge appropriately
entity.y = screenY_for_floor_at_dist - actualRenderedHeightFountain * 0.2;
} else if (entity instanceof EyeballMonster) {
- var eyeballWorldZ = entity.baseWorldYOffset; // currentBobbingYOffset removed
+ 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; // Eyeball anchor is 0.5, already centered.
+ entity.y = screenY_for_floor_at_dist - screenY_offset_due_to_worldZ;
} else if (entity instanceof Gate) {
- // Gate Y position to match treasure height
- // Ensure 'scale' and 'screenY_for_floor_at_dist' are defined earlier in this function.
- // 'scale' is derived from 'height' which is WALL_HEIGHT_FACTOR / dist.
- // 'screenY_for_floor_at_dist' = screenY_horizon + player_camera_height_projection_factor / dist;
- var gateSpriteOriginalHeight = entity.gateSprite.height; // Accessing the gate's sprite height
- var actualRenderedHeightGate = gateSpriteOriginalHeight * scale; // 'scale' is the dynamic scale factor
- entity.y = screenY_for_floor_at_dist - actualRenderedHeightGate * 0.2; // Match treasure's Y calculation
+ 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) {
- // Gate removed from this condition
- // Calculate Y position based on size multiplier (if any)
- var entitySizeMultiplier = entity.sizeMultiplier || 1.0; // Default to 1.0 if no multiplier
- var screenY_horizon = 2732 / 2; // Ensure screenY_horizon is defined in this specific scope if not already
- // 'height' should be available from earlier in the function: var height = WALL_HEIGHT_FACTOR / dist;
+ var entitySizeMultiplier = entity.sizeMultiplier || 1.0;
+ var screenY_horizon = SCREEN_HEIGHT_HALF;
if (entitySizeMultiplier < 1.0) {
- // For smaller entities, lower them proportionally
- // This means their visual base aligns more with larger entities, rather than their center.
- var sizeReduction = (1 - entitySizeMultiplier) * height * 0.5; // How much smaller they are in terms of screen height
- entity.y = screenY_horizon + sizeReduction; // Add to lower them (Y increases downwards)
+ var sizeReduction = (1 - entitySizeMultiplier) * height * 0.5;
+ entity.y = screenY_horizon + sizeReduction;
} else {
- // Normal positioning for regular-sized or larger entities (center on horizon)
entity.y = screenY_horizon;
}
} else {
- // Fallback for any other entity type (should ideally be handled above)
entity.y = screenY_horizon;
}
- // Scale adjustments
if (entity instanceof PowerUp) {
- var finalScale = scale * entity.spawnScaleMultiplier; // Apply spawn scale animation
+ var finalScale = scale * entity.spawnScaleMultiplier;
entity.scale.set(finalScale, finalScale);
} else if (entity.sizeMultiplier !== undefined) {
- // For monsters with custom size multipliers
var customScale = scale * entity.sizeMultiplier;
entity.scale.set(customScale, customScale);
} else {
- // Default scaling for other entities
entity.scale.set(scale, scale);
}
- // Apply distance-based tinting to entities
- var SHADE_START_FACTOR_ENTITIES = 0.3; // Match floor/ceiling behavior
- var MIN_SHADE_ENTITIES = 0.2; // General minimum shade for distant entities
+ 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);
- // --- Apply projectile light to entities ---
var totalProjectileLightInfluenceEntity = 0;
for (var k = 0; k < projectiles.length; k++) {
var proj = projectiles[k];
if (proj.active) {
@@ -3750,86 +3442,70 @@
}
}
}
entityShadeFactor = Math.min(1.0, entityShadeFactor + totalProjectileLightInfluenceEntity);
- // --- End projectile light for entities ---
var finalEntityTint;
if (entity instanceof Monster || entity instanceof EyeballMonster || entity instanceof ImpMonster || entity instanceof BossMonster) {
- // For Monster, EyeballMonster, ImpMonster, and BossMonster, 'entity' is the main container.
- // Tinting the container affects its children sprites. Base tint is white (0xFFFFFF).
- var r_ge = Math.floor(0xFF * entityShadeFactor);
- var g_ge = Math.floor(0xFF * entityShadeFactor);
- var b_ge = Math.floor(0xFF * entityShadeFactor);
+ 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;
- // Alpha is managed by internal sprite animations or visibility logic.
} else if (entity instanceof Treasure) {
- var baseTreasureTint = 0xdddddd; // Treasure has a specific base tint
+ var baseTreasureTint = 0xdddddd;
var r_t = baseTreasureTint >> 16 & 0xFF;
var g_t = baseTreasureTint >> 8 & 0xFF;
var b_t = baseTreasureTint & 0xFF;
- r_t = Math.floor(r_t * entityShadeFactor);
- g_t = Math.floor(g_t * entityShadeFactor);
- b_t = Math.floor(b_t * entityShadeFactor);
+ 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;
- // Preserve targetSprite.alpha for open/close animation.
} else if (entity instanceof PowerUp) {
- // PowerUp sprite base tint is white (0xFFFFFF).
- var r_p = Math.floor(0xFF * entityShadeFactor);
- var g_p = Math.floor(0xFF * entityShadeFactor);
- var b_p = Math.floor(0xFF * entityShadeFactor);
+ 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;
- // Preserve entity.powerUpSprite.alpha for collection animation.
} else if (entity instanceof Fountain) {
- var baseFountainTint = entity.state === 'full' ? 0xFFFFFF : 0x707070; // White when full, dark grey when empty
+ 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 = Math.floor(r_ft * entityShadeFactor);
- g_ft = Math.floor(g_ft * entityShadeFactor);
- b_ft = Math.floor(b_ft * entityShadeFactor);
+ 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; // Gate is tinted green
+ var baseGateTint = 0x00FF00;
var r_ga = baseGateTint >> 16 & 0xFF;
var g_ga = baseGateTint >> 8 & 0xFF;
var b_ga = baseGateTint & 0xFF;
- r_ga = Math.floor(r_ga * entityShadeFactor);
- g_ga = Math.floor(g_ga * entityShadeFactor);
- b_ga = Math.floor(b_ga * entityShadeFactor);
+ 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;
- // Preserve entity.gateSprite.alpha for its pulse animation.
}
- // Add to global list for sorting with walls
renderableObjects.push({
distance: entity.renderDist,
object: entity,
type: 'entity'
});
}
}
}
- // Sorting and adding to dynamicEntitiesContainer is now done in game.update,
- // after both walls and entities have been added to renderableObjects.
- // dynamicEntitiesContainer.removeChildren(); // Moved
- // visibleEntities.sort(...); // Removed
- // for (var j = 0; j < visibleEntities.length; j++) { // Removed
- // dynamicEntitiesContainer.addChild(visibleEntities[j]);
- // }
}
function castRayToPoint(startX, startY, targetX, targetY) {
var rayDirX = targetX - startX;
var rayDirY = targetY - startY;
var distance = Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
rayDirX /= distance;
rayDirY /= distance;
- var mapCheckX = Math.floor(startX);
- var mapCheckY = Math.floor(startY);
+ var 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;
@@ -3875,9 +3551,9 @@
var currentJoystickX, currentJoystickY, isJoystickConsideredActive;
if (joystickOverrideActive) {
currentJoystickX = joystickOverrideX;
currentJoystickY = joystickOverrideY;
- isJoystickConsideredActive = true; // Movement continues based on pre-attack state
+ isJoystickConsideredActive = true;
} else if (joystick && joystick.active) {
currentJoystickX = joystick.normalizedX;
currentJoystickY = joystick.normalizedY;
isJoystickConsideredActive = true;
@@ -3896,29 +3572,23 @@
controls.backward = false;
controls.left = false;
controls.right = false;
}
- // Check if player is in front of an interactable object
var showInteract = false;
var interactableObject = null;
- // Priority 1: Check for Gate interaction
if (gate) {
- // Check if gate exists and is valid
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) {
- // Interaction range for gate
var angleToGate = Math.atan2(dy_g, dx_g);
- var angleDiff_g = Math.abs((player.dir - angleToGate + Math.PI * 3) % (Math.PI * 2) - Math.PI);
+ var angleDiff_g = Math.abs((player.dir - angleToGate + Math.PI * 3) % MATH_PI_2 - Math.PI);
if (angleDiff_g < Math.PI / 3) {
- // 60-degree cone
showInteract = true;
interactableObject = gate;
}
}
}
- // Priority 2: If no gate selected, check for treasures
if (!showInteract) {
for (var i = 0; i < treasures.length; i++) {
var t = treasures[i];
if (t.isOpen) {
@@ -3927,21 +3597,18 @@
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) {
- //{cN} // Interaction range
var angleToTreasure = Math.atan2(dy_t, dx_t);
- var angleDiff_t = Math.abs((player.dir - angleToTreasure + Math.PI * 3) % (Math.PI * 2) - Math.PI);
+ var angleDiff_t = Math.abs((player.dir - angleToTreasure + Math.PI * 3) % MATH_PI_2 - Math.PI);
if (angleDiff_t < Math.PI / 3) {
- // 60-degree cone
showInteract = true;
interactableObject = t;
break;
}
}
}
}
- // Priority 3: If nothing selected yet, check for power-ups
if (!showInteract) {
for (var i = 0; i < powerUps.length; i++) {
var p = powerUps[i];
if (p.collected) {
@@ -3950,21 +3617,18 @@
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) {
- // Same interaction range
var angleToPowerUp = Math.atan2(dy_p, dx_p);
- var angleDiff_p = Math.abs((player.dir - angleToPowerUp + Math.PI * 3) % (Math.PI * 2) - Math.PI);
+ var angleDiff_p = Math.abs((player.dir - angleToPowerUp + Math.PI * 3) % MATH_PI_2 - Math.PI);
if (angleDiff_p < Math.PI / 3) {
- // Same 60-degree cone
showInteract = true;
interactableObject = p;
break;
}
}
}
}
- // Priority 4: If nothing selected yet, check for Fountains
if (!showInteract) {
for (var i = 0; i < fountains.length; i++) {
var f = fountains[i];
if (!f.isInteractable || f.state !== 'full') {
@@ -3973,24 +3637,20 @@
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) {
- // Interaction range
var angleToFountain = Math.atan2(dy_f, dx_f);
- var angleDiff_f = Math.abs((player.dir - angleToFountain + Math.PI * 3) % (Math.PI * 2) - Math.PI);
+ var angleDiff_f = Math.abs((player.dir - angleToFountain + Math.PI * 3) % MATH_PI_2 - Math.PI);
if (angleDiff_f < Math.PI / 3) {
- // 60-degree cone
showInteract = true;
interactableObject = f;
break;
}
}
}
}
- // Update attack button's interact state and target
controlButtons.attack.isInteract = showInteract;
- controlButtons.attack.interactTarget = interactableObject; // Use the new generic property name
- // Update new interaction prompt
+ controlButtons.attack.interactTarget = interactableObject;
if (showInteract && interactableObject) {
var mainMessageStr = "";
var showParentheticalPrompt = true;
if (interactableObject instanceof Treasure && !interactableObject.isOpen) {
@@ -4006,65 +3666,56 @@
} else {
mainMessageStr = "Exit";
}
} else {
- // No specific message or conditions not met for interaction
- showInteract = false; // Force hide if no valid message determined
+ showInteract = false;
}
if (showInteract) {
- // Re-check in case conditions above invalidated it
interactionPromptMainText.setText(mainMessageStr);
interactionPromptParentheticalGroup.visible = showParentheticalPrompt;
interactionPromptContainer.visible = true;
if (showParentheticalPrompt) {
- // Layout for "(Press [img] )"
- var spacing = 10; // Spacing between elements in the group
+ 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;
- // Center the parenthetical group's content
var firstElementEffectiveX = interactionPromptPressOpenText.x - interactionPromptPressOpenText.anchor.x * interactionPromptPressOpenText.width;
var shiftToCenterGroup = -(firstElementEffectiveX + parentheticalGroupWidth / 2);
interactionPromptPressOpenText.x += shiftToCenterGroup;
interactionPromptButtonImage.x += shiftToCenterGroup;
interactionPromptPressCloseText.x += shiftToCenterGroup;
- // Layout MainText and ParentheticalGroup side-by-side, then center them combined
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; // Center main text if no parenthetical part
+ interactionPromptMainText.x = 0;
}
} else {
interactionPromptContainer.visible = false;
}
} else {
interactionPromptContainer.visible = false;
}
- // Read from attack/interact button
controls.attack = controlButtons.attack.pressed;
}
function getDistanceToNearestWall(x, y) {
var minDist = Infinity;
- // Check a larger area around the player
- var checkRadius = 2; // Check 2 cells in each direction
- var startX = Math.max(0, Math.floor(x) - checkRadius);
- var endX = Math.min(MAP_SIZE - 1, Math.floor(x) + checkRadius);
- var startY = Math.max(0, Math.floor(y) - checkRadius);
- var endY = Math.min(MAP_SIZE - 1, Math.floor(y) + checkRadius);
+ 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) {
- // This is a wall cell, calculate distance to the nearest edge of this cell
var wallLeft = cellX;
var wallRight = cellX + 1;
var wallTop = cellY;
var wallBottom = cellY + 1;
- // Calculate distance to the nearest edge of the wall cell
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);
@@ -4075,32 +3726,27 @@
}
function pushPlayerAwayFromWalls() {
var currentDist = getDistanceToNearestWall(player.x, player.y);
if (currentDist < WALL_BUFFER) {
- // Player is too close to a wall, find the direction away from the nearest wall
- var pushDistance = WALL_BUFFER - currentDist + 0.05; // Add small extra margin
- // Calculate the gradient (direction of steepest distance increase)
+ 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);
- // Normalize the gradient
var gradMag = Math.sqrt(gradX * gradX + gradY * gradY);
if (gradMag > 0) {
gradX /= gradMag;
gradY /= gradMag;
- // Move in the direction of the gradient
var newX = player.x + gradX * pushDistance;
var newY = player.y + gradY * pushDistance;
if (canMoveTo(newX, newY)) {
player.x = newX;
player.y = newY;
} else {
- // If gradient direction doesn't work, try radial push
- var angles = [0, Math.PI / 4, Math.PI / 2, 3 * Math.PI / 4, Math.PI, 5 * Math.PI / 4, 3 * Math.PI / 2, 7 * Math.PI / 4];
+ 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)) {
@@ -4113,99 +3759,81 @@
}
}
}
function canMoveTo(targetX, targetY) {
- var cellX = Math.floor(targetX);
- var cellY = Math.floor(targetY);
- // Check bounds: if target is outside map, cannot move.
+ 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;
}
- // Check if the target cell itself is a wall
if (map[cellY][cellX].type === 1) {
return false;
}
- // Prevent walking through treasures (if any treasure is in this cell and not open)
for (var i = 0; i < treasures.length; i++) {
var t = treasures[i];
- if (Math.floor(t.mapX) === cellX && Math.floor(t.mapY) === cellY && !t.isOpen) {
+ if (fastFloor(t.mapX) === cellX && fastFloor(t.mapY) === cellY && !t.isOpen) {
return false;
}
}
- // Check if the player would be too close to any wall
var distToWall = getDistanceToNearestWall(targetX, targetY);
return distToWall >= WALL_BUFFER;
}
function canMonsterMoveTo(targetX, targetY, monster) {
- var targetCellX = Math.floor(targetX);
- var targetCellY = Math.floor(targetY);
- // Check bounds: if target is outside map, cannot move.
+ 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;
}
- // Check if the target cell itself is a wall
if (map[targetCellY][targetCellX].type === 1) {
return false;
}
- // Check if another monster is already in the target cell
for (var i = 0; i < monsters.length; i++) {
var otherMonster = monsters[i];
- if (otherMonster !== monster && Math.floor(otherMonster.mapX) === targetCellX && Math.floor(otherMonster.mapY) === targetCellY) {
- return false; // Target cell occupied by another monster
+ if (otherMonster !== monster && fastFloor(otherMonster.mapX) === targetCellX && fastFloor(otherMonster.mapY) === targetCellY) {
+ return false;
}
}
- // Check if the monster would be too close to any wall using its own buffer
var distToActualWall = getDistanceToNearestWall(targetX, targetY);
return distToActualWall >= MONSTER_WALL_BUFFER;
}
function updateMonsterPosition(monster, newMapX, newMapY) {
- var oldCellX = Math.floor(monster.mapX);
- var oldCellY = Math.floor(monster.mapY);
- var newCellX = Math.floor(newMapX);
- var newCellY = Math.floor(newMapY);
- // Only update map cell occupancy if the monster is actually changing cells
+ 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]) {
- // Check if old cell is valid
map[oldCellY][oldCellX].removeMonster();
}
- // else: old cell was invalid or monster wasn't registered there.
if (map[newCellY] && map[newCellY][newCellX]) {
- // Check if new cell is valid
map[newCellY][newCellX].addMonster();
} else {
- // Fallback: If new cell is invalid (this implies an issue if canMonsterMoveTo passed)
- // Re-register in old cell if it was valid, and don't move.
if (map[oldCellY] && map[oldCellY][oldCellX]) {
map[oldCellY][oldCellX].addMonster();
}
- return; // Do not tween to an invalid position.
+ return;
}
}
- // Tween the monster's visual/logical position
tween(monster, {
mapX: newMapX,
mapY: newMapY
}, {
duration: 300,
- // 300ms smooth animation
easing: function easing(t) {
return 1 - Math.pow(1 - t, 4);
- } // quartOut easing
+ }
});
}
function updatePlayerMovement(deltaTime) {
var moveSpeed = PLAYER_MOVE_SPEED * deltaTime;
var turnSpeed = PLAYER_TURN_SPEED * deltaTime;
var didMove = false;
- // Track player's last position to detect state changes
if (player.lastX === undefined) {
player.lastX = player.x;
}
if (player.lastY === undefined) {
player.lastY = player.y;
}
- // Get joystick values for analog control
var joystick = controlButtons.joystick;
var turnAmount = 0;
var moveAmount = 0;
var currentJoystickX, currentJoystickY, isJoystickConsideredActiveForMovement;
@@ -4221,57 +3849,48 @@
currentJoystickX = 0;
currentJoystickY = 0;
isJoystickConsideredActiveForMovement = false;
}
- // Handle rotation - use x-axis for turning
if (isJoystickConsideredActiveForMovement) {
var joystickMagnitudeX = Math.abs(currentJoystickX);
- var rampedTurnValue = 0; // Initialize to 0, will be 0 if in dead zone
- var JOYSTICK_TURN_DEAD_ZONE = 0.15; // Threshold below which joystick input for turning is minimal/zero
- var JOYSTICK_TURN_RAMP_EXPONENT = 2.5; // Higher value means more ramping towards the edges
+ var rampedTurnValue = 0;
+ var JOYSTICK_TURN_DEAD_ZONE = 0.15;
+ var JOYSTICK_TURN_RAMP_EXPONENT = 2.5;
if (joystickMagnitudeX > JOYSTICK_TURN_DEAD_ZONE) {
- // Remap the input from [JOYSTICK_TURN_DEAD_ZONE, 1.0] to [0.0, 1.0]
var normalizedInputX = (joystickMagnitudeX - JOYSTICK_TURN_DEAD_ZONE) / (1.0 - JOYSTICK_TURN_DEAD_ZONE);
- // Apply ramping (power curve)
rampedTurnValue = Math.pow(normalizedInputX, JOYSTICK_TURN_RAMP_EXPONENT);
}
var rampedTurnAmount = Math.sign(currentJoystickX) * rampedTurnValue;
- turnAmount = rampedTurnAmount * turnSpeed * 1.4; // Retain existing overall speed multiplier
+ turnAmount = rampedTurnAmount * turnSpeed * 1.4;
player.dir += turnAmount;
while (player.dir < 0) {
- player.dir += Math.PI * 2;
+ player.dir += MATH_PI_2;
}
- while (player.dir >= Math.PI * 2) {
- player.dir -= Math.PI * 2;
+ while (player.dir >= MATH_PI_2) {
+ player.dir -= MATH_PI_2;
}
- }
- // Also support digital controls for rotation
- else if (controls.left) {
+ } else if (controls.left) {
player.dir -= turnSpeed;
while (player.dir < 0) {
- player.dir += Math.PI * 2;
+ player.dir += MATH_PI_2;
}
} else if (controls.right) {
player.dir += turnSpeed;
- while (player.dir >= Math.PI * 2) {
- player.dir -= Math.PI * 2;
+ while (player.dir >= MATH_PI_2) {
+ player.dir -= MATH_PI_2;
}
}
- // Calculate movement vector
var dx = 0,
dy = 0;
- // Handle movement - use y-axis for forward/backward
var actualMoveSpeed = moveSpeed * player.moveSpeedMultiplier;
if (isJoystickConsideredActiveForMovement) {
- moveAmount = -currentJoystickY * actualMoveSpeed; // Negative because up is negative y
+ moveAmount = -currentJoystickY * actualMoveSpeed;
if (Math.abs(moveAmount) > 0.01) {
dx += Math.cos(player.dir) * moveAmount;
dy += Math.sin(player.dir) * moveAmount;
didMove = true;
}
- }
- // Also support digital controls for movement
- else if (controls.forward) {
+ } else if (controls.forward) {
dx += Math.cos(player.dir) * actualMoveSpeed;
dy += Math.sin(player.dir) * actualMoveSpeed;
didMove = true;
} else if (controls.backward) {
@@ -4280,48 +3899,39 @@
didMove = true;
}
var inputMoveIntent = didMove;
didMove = false;
- // Enhanced collision detection with proper wall sliding
if (dx !== 0 || dy !== 0) {
var currentX = player.x;
var currentY = player.y;
- // Try moving to the exact target position first
var targetX = currentX + dx;
var targetY = currentY + dy;
if (canMoveTo(targetX, targetY)) {
- // No collision, move normally
player.x = targetX;
player.y = targetY;
didMove = true;
} else {
- // Collision detected, try wall sliding
var moved = false;
- // Try moving only along X axis
if (dx !== 0 && canMoveTo(currentX + dx, currentY)) {
player.x = currentX + dx;
moved = true;
}
- // Try moving only along Y axis
if (dy !== 0 && canMoveTo(currentX, currentY + dy)) {
player.y = currentY + dy;
moved = true;
}
- // If basic sliding failed, try gradual movement
if (!moved) {
var maxSteps = 10;
for (var step = 1; step <= maxSteps; step++) {
var fraction = step / maxSteps;
- // Try X movement with varying intensity
if (dx !== 0) {
var testX = currentX + dx * fraction;
if (canMoveTo(testX, currentY)) {
player.x = testX;
moved = true;
break;
}
}
- // Try Y movement with varying intensity
if (dy !== 0) {
var testY = currentY + dy * fraction;
if (canMoveTo(currentX, testY)) {
player.y = testY;
@@ -4333,31 +3943,19 @@
}
didMove = moved;
}
}
- // Always ensure player maintains proper distance from walls
pushPlayerAwayFromWalls();
- // Play walk sound if there was input intent AND actual movement occurred
if (didMove && inputMoveIntent && LK.ticks % 20 === 0) {
LK.getSound('walk').play();
}
- // Only attempt to attack if attack button is pressed and we can attack
- // Also, ensure the attack button is not currently being used for interaction.
- // Only attempt to attack if attack button is pressed and we can attack
- // Also, ensure the attack button is not currently being used for interaction.
if (controls.attack && canAttack && !controlButtons.attack.isInteract) {
attackAction();
}
- // Check for collisions with monsters
checkMonsterCollisions();
- // Check for collisions with treasures
checkTreasureCollisions();
- // Gate collision is now handled by the interact button
- // Check for collisions with power-ups
checkPowerUpCollisions();
- // Update player marker on minimap
updateMiniMap();
- // Update player's last position
player.lastX = player.x;
player.lastY = player.y;
}
function checkPowerUpCollisions() {
@@ -4368,32 +3966,23 @@
}
var dx = powerUp.mapX - player.x;
var dy = powerUp.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
- if (dist < 0.6) {
- // Power-ups are no longer collected by collision.
- // This block is intentionally left empty or can be removed if no other proximity logic is needed.
- // For now, we ensure no collection happens here.
- }
+ if (dist < 0.6) {}
}
}
function attackAction() {
- // Check if attack is on cooldown
if (!canAttack) {
return;
}
- // Set attack on cooldown
canAttack = false;
var currentAttackCooldown = attackCooldown;
if (player.attackBuffActive) {
- currentAttackCooldown /= 2; // Halve cooldown if attack buff is active
+ currentAttackCooldown /= 2;
}
- // Create and fire projectile
var projectile = new Projectile();
projectile.fire(globalRightHand.x, globalRightHand.y - 400);
- // Play player projectile sound effect
LK.getSound('playerprojectile').play();
- // Add hand recoil animation
tween(globalRightHand, {
y: 2732 - 50,
rotation: -0.1
}, {
@@ -4406,135 +3995,103 @@
duration: 300
});
}
});
- // Add to projectile layer
projectiles.push(projectile);
projectileLayer.addChild(projectile);
- // Update attack button visual
var attackButton = controlButtons.attack;
attackButton.updateCooldown(0);
- // Cooldown animation
var _cooldownTick = function cooldownTick(progress) {
attackButton.updateCooldown(progress);
if (progress < 1) {
LK.setTimeout(function () {
_cooldownTick(progress + 0.05);
- }, currentAttackCooldown / 20); // Use currentAttackCooldown
+ }, currentAttackCooldown / 20);
}
};
_cooldownTick(0);
- // Reset cooldown after specified time
LK.setTimeout(function () {
canAttack = true;
- }, currentAttackCooldown); // Use currentAttackCooldown
+ }, currentAttackCooldown);
}
function updateProjectiles(deltaTime) {
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
- // Update projectile position
var remove = projectile.update(deltaTime);
- // Check if projectile has gone off screen or should be removed
if (remove) {
- // Remove projectile
projectile.destroy();
projectiles.splice(i, 1);
continue;
}
- // NEW: Player collision check for FireballProjectiles
if (!remove && projectile instanceof FireballProjectile) {
var dx_player_fireball = projectile.worldX - player.x;
var dy_player_fireball = projectile.worldY - player.y;
- // Assuming player's collision center is at Z=0.5
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; // Slightly more generous for player hit by fireball
+ var PLAYER_HITBOX_RADIUS_FOR_FIREBALL = 0.45;
if (dist_player_sq_fireball < PLAYER_HITBOX_RADIUS_FOR_FIREBALL * PLAYER_HITBOX_RADIUS_FOR_FIREBALL) {
- // Player hit by fireball
var baseFireballDamage = projectile.damage;
var damageReductionFireball = player.defense * 0.5;
- var damageTakenByFireball = Math.max(0.5, baseFireballDamage - damageReductionFireball); // Apply defense, minimum 0.5 damage
+ var damageTakenByFireball = Math.max(0.5, baseFireballDamage - damageReductionFireball);
player.health -= damageTakenByFireball;
updateUI();
- LK.effects.flashScreen(0xffaa00, 200); // Orange flash for fireball hit
+ LK.effects.flashScreen(0xffaa00, 200);
LK.getSound('hit').play();
- createParticleExplosion(projectile.worldX, projectile.worldY, 15, 0xFF8C00, false); // Fire-like particles
+ createParticleExplosion(projectile.worldX, projectile.worldY, 15, 0xFF8C00, false);
if (player.health <= 0) {
LK.showGameOver();
}
- // Projectile is consumed
projectile.destroy();
projectiles.splice(i, 1);
- continue; // Skip to next projectile in the main loop
+ continue;
}
}
- // END NEW Player collision check
- // Check if projectile hits a monster (This is for player's projectiles hitting monsters)
var hitMonster = false;
var hitMonsterIndex = -1;
for (var j = 0; j < monsters.length; j++) {
var monster = monsters[j];
if (monster.visible) {
- // Get the vector from player to monster in world space
var monsterDx = monster.mapX - player.x;
var monsterDy = monster.mapY - player.y;
- // Calculate distance to monster in world space
var monsterDist = Math.sqrt(monsterDx * monsterDx + monsterDy * monsterDy);
- // Calculate the projectile's travel distance in world units
var projectileWorldDist = projectile.distance;
- // Calculate depth difference: projectile must be at a similar depth to the monster.
var distanceDifference = Math.abs(projectileWorldDist - monsterDist);
- var depthHit = distanceDifference < 0.5; // Projectile within 0.5 world units of monster's depth.
- // Calculate horizontal collision based on monster's apparent width.
- // monster.width is its current scaled width on screen (approximately WALL_HEIGHT_FACTOR / monsterDist).
+ var depthHit = distanceDifference < 0.5;
var horizontalDifference = Math.abs(projectile.x - monster.x);
- // The collision zone is a factor of the monster's full apparent width.
- // A factor of 0.6 means the projectile's center needs to be within 0.6 * monster.width of the monster's screen center X.
- // If monster.width is its visual width (e.g. 100px), this means horizontalDifference < 60px.
- // The monster's visual half-width is monster.width / 2 (e.g. 50px).
- // So, this allows the projectile's center to be (0.6 - 0.5) = 0.1 * monster.width (e.g. 10px) outside the visual edge.
- var COLLISION_WIDTH_FACTOR = 0.6; // Adjust this for more/less generous horizontal collision.
+ var COLLISION_WIDTH_FACTOR = 0.6;
var horizontalHit = horizontalDifference < monster.width * COLLISION_WIDTH_FACTOR;
- // Hit if projectile is close enough in depth AND horizontally aligned considering monster's perceived width.
if (depthHit && horizontalHit) {
- // NEW: Check if this projectile has already hit this specific monster
if (projectile.hitMonsters.indexOf(monster) !== -1) {
- continue; // Already hit this monster, check the next one
+ continue;
}
- // If not, this is a new monster for this projectile. Record the hit.
projectile.hitMonsters.push(monster);
hitMonster = true;
hitMonsterIndex = j;
- break; //{i2} // Found a new monster to hit, break from monster loop
+ break;
}
}
}
- // Handle monster hit logic (unchanged)
if (hitMonster && hitMonsterIndex !== -1) {
var monster = monsters[hitMonsterIndex];
- var killed = monster.takeDamage(player.attackPower); // Use player.attackPower
- // Create a small red particle explosion at the monster's hit location
- createParticleExplosion(monster.mapX, monster.mapY, 8, 0xFF0000, false); // Red Tint, 8 particles, isWallHit = false
+ var killed = monster.takeDamage(player.attackPower);
+ createParticleExplosion(monster.mapX, monster.mapY, 8, 0xFF0000, false);
if (killed) {
- // Create larger particle explosion for monster death at monster's world position
- var explosionParticleCount = 150; // Default for Monster (Ogre) and BossMonster
- var explosionTintColor = 0xFF0000; // Default red
+ var explosionParticleCount = 150;
+ var explosionTintColor = 0xFF0000;
if (monster instanceof ImpMonster) {
- explosionParticleCount = 75; // Half particles for Imp
+ explosionParticleCount = 75;
} else if (monster instanceof EyeballMonster) {
- explosionParticleCount = 112; // Between Imp (75) and Ogre/Boss (150)
+ explosionParticleCount = 112;
}
- // Monster (Ogre) and BossMonster will use the default 150 particles.
- createParticleExplosion(monster.mapX, monster.mapY, explosionParticleCount, explosionTintColor, false); // isWallHit = false
- // Play enemy death sounds
+ createParticleExplosion(monster.mapX, monster.mapY, explosionParticleCount, explosionTintColor, false);
LK.getSound('enemyexplosion').play();
LK.setTimeout(function () {
LK.getSound('pixeldrop').play();
- }, 250); // Play pixeldrop 250ms after explosion starts
- map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster();
+ }, 250);
+ map[fastFloor(monster.mapY)][fastFloor(monster.mapX)].removeMonster();
monster.destroy();
monsters.splice(hitMonsterIndex, 1);
- player.score += 10;
+ // player.score += 10; // Score removed
updateUI();
checkLevelCompletion();
}
projectile.destroy();
@@ -4548,36 +4105,27 @@
var monster = monsters[i];
var dx = monster.mapX - player.x;
var dy = monster.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
- // Determine collision distance based on monster type
- var collisionDistance = 0.5; // Default
+ var collisionDistance = 0.5;
if (monster instanceof ImpMonster) {
- collisionDistance = 0.35; // Imps need to be closer to actually hit the player
+ collisionDistance = 0.35;
}
if (dist < collisionDistance && monster.canAttack) {
- // Player hit by monster
- var baseDamage = monster.baseAttack || 1; // Use monster's baseAttack, fallback to 1 if undefined
- // BossMonster damage is handled by its fireball projectile, not direct collision here.
+ var baseDamage = monster.baseAttack || 1;
var damageReduction = player.defense * 0.5;
- var damageTaken = Math.max(0.5, baseDamage - damageReduction); // Apply defense, minimum 0.5 damage
+ var damageTaken = Math.max(0.5, baseDamage - damageReduction);
player.health -= damageTaken;
updateUI();
- // Visual feedback
LK.effects.flashScreen(0xff0000, 300);
- // Play player hurt sound
LK.getSound('playerhurt').play();
- // Push player back slightly
player.x -= dx * 0.3;
player.y -= dy * 0.3;
- // Set monster attack on cooldown
monster.canAttack = false;
monster.lastAttackTime = currentTime;
- // Reset monster attack after cooldown
LK.setTimeout(function () {
monster.canAttack = true;
}, monster.attackCooldown);
- // Visual feedback for monster attack
tween(monster, {
alpha: 0.5
}, {
duration: 200,
@@ -4588,9 +4136,8 @@
duration: 200
});
}
});
- // Check game over
if (player.health <= 0) {
LK.showGameOver();
}
break;
@@ -4602,18 +4149,12 @@
var treasure = treasures[i];
var dx = treasure.mapX - player.x;
var dy = treasure.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
- // No auto-collect, handled by interact button
}
}
-// function checkGateCollision() { ... } // This function is now removed.
-// Its logic has been integrated into ControlButton.down
function checkLevelCompletion() {
- // Level is only considered "complete" if all monsters are defeated
- // This allows the gate to activate
if (monsters.length === 0) {
- // If gate is not visible, make it pulse more dramatically to draw attention
if (gate) {
tween(gate, {
alpha: 0.2
}, {
@@ -4625,155 +4166,99 @@
duration: 300
});
}
});
- // Show hint text
var gateHintText = new Text2('Find the exit gate!', {
size: 60,
fill: 0x00FF55
});
gateHintText.anchor.set(0.5, 0.5);
- gateHintText.x = 2048 / 2;
+ gateHintText.x = SCREEN_WIDTH_HALF;
gateHintText.y = 200;
gameLayer.addChild(gateHintText);
- // Remove hint after a few seconds
LK.setTimeout(function () {
gateHintText.destroy();
}, 3000);
}
} else {
- // Show hint text about defeating monsters first
if (gate && gate.visible && LK.ticks % 300 === 0) {
var monsterHintText = new Text2('Defeat all monsters to activate the exit!', {
size: 60,
fill: 0xFF5555
});
monsterHintText.anchor.set(0.5, 0.5);
- monsterHintText.x = 2048 / 2;
+ monsterHintText.x = SCREEN_WIDTH_HALF;
monsterHintText.y = 200;
gameLayer.addChild(monsterHintText);
- // Remove hint after a few seconds
LK.setTimeout(function () {
monsterHintText.destroy();
}, 3000);
}
}
}
function updateMiniMap() {
- // Update player marker position on minimap
playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE;
playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE;
- // Draw player direction indicator
var dirX = Math.cos(player.dir) * 15;
var dirY = Math.sin(player.dir) * 15;
}
-// Game update method
game.update = function () {
if (gameState === 'title') {
- // Title screen logic (if any animation or interaction beyond button press is needed)
return;
}
var currentTime = Date.now();
var deltaTime = currentTime - lastTime;
lastTime = currentTime;
- // Update controls
updateControls();
- // Update player
updatePlayerMovement(deltaTime);
- // --- Rendering Phase ---
- renderableObjects = []; // Clear the list for the current frame
- rayCasting(); // Populates renderableObjects with wall strips
- renderEntities(); // Populates renderableObjects with dynamic entities
- // Sort all collected objects by distance (farthest first)
- // This ensures correct Z-ordering.
- renderableObjects.sort(function (a, b) {
- if (a.distance !== b.distance) {
- return b.distance - a.distance; // Farthest objects (larger distance) come first in the array
- }
- // Tie-breaking for objects at the exact same distance:
- // Render walls on top of entities if distances are equal.
- // This means entities should come earlier in the sorted list (rendered first).
- if (a.type === 'entity' && b.type === 'wall') {
- return -1; // Entity (a) comes before wall (b)
- }
- if (a.type === 'wall' && b.type === 'entity') {
- return 1; // Wall (a) comes after entity (b)
- }
- // For two walls at the same distance, maintain their original screen order (left to right)
- if (a.type === 'wall' && b.type === 'wall') {
- return a.originalSortOrder - b.originalSortOrder;
- }
- // For two entities at same distance, or other unhandled ties, default sort behavior (often stable)
- return 0;
- });
- // Clear the main container for 3D scene elements and add sorted objects
- dynamicEntitiesContainer.removeChildren();
- for (var i = 0; i < renderableObjects.length; i++) {
- var ro = renderableObjects[i];
- // Only add the object if it's marked as visible.
- // Note: ro.object.visible might have been set to false by rayCasting (for empty strips)
- // or renderEntities (for occluded/off-FOV entities).
- if (ro.object.visible) {
- dynamicEntitiesContainer.addChild(ro.object);
- }
- }
- // --- End of Rendering Phase ---
- // Update monsters AI and animations
- // Monster visibility (ro.object.visible) is now correctly set by renderEntities before this logic.
+ renderableObjects = [];
+ rayCasting();
+ renderEntities();
+ EntityRenderer.updateEntities(renderableObjects);
if (LK.ticks % 15 === 0) {
- // Monster movement less frequent
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];
- // Check if monster is visible (set by renderEntities) and has moved
if (monster.visible && (monster.mapX !== monster.lastMoveX || monster.mapY !== monster.lastMoveY)) {
monster.updateAnimation();
- // Store current position for next comparison
monster.lastMoveX = monster.mapX;
monster.lastMoveY = monster.mapY;
}
- // Update Imp sound behavior
if (monster instanceof ImpMonster) {
monster.updateSoundBehavior();
}
if (monster instanceof EyeballMonster) {
monster.updateSoundBehavior();
}
- // Update Ogre (Monster class) sound behavior
if (monster instanceof Monster) {
if (typeof monster.updateSoundBehavior === 'function') {
monster.updateSoundBehavior();
}
}
}
- // Update fountain sounds
for (var i = 0; i < fountains.length; i++) {
var fountainInstance = fountains[i];
if (fountainInstance.updateSoundBehavior) {
fountainInstance.updateSoundBehavior();
}
}
- // Update projectiles
updateProjectiles(deltaTime);
- // Update fountain particles
for (var i = 0; i < fountains.length; i++) {
if (fountains[i].updateParticles) {
fountains[i].updateParticles();
}
}
- // Update particle explosions
particlePoolManager.updateActiveParticles();
+ MemoryManager.performCleanup();
};
-// Global function to display power-up effect text messages
-var powerUpEffectTextDisplaying = null; // To manage one text at a time
+var powerUpEffectTextDisplaying = null;
function showPowerUpEffectText(message) {
- // If a text is already displaying, remove it first
if (powerUpEffectTextDisplaying && powerUpEffectTextDisplaying.parent) {
- tween.stop(powerUpEffectTextDisplaying); // Stop any ongoing tween
- tween.stop(powerUpEffectTextDisplaying); // Stop any ongoing tween
+ tween.stop(powerUpEffectTextDisplaying);
+ tween.stop(powerUpEffectTextDisplaying);
powerUpEffectTextDisplaying.parent.removeChild(powerUpEffectTextDisplaying);
powerUpEffectTextDisplaying.destroy();
powerUpEffectTextDisplaying = null;
}
@@ -4786,21 +4271,19 @@
wordWrap: true,
wordWrapWidth: 1800
});
effectText.anchor.set(0.5, 0.5);
- effectText.x = 2048 / 2;
- effectText.y = 2732 / 2; // Positioned higher on the screen
+ effectText.x = SCREEN_WIDTH_HALF;
+ effectText.y = SCREEN_HEIGHT_HALF;
effectText.alpha = 1.0;
- gameLayer.addChild(effectText); // Assuming gameLayer is accessible globally
+ gameLayer.addChild(effectText);
powerUpEffectTextDisplaying = effectText;
LK.setTimeout(function () {
if (effectText && effectText.parent) {
- // Check if still valid
tween(effectText, {
alpha: 0
}, {
duration: 500,
- // Fade out over 0.5 seconds
easing: tween.linear,
onFinish: function onFinish() {
if (effectText.parent) {
effectText.parent.removeChild(effectText);
@@ -4811,44 +4294,33 @@
}
}
});
}
- }, 2500); // Start fade-out after 2.5 seconds
+ }, 2500);
}
-// Game initialization
setupGame();
-// Event handlers
game.down = function (x, y, obj) {
if (gameState === 'title') {
- // Let title screen elements (like start button) handle their own down events.
- // The game-wide down handler should not interfere.
return;
}
- activeControlForGlobalHandlers = null; // Reset at the start of a new touch
- var eventObjForAttack = Object.assign({}, obj); // Fresh event object for attack button
+ activeControlForGlobalHandlers = null;
+ var eventObjForAttack = Object.assign({}, obj);
eventObjForAttack.stopPropagation = false;
var attackBtn = controlButtons.attack;
var attackBtnLocalX = x - attackBtn.x;
var attackBtnLocalY = y - attackBtn.y;
- // Use the same generous hit radius as in the original code
var attackBtnHitRadius = 150 * 3.5;
var attackBtnDistSq = attackBtnLocalX * attackBtnLocalX + attackBtnLocalY * attackBtnLocalY;
if (attackBtnDistSq <= attackBtnHitRadius * attackBtnHitRadius) {
activeControlForGlobalHandlers = attackBtn;
attackBtn.down(attackBtnLocalX, attackBtnLocalY, eventObjForAttack);
- // If the touch is within the attack button's area, it's considered handled by the attack button,
- // regardless of whether it internally decided to stop propagation (e.g. attack on cooldown).
- // This prevents the joystick from also processing this touch.
return;
}
- // If we reach here, the touch was NOT on the attack button.
- // Now check for joystick interaction using original quadrant check.
var joystick = controlButtons.joystick;
if (x < 800 && y > 2732 - 800) {
activeControlForGlobalHandlers = joystick;
- var eventObjForJoystick = Object.assign({}, obj); // Fresh event object for joystick
+ var eventObjForJoystick = Object.assign({}, obj);
eventObjForJoystick.stopPropagation = false;
- // Pass local coordinates to joystick.down
joystick.down(x - joystick.x, y - joystick.y, eventObjForJoystick);
}
};
game.up = function (x, y, obj) {
@@ -4859,115 +4331,93 @@
var attackBtnLocalX = x - controlButtons.attack.x;
var attackBtnLocalY = y - controlButtons.attack.y;
controlButtons.attack.up(attackBtnLocalX, attackBtnLocalY, obj);
} else if (activeControlForGlobalHandlers === controlButtons.joystick) {
- // Only call joystick.up if the joystick itself believes it's active
if (controlButtons.joystick.active) {
var joystickLocalX = x - controlButtons.joystick.x;
var joystickLocalY = y - controlButtons.joystick.y;
controlButtons.joystick.up(joystickLocalX, joystickLocalY, obj);
}
}
- activeControlForGlobalHandlers = null; // Reset after touch ends
+ activeControlForGlobalHandlers = null;
};
game.move = function (x, y, obj) {
if (gameState === 'title') {
return;
}
- // Handle general screen move
if (activeControlForGlobalHandlers === controlButtons.joystick) {
- // Only call joystick.move if the joystick itself believes it's active
if (controlButtons.joystick.active) {
var joystickLocalX = x - controlButtons.joystick.x;
var joystickLocalY = y - controlButtons.joystick.y;
controlButtons.joystick.move(joystickLocalX, joystickLocalY, obj);
}
}
- // Attack button doesn't typically have a .move action, so no changes needed for it here.
};
var MonsterAI = {
moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) {
var monsterX = monster.mapX;
var monsterY = monster.mapY;
var dxToPlayer = playerX - monsterX;
var dyToPlayer = playerY - monsterY;
var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer);
- // Update monster's perception of player (line of sight)
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 MONSTER SPECIFIC AI
if (monster instanceof BossMonster) {
var effectiveMoveSpeedBoss = typeof monster.moveSpeed !== 'undefined' ? monster.moveSpeed : 0.15;
var moveDirXBoss = 0;
var moveDirYBoss = 0;
- // BOSS CHASE LOGIC - more aggressive than regular monsters
var bossShouldChase = false;
if (distToPlayer <= MONSTER_AGGRO_RANGE * 1.5) {
- // Boss has longer aggro range
- bossShouldChase = true; // Boss always chases within extended range
+ bossShouldChase = true;
}
- // FIREBALL SHOOTING - with better error handling
if (monster.canSeePlayer && distToPlayer <= MONSTER_AGGRO_RANGE) {
- // Reset canShootFireball if it's been too long (failsafe)
if (!monster.canShootFireball && currentTimeForAI - monster.lastFireballTime > monster.fireballCooldown + 1000) {
- console.log("Boss fireball cooldown reset (failsafe)");
monster.canShootFireball = true;
}
if (monster.canShootFireball) {
monster.performFireballAttack(playerX, playerY);
}
}
- // BOSS MOVEMENT LOGIC
if (!bossShouldChase) {
- return; // Boss not chasing
+ return;
}
if (distToPlayer < BOSS_TOO_CLOSE_DISTANCE) {
- // Back up
if (distToPlayer > 0.1) {
moveDirXBoss = -dxToPlayer / distToPlayer;
moveDirYBoss = -dyToPlayer / distToPlayer;
}
} else if (distToPlayer > BOSS_PREFERRED_DISTANCE) {
- // Move closer
if (distToPlayer > 0.1) {
moveDirXBoss = dxToPlayer / distToPlayer;
moveDirYBoss = dyToPlayer / distToPlayer;
}
} else {
- // Strafe - but only if we can see the player
if (monster.canSeePlayer) {
- var perpendicularAngle = Math.atan2(dyToPlayer, dxToPlayer) + (Math.random() > 0.5 ? Math.PI / 2 : -Math.PI / 2);
+ 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 {
- // If we can't see player, move toward last known position
moveDirXBoss = dxToPlayer / distToPlayer;
moveDirYBoss = dyToPlayer / distToPlayer;
}
}
- // BOSS MOVEMENT EXECUTION with comprehensive fallbacks
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 {
- // Try X-only movement
if (moveDirXBoss !== 0 && canMonsterMoveTo(monsterX + moveDirXBoss * effectiveMoveSpeedBoss, monsterY, monster)) {
updateMonsterPosition(monster, monsterX + moveDirXBoss * effectiveMoveSpeedBoss, monsterY);
- }
- // Try Y-only movement
- else if (moveDirYBoss !== 0 && canMonsterMoveTo(monsterX, monsterY + moveDirYBoss * effectiveMoveSpeedBoss, monster)) {
+ } else if (moveDirYBoss !== 0 && canMonsterMoveTo(monsterX, monsterY + moveDirYBoss * effectiveMoveSpeedBoss, monster)) {
updateMonsterPosition(monster, monsterX, monsterY + moveDirYBoss * effectiveMoveSpeedBoss);
- }
- // Try alternative directions
- else {
- var alternativeAngles = [0, Math.PI / 4, Math.PI / 2, 3 * Math.PI / 4, Math.PI, 5 * Math.PI / 4, 3 * Math.PI / 2, 7 * Math.PI / 4];
+ } 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)) {
@@ -4977,11 +4427,10 @@
}
}
}
}
- return; // Boss AI complete
+ return;
}
- // REGULAR MONSTER AI (unchanged)
var shouldChase = false;
if (distToPlayer <= MONSTER_AGGRO_RANGE) {
if (monster.canSeePlayer) {
shouldChase = true;
@@ -5036,29 +4485,25 @@
}
}
};
function ensureMapConnectivity() {
- // Flood fill from player starting position to check accessibility
var visited = [];
for (var y = 0; y < MAP_SIZE; y++) {
visited[y] = [];
for (var x = 0; x < MAP_SIZE; x++) {
visited[y][x] = false;
}
}
var queue = [];
- // Start from player position (1,1)
queue.push({
x: 1,
y: 1
});
visited[1][1] = true;
- // Perform flood fill
while (queue.length > 0) {
var current = queue.shift();
var x = current.x;
var y = current.y;
- // Check all four neighbors
var neighbors = [{
x: x + 1,
y: y
}, {
@@ -5073,9 +4518,8 @@
}];
for (var i = 0; i < neighbors.length; i++) {
var nx = neighbors[i].x;
var ny = neighbors[i].y;
- // Check if neighbor is valid and not visited
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) {
visited[ny][nx] = true;
queue.push({
x: nx,
@@ -5083,14 +4527,11 @@
});
}
}
}
- // Check for unreachable areas and create paths to them
for (var y = 1; y < MAP_SIZE - 1; y++) {
for (var x = 1; x < MAP_SIZE - 1; x++) {
- // If it's a floor tile but wasn't visited, it's unreachable
if (map[y][x].type === 0 && !visited[y][x]) {
- // Find the nearest accessible cell
var nearestX = -1;
var nearestY = -1;
var minDist = MAP_SIZE * MAP_SIZE;
for (var cy = 1; cy < MAP_SIZE - 1; cy++) {
@@ -5104,12 +4545,10 @@
}
}
}
}
- // Create a path from the unreachable area to the nearest accessible area
if (nearestX !== -1) {
createPath(x, y, nearestX, nearestY);
- // Update visited map by doing a mini flood fill from this newly connected point
var pathQueue = [{
x: x,
y: y
}];
@@ -5148,46 +4587,38 @@
}
}
}
function createPath(startX, startY, endX, endY) {
- // Ensure path doesn't go through outer walls
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));
- // Determine direction (horizontal or vertical first)
if (Math.random() < 0.5) {
- // Horizontal first, then vertical
var x = startX;
while (x !== endX) {
x += x < endX ? 1 : -1;
- // Ensure we don't carve through outer walls
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;
- // Ensure we don't carve through outer walls
if (y >= 1 && y < MAP_SIZE - 1 && map[y][endX].type === 1) {
map[y][endX].setType(0);
}
}
} else {
- // Vertical first, then horizontal
var y = startY;
while (y !== endY) {
y += y < endY ? 1 : -1;
- // Ensure we don't carve through outer walls
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;
- // Ensure we don't carve through outer walls
if (x >= 1 && x < MAP_SIZE - 1 && map[endY][x].type === 1) {
map[endY][x].setType(0);
}
}
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