/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BoostButton = Container.expand(function () {
var self = Container.call(this);
var buttonSprite = self.attachAsset('attackButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
// Change tint to blue to differentiate from attack button
buttonSprite.tint = 0x3366ff;
// Add text to indicate boost function
var boostText = new Text2('BOOST', {
size: 40,
fill: 0xFFFFFF
});
boostText.anchor.set(0.5, 0.5);
self.addChild(boostText);
self.pressed = false;
self.active = false;
self.cooldown = false;
self.down = function (x, y, obj) {
if (self.cooldown) return;
self.pressed = true;
buttonSprite.alpha = 0.7;
self.active = true;
// Apply boost effect
PLAYER_MOVE_SPEED *= 3;
};
self.up = function (x, y, obj) {
self.pressed = false;
if (self.active) {
// Reset speed to normal
PLAYER_MOVE_SPEED = PLAYER_MOVE_SPEED / 3;
self.active = false;
// Set cooldown
self.cooldown = true;
buttonSprite.alpha = 0.3;
// Reset cooldown after 5 seconds
LK.setTimeout(function () {
self.cooldown = false;
buttonSprite.alpha = 1;
}, 5000);
}
};
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
});
// Set arrow direction based on button type
if (direction === 'up') {
arrowSprite.rotation = 0;
} else if (direction === 'right') {
arrowSprite.rotation = Math.PI / 2;
} else if (direction === 'down') {
arrowSprite.rotation = Math.PI;
} else if (direction === 'left') {
arrowSprite.rotation = Math.PI * 1.5;
} else if (direction === 'attack') {
arrowSprite.visible = false;
buttonSprite = self.attachAsset('attackButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.5,
scaleY: 3.5
});
}
self.direction = direction;
self.pressed = false;
self.updateCooldown = function (ratio) {
if (self.direction === 'attack') {
// Update the button alpha based on cooldown ratio (0 to 1)
buttonSprite.alpha = 0.3 + ratio * 0.7;
}
};
self.down = function (x, y, obj) {
self.pressed = true;
buttonSprite.alpha = 0.7;
if (self.direction === 'attack' && canAttack) {
// Trigger attack immediately when the attack button is pressed
attackAction();
}
};
self.up = function (x, y, obj) {
self.pressed = false;
if (self.direction === 'attack') {
if (!canAttack) {
buttonSprite.alpha = 0.3; // Show as disabled
} else {
buttonSprite.alpha = 1;
}
} else {
buttonSprite.alpha = 1;
}
};
return self;
});
var Gate = Container.expand(function () {
var self = Container.call(this);
// Create gate visual using existing wall asset but with a different color
var gateSprite = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
// Make the gate distinct with a green tint
gateSprite.tint = 0x00FF00;
self.mapX = 0;
self.mapY = 0;
// Pulse animation to make gate more visible
var _animateGate = function animateGate() {
tween(gateSprite, {
alpha: 0.7,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
onFinish: function onFinish() {
tween(gateSprite, {
alpha: 1,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
onFinish: _animateGate
});
}
});
};
// Start the pulsing animation
_animateGate();
return self;
});
var MapCell = Container.expand(function () {
var self = Container.call(this);
self.type = 0; // 0 = floor, 1 = wall
self.monster = null;
self.treasure = null;
self.gate = null;
self.setType = function (type) {
self.type = type;
self.updateVisual();
};
self.updateVisual = function () {
self.removeChildren();
if (self.type === 1) {
self.attachAsset('mapWall', {
anchorX: 0,
anchorY: 0
});
} else {
self.attachAsset('mapFloor', {
anchorX: 0,
anchorY: 0
});
}
};
self.addMonster = function () {
if (self.type === 0 && !self.monster && !self.treasure) {
self.monster = true;
return true;
}
return false;
};
self.addTreasure = function () {
if (self.type === 0 && !self.monster && !self.treasure) {
self.treasure = true;
return true;
}
return false;
};
self.removeMonster = function () {
self.monster = null;
};
self.removeTreasure = function () {
self.treasure = null;
};
self.addGate = function () {
if (self.type === 0 && !self.monster && !self.treasure && !self.gate) {
self.gate = true;
return true;
}
return false;
};
self.removeGate = function () {
self.gate = null;
};
return self;
});
var Monster = Container.expand(function () {
var self = Container.call(this);
var monsterSprite = self.attachAsset('monster', {
anchorX: 0.5,
anchorY: 0.5
});
self.mapX = 0;
self.mapY = 0;
self.health = 3;
self.takeDamage = function () {
self.health -= 1;
LK.getSound('hit').play();
// Visual feedback for hit
tween(monsterSprite, {
alpha: 0.2
}, {
duration: 100,
onFinish: function onFinish() {
tween(monsterSprite, {
alpha: 1
}, {
duration: 100
});
}
});
return self.health <= 0;
};
return self;
});
var Projectile = Container.expand(function () {
var self = Container.call(this);
var projectileSprite = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
self.speed = 10; // Speed of horizontal movement
self.startX = 0; // Starting X position on screen
self.targetX = 0; // How far to move
self.visible = false; // Start invisible
self.active = false; // Projectile state
self.isClone = false; // Track if this is a mirrored clone
// Method to flip the projectile sprite horizontally
self.setMirrored = function (mirrored) {
self.isClone = mirrored;
if (mirrored) {
// Flip the sprite horizontally by setting negative scale
projectileSprite.scaleX = -0.5;
} else {
projectileSprite.scaleX = 0.5;
}
};
self.update = function (deltaTime) {
if (!self.active) {
return false;
}
// Move projectile based on speed direction
self.x -= self.speed;
// Create pulsating effect for better visibility
if (LK.ticks % 20 === 0) {
tween(projectileSprite, {
scaleX: self.isClone ? -0.6 : 0.6,
scaleY: 0.6
}, {
duration: 200,
onFinish: function onFinish() {
tween(projectileSprite, {
scaleX: self.isClone ? -0.5 : 0.5,
scaleY: 0.5
}, {
duration: 200
});
}
});
}
// Return true if projectile has gone off either side of screen
if (self.speed > 0) {
// For projectiles moving left to right (clones), check right boundary
return self.x > 2048 + 100;
} else {
// For normal projectiles moving right to left, check left boundary
return self.x < -100;
}
};
self.fire = function (screenX, screenY) {
// Position at right side of screen
self.x = 2048 + 100; // Start off-screen to the right
self.y = screenY || 2732 - 400; // Position at bottom of screen instead of middle
self.visible = true;
self.active = true;
};
return self;
});
var RaycastStrip = Container.expand(function () {
var self = Container.call(this);
var wall = self.attachAsset('wall', {
anchorX: 0,
anchorY: 0
});
var ceiling = self.attachAsset('ceiling', {
anchorX: 0,
anchorY: 0
});
var floor = self.attachAsset('floor', {
anchorX: 0,
anchorY: 0
});
self.updateStrip = function (stripWidth, wallHeight, stripIdx, wallType, distance) {
// Wall setup
wall.width = stripWidth;
wall.height = wallHeight;
wall.y = (2732 - wallHeight) / 2;
// Ceiling setup
ceiling.width = stripWidth;
ceiling.height = wall.y;
ceiling.y = 0;
// Floor setup
floor.width = stripWidth;
floor.height = ceiling.height;
floor.y = wall.y + wallHeight;
// Adjust positions
self.x = stripIdx * stripWidth;
// Add distance shading effect
var shade = Math.max(0.3, 1 - distance / 10);
wall.alpha = shade;
ceiling.alpha = shade * 0.7;
floor.alpha = shade * 0.8;
};
return self;
});
var Treasure = Container.expand(function () {
var self = Container.call(this);
var treasureSprite = self.attachAsset('treasure', {
anchorX: 0.5,
anchorY: 0.5
});
self.mapX = 0;
self.mapY = 0;
self.value = 1;
// Animate the treasure to make it more appealing
var _animateTreasure = function animateTreasure() {
// Animation removed to stop spinning
};
// No longer calling animation
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111111
});
/****
* Game Code
****/
// Game constants
var MAP_SIZE = 16;
var CELL_SIZE = 20;
var MINI_MAP_SCALE = 1;
var STRIP_WIDTH = 32; // Increased strip width to reduce ray count
var NUM_RAYS = Math.ceil(2048 / STRIP_WIDTH);
var FOV = Math.PI / 3; // 60 degrees field of view
var HALF_FOV = FOV / 2;
var PLAYER_MOVE_SPEED = 0.002; // Reduced from 0.005 to make movement slower
var PLAYER_TURN_SPEED = 0.002; // Reduced from 0.005 to make turning slower
var WALL_HEIGHT_FACTOR = 600;
var MAX_RENDER_DISTANCE = 16;
var MONSTER_COUNT = 5;
var TREASURE_COUNT = 10;
// Game state
var map = [];
var player = {
x: 1.5,
y: 1.5,
dir: 0,
health: 5,
score: 0,
level: 1
};
var controls = {
forward: false,
backward: false,
left: false,
right: false,
attack: false,
boost: false
};
var monsters = [];
var treasures = [];
var projectiles = [];
var gate = null;
var lastTime = Date.now();
var canAttack = true;
var attackCooldown = 3000; // 3 second cooldown
// UI elements
var miniMap;
var rayCastView;
var healthText;
var scoreText;
var levelText;
var monsterText;
var controlButtons = {};
var playerMarker;
// Setup game
function setupGame() {
// Create the rayCast view container
rayCastView = new Container();
game.addChild(rayCastView);
// Create raycast strips
for (var i = 0; i < NUM_RAYS; i++) {
var strip = new RaycastStrip();
rayCastView.addChild(strip);
}
// Create minimap container
miniMap = new Container();
miniMap.x = (2048 - MAP_SIZE * CELL_SIZE * MINI_MAP_SCALE) / 2; // Center horizontally
miniMap.y = 20; // Keep at top
game.addChild(miniMap);
// Generate map
generateMap();
// Create player marker
playerMarker = game.addChild(LK.getAsset('player', {
anchorX: 0.5,
anchorY: 0.5
}));
// Create UI elements
createUI();
// Create control buttons
createControlButtons();
// Start ottoman ambience music in a loop
LK.playMusic('ottoman_ambience', {
loop: true
});
}
function generateMap() {
// Clear existing map
miniMap.removeChildren();
map = [];
// Remove existing monsters and treasures
for (var i = 0; i < monsters.length; i++) {
monsters[i].destroy();
}
monsters = [];
for (var i = 0; i < treasures.length; i++) {
treasures[i].destroy();
}
treasures = [];
// Clear projectiles
for (var i = 0; i < projectiles.length; i++) {
projectiles[i].destroy();
}
projectiles = [];
// Generate base map with borders
for (var y = 0; y < MAP_SIZE; y++) {
map[y] = [];
for (var x = 0; x < MAP_SIZE; x++) {
var cell = new MapCell();
cell.x = x * CELL_SIZE * MINI_MAP_SCALE;
cell.y = y * CELL_SIZE * MINI_MAP_SCALE;
// Create outer walls
if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) {
cell.setType(1); // Wall
} else {
// Random interior walls based on level difficulty
var wallChance = 0.2 + player.level * 0.03;
if (Math.random() < wallChance && !(x === 1 && y === 1)) {
// Ensure starting position is clear
cell.setType(1); // Wall
} else {
cell.setType(0); // Floor
}
}
map[y][x] = cell;
miniMap.addChild(cell);
}
}
// Check map connectivity and fix walled-off areas
ensureMapConnectivity();
// Create monsters
var monstersToPlace = MONSTER_COUNT + Math.floor(player.level * 0.5);
for (var i = 0; i < monstersToPlace; i++) {
placeMonster();
}
// Create treasures
var treasuresToPlace = TREASURE_COUNT;
for (var i = 0; i < treasuresToPlace; i++) {
placeTreasure();
}
// Reset player position
player.x = 1.5;
player.y = 1.5;
player.dir = 0;
// Place exit gate
gate = placeGate();
}
function placeMonster() {
// Find a random empty cell
var x, y;
var attempts = 0;
do {
x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
attempts++;
// Make sure it's not too close to the player
var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
if (attempts > 100) {
break;
} // Prevent infinite loop
} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || distToPlayer < 3);
if (attempts <= 100) {
map[y][x].addMonster();
var monster = new Monster();
monster.mapX = x;
monster.mapY = y;
monster.health = 2 + Math.floor(player.level / 3); // Monsters get tougher with level
monsters.push(monster);
game.addChild(monster);
}
}
function placeTreasure() {
// Find a random empty cell
var x, y;
var attempts = 0;
do {
x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
attempts++;
if (attempts > 100) {
break;
} // Prevent infinite loop
} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || map[y][x].gate);
if (attempts <= 100) {
map[y][x].addTreasure();
var treasure = new Treasure();
treasure.mapX = x;
treasure.mapY = y;
treasure.value = 1 + Math.floor(Math.random() * player.level);
treasures.push(treasure);
game.addChild(treasure);
}
}
function placeGate() {
// Place gate in a random valid location
var x, y;
var attempts = 0;
var validPositions = [];
// Collect all valid positions first
for (var i = 0; i < 100; i++) {
x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
// Check if the cell is suitable
if (map[y][x].type === 0 && !map[y][x].monster && !map[y][x].treasure && !map[y][x].gate) {
// Make sure it's not too close to the player (at least 2 cells away)
var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
if (distToPlayer > 2) {
// Add to valid positions
validPositions.push({
x: x,
y: y
});
}
}
}
// If we found valid spots, choose one randomly
if (validPositions.length > 0) {
// Pick a random position from the valid ones
var randomIndex = Math.floor(Math.random() * validPositions.length);
var chosenPos = validPositions[randomIndex];
var gateX = chosenPos.x;
var gateY = chosenPos.y;
// Place the gate
map[gateY][gateX].addGate();
var gate = new Gate();
gate.mapX = gateX;
gate.mapY = gateY;
game.addChild(gate);
return gate;
}
return null;
}
function createUI() {
// Health display
healthText = new Text2('Health: ' + player.health, {
size: 40,
fill: 0xFF5555
});
healthText.anchor.set(0, 0);
LK.gui.topRight.addChild(healthText);
healthText.x = -200;
healthText.y = 20;
// Score display
scoreText = new Text2('Score: ' + player.score, {
size: 40,
fill: 0xFFFF55
});
scoreText.anchor.set(0, 0);
LK.gui.topRight.addChild(scoreText);
scoreText.x = -200;
scoreText.y = 80;
// Level display
levelText = new Text2('Level: ' + player.level, {
size: 40,
fill: 0x55FF55
});
levelText.anchor.set(0, 0);
LK.gui.topRight.addChild(levelText);
levelText.x = -200;
levelText.y = 140;
// Monster counter display
monsterText = new Text2('Monsters: 0', {
size: 40,
fill: 0xFF9955
});
monsterText.anchor.set(0, 0);
LK.gui.topRight.addChild(monsterText);
monsterText.x = -200;
monsterText.y = 200;
// Update UI displays
updateUI();
}
function updateUI() {
healthText.setText('Health: ' + player.health);
scoreText.setText('Score: ' + player.score);
levelText.setText('Level: ' + player.level);
monsterText.setText('Monsters: ' + monsters.length);
// Update score in LK system
LK.setScore(player.score);
}
function createControlButtons() {
// Create directional buttons with increased size and more centered position
controlButtons.up = new ControlButton('up');
controlButtons.up.x = 400;
controlButtons.up.y = 2732 - 700;
game.addChild(controlButtons.up);
controlButtons.right = new ControlButton('right');
controlButtons.right.x = 650;
controlButtons.right.y = 2732 - 500;
game.addChild(controlButtons.right);
controlButtons.down = new ControlButton('down');
controlButtons.down.x = 400;
controlButtons.down.y = 2732 - 300;
game.addChild(controlButtons.down);
controlButtons.left = new ControlButton('left');
controlButtons.left.x = 150;
controlButtons.left.y = 2732 - 500;
game.addChild(controlButtons.left);
// Create attack button more centered on the right side
controlButtons.attack = new ControlButton('attack');
controlButtons.attack.x = 2048 - 400;
controlButtons.attack.y = 2732 - 500;
game.addChild(controlButtons.attack);
// Create boost button in the middle, moved a little to the right
controlButtons.boost = new BoostButton();
controlButtons.boost.x = 2048 / 2 + 150;
controlButtons.boost.y = 2732 - 500;
game.addChild(controlButtons.boost);
}
function rayCasting() {
var rayAngle, distToWall, rayDirX, rayDirY, mapCheckX, mapCheckY;
var distX, distY;
var rayStartX = player.x;
var rayStartY = player.y;
for (var rayIdx = 0; rayIdx < NUM_RAYS; rayIdx++) {
// Calculate ray angle (center ray + offset based on ray index)
rayAngle = player.dir - HALF_FOV + rayIdx / NUM_RAYS * FOV;
// Get direction vector
rayDirX = Math.cos(rayAngle);
rayDirY = Math.sin(rayAngle);
// Distance to wall
distToWall = 0;
hitWall = false;
// Step size for ray casting
var stepSizeX = Math.abs(1 / rayDirX);
var stepSizeY = Math.abs(1 / rayDirY);
// Which block we're checking
mapCheckX = Math.floor(rayStartX);
mapCheckY = Math.floor(rayStartY);
// Length of ray from current position to next x or y-side
var sideDistX, sideDistY;
// Direction to step in x or y direction (either +1 or -1)
var stepX = rayDirX >= 0 ? 1 : -1;
var stepY = rayDirY >= 0 ? 1 : -1;
// Calculate distance to first x and y side
if (rayDirX < 0) {
sideDistX = (rayStartX - mapCheckX) * stepSizeX;
} else {
sideDistX = (mapCheckX + 1.0 - rayStartX) * stepSizeX;
}
if (rayDirY < 0) {
sideDistY = (rayStartY - mapCheckY) * stepSizeY;
} else {
sideDistY = (mapCheckY + 1.0 - rayStartY) * stepSizeY;
}
// Perform DDA (Digital Differential Analysis)
var hit = false;
var side = 0; // 0 for x-side, 1 for y-side
var maxDistance = MAX_RENDER_DISTANCE;
while (!hit && distToWall < maxDistance) {
// Jump to next map square
if (sideDistX < sideDistY) {
sideDistX += stepSizeX;
mapCheckX += stepX;
side = 0;
} else {
sideDistY += stepSizeY;
mapCheckY += stepY;
side = 1;
}
// Check if ray has hit a wall
if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) {
hit = true;
distToWall = maxDistance;
} else if (map[mapCheckY][mapCheckX].type === 1) {
hit = true;
// Calculate exact distance to avoid fisheye effect
if (side === 0) {
distToWall = (mapCheckX - rayStartX + (1 - stepX) / 2) / rayDirX;
} else {
distToWall = (mapCheckY - rayStartY + (1 - stepY) / 2) / rayDirY;
}
}
}
// Calculate height of wall based on distance
var wallHeight = Math.min(2732, WALL_HEIGHT_FACTOR / distToWall);
// Update the strip
var strip = rayCastView.children[rayIdx];
strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, 1, distToWall);
}
// Render monsters and treasures
renderEntities();
}
function renderEntities() {
// First, hide all entities
for (var i = 0; i < monsters.length; i++) {
monsters[i].visible = false;
}
for (var i = 0; i < treasures.length; i++) {
treasures[i].visible = false;
}
// Hide gate initially
if (gate) {
gate.visible = false;
}
// Calculate entity positions relative to player
for (var i = 0; i < monsters.length; i++) {
var monster = monsters[i];
// Vector from player to monster
var dx = monster.mapX - player.x;
var dy = monster.mapY - player.y;
// Distance to monster
var dist = Math.sqrt(dx * dx + dy * dy);
// Angle between player's direction and monster
var angle = Math.atan2(dy, dx) - player.dir;
// Normalize angle
while (angle < -Math.PI) {
angle += Math.PI * 2;
}
while (angle > Math.PI) {
angle -= Math.PI * 2;
}
// Check if monster is in field of view
if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
// Check if there's a wall between player and monster
var rayDirX = Math.cos(player.dir + angle);
var rayDirY = Math.sin(player.dir + angle);
var rayHit = castRayToPoint(player.x, player.y, monster.mapX, monster.mapY);
if (!rayHit.hit || rayHit.dist > dist - 0.5) {
// Monster is visible
monster.visible = true;
// Calculate screen position
var screenX = (0.5 + angle / FOV) * 2048;
// Calculate height based on distance
var height = WALL_HEIGHT_FACTOR / dist;
// Position monster in the middle of the screen
monster.x = screenX;
monster.y = 2732 / 2;
// Scale monster based on distance
var scale = height / 100;
monster.scale.set(scale, scale);
}
}
}
// Render treasures with the same logic
for (var i = 0; i < treasures.length; i++) {
var treasure = treasures[i];
var dx = treasure.mapX - player.x;
var dy = treasure.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx) - player.dir;
while (angle < -Math.PI) {
angle += Math.PI * 2;
}
while (angle > Math.PI) {
angle -= Math.PI * 2;
}
if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
var rayHit = castRayToPoint(player.x, player.y, treasure.mapX, treasure.mapY);
if (!rayHit.hit || rayHit.dist > dist - 0.5) {
treasure.visible = true;
var screenX = (0.5 + angle / FOV) * 2048;
var height = WALL_HEIGHT_FACTOR / dist;
treasure.x = screenX;
treasure.y = 2732 / 2;
var scale = height / 100;
treasure.scale.set(scale, scale);
}
}
}
// Render gate with the same logic as treasures and monsters
if (gate) {
var dx = gate.mapX - player.x;
var dy = gate.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx) - player.dir;
while (angle < -Math.PI) {
angle += Math.PI * 2;
}
while (angle > Math.PI) {
angle -= Math.PI * 2;
}
if (Math.abs(angle) < HALF_FOV && dist < MAX_RENDER_DISTANCE) {
var rayHit = castRayToPoint(player.x, player.y, gate.mapX, gate.mapY);
if (!rayHit.hit || rayHit.dist > dist - 0.5) {
gate.visible = true;
var screenX = (0.5 + angle / FOV) * 2048;
var height = WALL_HEIGHT_FACTOR / dist;
gate.x = screenX;
gate.y = 2732 / 2;
var scale = height / 100;
gate.scale.set(scale, scale);
}
}
}
}
function castRayToPoint(startX, startY, targetX, targetY) {
var rayDirX = targetX - startX;
var rayDirY = targetY - startY;
var distance = Math.sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
rayDirX /= distance;
rayDirY /= distance;
var mapCheckX = Math.floor(startX);
var mapCheckY = Math.floor(startY);
var stepSizeX = Math.abs(1 / rayDirX);
var stepSizeY = Math.abs(1 / rayDirY);
var stepX = rayDirX >= 0 ? 1 : -1;
var stepY = rayDirY >= 0 ? 1 : -1;
var sideDistX, sideDistY;
if (rayDirX < 0) {
sideDistX = (startX - mapCheckX) * stepSizeX;
} else {
sideDistX = (mapCheckX + 1.0 - startX) * stepSizeX;
}
if (rayDirY < 0) {
sideDistY = (startY - mapCheckY) * stepSizeY;
} else {
sideDistY = (mapCheckY + 1.0 - startY) * stepSizeY;
}
var hit = false;
var side = 0;
var distToWall = 0;
while (!hit && distToWall < distance) {
if (sideDistX < sideDistY) {
sideDistX += stepSizeX;
mapCheckX += stepX;
side = 0;
distToWall = sideDistX - stepSizeX;
} else {
sideDistY += stepSizeY;
mapCheckY += stepY;
side = 1;
distToWall = sideDistY - stepSizeY;
}
if (mapCheckX < 0 || mapCheckX >= MAP_SIZE || mapCheckY < 0 || mapCheckY >= MAP_SIZE || !map[mapCheckY] || !map[mapCheckY][mapCheckX]) {
break;
} else if (map[mapCheckY][mapCheckX].type === 1) {
hit = true;
}
}
return {
hit: hit,
dist: distToWall
};
}
function updateControls() {
// Read from control buttons
controls.forward = controlButtons.up.pressed;
controls.backward = controlButtons.down.pressed;
controls.left = controlButtons.left.pressed;
controls.right = controlButtons.right.pressed;
controls.attack = controlButtons.attack.pressed;
controls.boost = controlButtons.boost && controlButtons.boost.active;
}
function updatePlayerMovement(deltaTime) {
var moveSpeed = PLAYER_MOVE_SPEED * deltaTime;
var turnSpeed = PLAYER_TURN_SPEED * deltaTime;
var dx = 0,
dy = 0;
var didMove = false;
// Track player's last position to detect state changes
if (player.lastX === undefined) {
player.lastX = player.x;
}
if (player.lastY === undefined) {
player.lastY = player.y;
}
// Handle rotation
if (controls.left) {
player.dir -= turnSpeed;
while (player.dir < 0) {
player.dir += Math.PI * 2;
}
}
if (controls.right) {
player.dir += turnSpeed;
while (player.dir >= Math.PI * 2) {
player.dir -= Math.PI * 2;
}
}
// Handle movement
if (controls.forward) {
dx += Math.cos(player.dir) * moveSpeed;
dy += Math.sin(player.dir) * moveSpeed;
didMove = true;
}
if (controls.backward) {
dx -= Math.cos(player.dir) * moveSpeed;
dy -= Math.sin(player.dir) * moveSpeed;
didMove = true;
}
// Collision detection
var newX = player.x + dx;
var newY = player.y + dy;
var cellX = Math.floor(newX);
var cellY = Math.floor(newY);
// Check if we can move to the new position
if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE && map[cellY] && map[cellY][cellX]) {
if (map[cellY][cellX].type === 0) {
player.x = newX;
player.y = newY;
if (didMove && LK.ticks % 20 === 0) {
LK.getSound('walk').play();
}
}
}
// Only attempt to attack if attack button is pressed and we can attack
if (controls.attack && canAttack) {
attackAction();
}
// Check for collisions with monsters
checkMonsterCollisions();
// Check for collisions with treasures
checkTreasureCollisions();
// Check for gate collision
checkGateCollision();
// Update player marker on minimap
updateMiniMap();
// Update player's last position
player.lastX = player.x;
player.lastY = player.y;
}
function attackAction() {
// Check if attack is on cooldown
if (!canAttack) {
return;
}
// Set attack on cooldown
canAttack = false;
// Play attack sound
LK.getSound('attack').play();
// Create main projectile from right side
var projectile = new Projectile();
// Set scale for appropriate size
var scale = 1.0;
projectile.scale.set(scale, scale);
// Set this as the original, non-mirrored projectile
projectile.setMirrored(false);
// Initialize and fire the projectile from right to left
projectile.fire(2048 + 100, 2732 / 2);
// Add visual pulse effect to make projectile more visible
tween(projectile, {
alpha: 0.7,
scaleX: scale * 1.2,
scaleY: scale * 1.2
}, {
duration: 500,
onFinish: function onFinish() {
tween(projectile, {
alpha: 1,
scaleX: scale,
scaleY: scale
}, {
duration: 500
});
}
});
// Add to the game
projectiles.push(projectile);
game.addChild(projectile);
// Create clone projectile on opposite side (left side)
var cloneProjectile = new Projectile();
cloneProjectile.scale.set(scale, scale);
// Set this as the mirrored clone
cloneProjectile.setMirrored(true);
// Make clone start on the left side
cloneProjectile.x = -100;
cloneProjectile.y = 2732 / 2;
cloneProjectile.visible = true;
cloneProjectile.active = true;
// Reverse speed to make it move right to left
cloneProjectile.speed = -cloneProjectile.speed;
// Add visual pulse effect to clone
tween(cloneProjectile, {
alpha: 0.7,
scaleX: scale * 1.2,
scaleY: scale * 1.2
}, {
duration: 500,
onFinish: function onFinish() {
tween(cloneProjectile, {
alpha: 1,
scaleX: scale,
scaleY: scale
}, {
duration: 500
});
}
});
// Add to the game
projectiles.push(cloneProjectile);
game.addChild(cloneProjectile);
// Start cooldown animation for attack button
var attackButton = controlButtons.attack;
attackButton.updateCooldown(0);
// Create a tween for the cooldown visual effect
tween(attackButton, {
_cooldownProgress: 1
}, {
duration: attackCooldown,
onFinish: function onFinish() {
canAttack = true;
attackButton.updateCooldown(1);
}
});
// Update button visual during cooldown
var _cooldownTick = function cooldownTick(progress) {
attackButton.updateCooldown(progress);
if (progress < 1) {
LK.setTimeout(function () {
_cooldownTick(progress + 0.05);
}, attackCooldown / 20);
}
};
_cooldownTick(0);
// Reset cooldown after specified time
LK.setTimeout(function () {
canAttack = true;
}, attackCooldown);
}
function updateProjectiles(deltaTime) {
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectile = projectiles[i];
// Update projectile position
var remove = projectile.update(deltaTime);
// Check if projectile has gone off screen or should be removed
if (remove) {
// Remove projectile
projectile.destroy();
projectiles.splice(i, 1);
continue;
}
// Check if projectile hits a monster
var hitMonster = false;
var hitMonsterIndex = -1;
for (var j = 0; j < monsters.length; j++) {
var monster = monsters[j];
// Calculate distance between player and monster for range check
var distToMonster = Math.sqrt(Math.pow(monster.mapX - player.x, 2) + Math.pow(monster.mapY - player.y, 2));
// Only allow hits if monster is within range (5 map units) - stricter range check
var inRange = distToMonster <= 2.5;
// Simple screen space collision check with added distance constraint
if (monster.visible && inRange && Math.abs(projectile.x - monster.x) < 100 && Math.abs(projectile.y - monster.y) < 100) {
hitMonster = true;
hitMonsterIndex = j;
break;
}
}
// Handle monster hit
if (hitMonster && hitMonsterIndex !== -1) {
var monster = monsters[hitMonsterIndex];
var killed = monster.takeDamage();
if (killed) {
// Remove monster
map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster();
monster.destroy();
monsters.splice(hitMonsterIndex, 1);
// Increase score
player.score += 10;
updateUI();
// Check for level completion
checkLevelCompletion();
}
// Remove projectile on hit
projectile.destroy();
projectiles.splice(i, 1);
}
}
}
function checkMonsterCollisions() {
for (var i = 0; i < monsters.length; i++) {
var monster = monsters[i];
var dx = monster.mapX - player.x;
var dy = monster.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) {
// Player hit by monster
player.health--;
updateUI();
// Visual feedback
LK.effects.flashScreen(0xff0000, 300);
// Play sound
LK.getSound('hit').play();
// Push player back slightly
player.x -= dx * 0.3;
player.y -= dy * 0.3;
// Check game over
if (player.health <= 0) {
LK.showGameOver();
}
break;
}
}
}
function checkTreasureCollisions() {
for (var i = treasures.length - 1; i >= 0; i--) {
var treasure = treasures[i];
var dx = treasure.mapX - player.x;
var dy = treasure.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) {
// Collect treasure
player.score += treasure.value * 5;
updateUI();
// Play sound
LK.getSound('collect').play();
// Remove treasure
map[Math.floor(treasure.mapY)][Math.floor(treasure.mapX)].removeTreasure();
treasure.destroy();
treasures.splice(i, 1);
// Check level completion
checkLevelCompletion();
}
}
}
function checkGateCollision() {
if (!gate) {
return;
}
var dx = gate.mapX - player.x;
var dy = gate.mapY - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.7) {
// Check if all monsters need to be defeated first
if (monsters.length > 0) {
// Display message that monsters need to be defeated
var warningText = new Text2('Defeat all monsters\nbefore exiting!', {
size: 60,
fill: 0xFF5555
});
warningText.anchor.set(0.5, 0.5);
warningText.x = 2048 / 2;
warningText.y = 2732 / 2;
game.addChild(warningText);
// Remove text after a few seconds
LK.setTimeout(function () {
warningText.destroy();
}, 2000);
return;
}
// Player reached the gate - complete level
// Play collect sound
LK.getSound('collect').play();
// Add points for completing level
player.score += 50 * player.level;
// Remove gate from map
map[Math.floor(gate.mapY)][Math.floor(gate.mapX)].removeGate();
gate.destroy();
gate = null;
// Level up
player.level++;
storage.level = player.level;
// Restore health
player.health = Math.min(player.health + 2, 5);
// Update UI
updateUI();
// Show level complete with gate messaging
var levelCompleteText = new Text2('Level ' + (player.level - 1) + ' Complete!\nYou found the exit!', {
size: 80,
fill: 0x00FF55
});
levelCompleteText.anchor.set(0.5, 0.5);
levelCompleteText.x = 2048 / 2;
levelCompleteText.y = 2732 / 2;
game.addChild(levelCompleteText);
// Generate new level after a delay
LK.setTimeout(function () {
levelCompleteText.destroy();
generateMap();
}, 2000);
}
}
function checkLevelCompletion() {
// Level is only considered "complete" if all monsters are defeated
// This allows the gate to activate
if (monsters.length === 0) {
// If gate is not visible, make it pulse more dramatically to draw attention
if (gate) {
tween(gate, {
alpha: 0.2
}, {
duration: 300,
onFinish: function onFinish() {
tween(gate, {
alpha: 1
}, {
duration: 300
});
}
});
// Show hint text
var gateHintText = new Text2('Find the exit gate!', {
size: 60,
fill: 0x00FF55
});
gateHintText.anchor.set(0.5, 0.5);
gateHintText.x = 2048 / 2;
gateHintText.y = 200;
game.addChild(gateHintText);
// Remove hint after a few seconds
LK.setTimeout(function () {
gateHintText.destroy();
}, 3000);
}
} else {
// Show hint text about defeating monsters first
if (gate && gate.visible && LK.ticks % 300 === 0) {
var monsterHintText = new Text2('Defeat all monsters to activate the exit!', {
size: 60,
fill: 0xFF5555
});
monsterHintText.anchor.set(0.5, 0.5);
monsterHintText.x = 2048 / 2;
monsterHintText.y = 200;
game.addChild(monsterHintText);
// Remove hint after a few seconds
LK.setTimeout(function () {
monsterHintText.destroy();
}, 3000);
}
}
}
function updateMiniMap() {
// Update player marker position on minimap
playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE;
playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE;
// Draw player direction indicator
var dirX = Math.cos(player.dir) * 15;
var dirY = Math.sin(player.dir) * 15;
}
// Game update method
game.update = function () {
var currentTime = Date.now();
var deltaTime = currentTime - lastTime;
lastTime = currentTime;
// Update controls
updateControls();
// Update player
updatePlayerMovement(deltaTime);
// Update monsters (only move every few ticks to make movement slower)
if (LK.ticks % 10 === 0) {
for (var i = 0; i < monsters.length; i++) {
MonsterAI.moveTowardsPlayer(monsters[i], player.x, player.y, map);
}
}
// Update projectiles
updateProjectiles(deltaTime);
// Update raycast view
rayCasting();
};
// Game initialization
setupGame();
// Event handlers
game.down = function (x, y, obj) {
// Handle general screen press
};
game.up = function (x, y, obj) {
// Handle general screen release
};
game.move = function (x, y, obj) {
// Handle general screen move
};
var MonsterAI = {
moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) {
// Get monster's map position
var monsterX = monster.mapX;
var monsterY = monster.mapY;
// Vector from monster to player
var dx = playerX - monsterX;
var dy = playerY - monsterY;
// Only move if monster is within a certain distance to the player
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 8) {
return;
} // Don't move if too far away
// Normalize direction vector
var length = Math.sqrt(dx * dx + dy * dy);
if (length > 0) {
dx /= length;
dy /= length;
}
// Movement speed (slower than player)
var moveSpeed = 0.05; // Increased for smoother animation
// Calculate potential new position
var newX = monsterX + dx * moveSpeed;
var newY = monsterY + dy * moveSpeed;
// Round to get map cell coordinates
var cellX = Math.floor(newX);
var cellY = Math.floor(newY);
// Check if new position is valid (not a wall or another monster)
if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) {
if (map[cellY][cellX].type === 0 && !map[cellY][cellX].monster) {
// Update map cell references
map[Math.floor(monsterY)][Math.floor(monsterX)].removeMonster();
map[cellY][cellX].addMonster();
// Update monster position with smooth animation
monster.mapX = newX;
monster.mapY = newY;
// Calculate screen position for the moved monster
var monsterToPlayerDX = playerX - newX;
var monsterToPlayerDY = playerY - newY;
var monsterDist = Math.sqrt(monsterToPlayerDX * monsterToPlayerDX + monsterToPlayerDY * monsterToPlayerDY);
var monsterAngle = Math.atan2(monsterToPlayerDY, monsterToPlayerDX) - Math.atan2(dy, dx);
// Position monster in the middle of the screen with updated coordinates
monster.x = 2048 / 2;
monster.y = 2732 / 2;
// Scale monster based on distance
var scale = WALL_HEIGHT_FACTOR / (monsterDist * 100);
monster.scale.set(scale, scale);
}
}
}
};
function ensureMapConnectivity() {
// Flood fill from player starting position to check accessibility
var visited = [];
for (var y = 0; y < MAP_SIZE; y++) {
visited[y] = [];
for (var x = 0; x < MAP_SIZE; x++) {
visited[y][x] = false;
}
}
var queue = [];
// Start from player position (1,1)
queue.push({
x: 1,
y: 1
});
visited[1][1] = true;
// Perform flood fill
while (queue.length > 0) {
var current = queue.shift();
var x = current.x;
var y = current.y;
// Check all four neighbors
var neighbors = [{
x: x + 1,
y: y
}, {
x: x - 1,
y: y
}, {
x: x,
y: y + 1
}, {
x: x,
y: y - 1
}];
for (var i = 0; i < neighbors.length; i++) {
var nx = neighbors[i].x;
var ny = neighbors[i].y;
// Check if neighbor is valid and not visited
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) {
visited[ny][nx] = true;
queue.push({
x: nx,
y: ny
});
}
}
}
// Check for unreachable areas and create paths to them
for (var y = 1; y < MAP_SIZE - 1; y++) {
for (var x = 1; x < MAP_SIZE - 1; x++) {
// If it's a floor tile but wasn't visited, it's unreachable
if (map[y][x].type === 0 && !visited[y][x]) {
// Find the nearest accessible cell
var nearestX = -1;
var nearestY = -1;
var minDist = MAP_SIZE * MAP_SIZE;
for (var cy = 1; cy < MAP_SIZE - 1; cy++) {
for (var cx = 1; cx < MAP_SIZE - 1; cx++) {
if (visited[cy][cx]) {
var dist = (cx - x) * (cx - x) + (cy - y) * (cy - y);
if (dist < minDist) {
minDist = dist;
nearestX = cx;
nearestY = cy;
}
}
}
}
// Create a path from the unreachable area to the nearest accessible area
if (nearestX !== -1) {
createPath(x, y, nearestX, nearestY);
// Update visited map by doing a mini flood fill from this newly connected point
var pathQueue = [{
x: x,
y: y
}];
visited[y][x] = true;
while (pathQueue.length > 0) {
var current = pathQueue.shift();
var px = current.x;
var py = current.y;
var pathNeighbors = [{
x: px + 1,
y: py
}, {
x: px - 1,
y: py
}, {
x: px,
y: py + 1
}, {
x: px,
y: py - 1
}];
for (var j = 0; j < pathNeighbors.length; j++) {
var nx = pathNeighbors[j].x;
var ny = pathNeighbors[j].y;
if (nx >= 0 && nx < MAP_SIZE && ny >= 0 && ny < MAP_SIZE && map[ny][nx].type === 0 && !visited[ny][nx]) {
visited[ny][nx] = true;
pathQueue.push({
x: nx,
y: ny
});
}
}
}
}
}
}
}
}
function createPath(startX, startY, endX, endY) {
// Determine direction (horizontal or vertical first)
if (Math.random() < 0.5) {
// Horizontal first, then vertical
var x = startX;
while (x !== endX) {
x += x < endX ? 1 : -1;
if (map[startY][x].type === 1) {
map[startY][x].setType(0); // Convert wall to floor
}
}
var y = startY;
while (y !== endY) {
y += y < endY ? 1 : -1;
if (map[y][endX].type === 1) {
map[y][endX].setType(0); // Convert wall to floor
}
}
} else {
// Vertical first, then horizontal
var y = startY;
while (y !== endY) {
y += y < endY ? 1 : -1;
if (map[y][startX].type === 1) {
map[y][startX].setType(0); // Convert wall to floor
}
}
var x = startX;
while (x !== endX) {
x += x < endX ? 1 : -1;
if (map[endY][x].type === 1) {
map[endY][x].setType(0); // Convert wall to floor
}
}
}
} ===================================================================
--- original.js
+++ change.js
@@ -437,10 +437,12 @@
// Create UI elements
createUI();
// Create control buttons
createControlButtons();
- // Start background music
- LK.playMusic('dungeon');
+ // Start ottoman ambience music in a loop
+ LK.playMusic('ottoman_ambience', {
+ loop: true
+ });
}
function generateMap() {
// Clear existing map
miniMap.removeChildren();
Tan wall. In-Game asset. 2d. High contrast. No shadows
pixel detailed A Janissary of the Ottoman Empire. In-Game asset. 2d. High contrast. No shadows
pixel ottoman dagger. In-Game asset. 2d. High contrast. No shadows
ottoman lady pixel. In-Game asset. 2d. High contrast. No shadows
ottoman walls pixel. In-Game asset. 2d. High contrast. No shadows