User prompt
Slow down spinning a little
User prompt
Make the player spin faster
User prompt
Make the monster follow the player
User prompt
Slow down the monsters attack rate
User prompt
Make the monster move towards the player
User prompt
Make it move further every tick
User prompt
Make the distance the monster moves much less far but it moves every 10 tics
User prompt
Make it every 80 ticks
User prompt
Slow the monster down
User prompt
Make the monsters map position move towards the player
User prompt
Implement monster movement
User prompt
Make the projectile use ray-casting to shoot
User prompt
Speed up the speed of the sizing down of the projectile
User prompt
Make the projectile ray much slower
User prompt
Make the ray the same speed as the projectile
User prompt
Hook up the size of the projectile to a spot on the 2d plane that is raycasted and if the projectile spot hits the monster than the monster is hit
User prompt
Make it so it has to be closer to hit
User prompt
Make it so the projectile only hits the monster if it is close enough
User prompt
Slow the projectile down
User prompt
The projectile should stay in the center of the screen and get smaller to show it getting further away
User prompt
We are playing this game from a first person perspective, and so the projectile should fire out from the player
User prompt
Implement monster movement
User prompt
Delete the monster movement script and make it actually move towards the player
User prompt
Make the monsters keep moving forward the entire level
User prompt
Make the monsters move side to side ↪💡 Consider importing and using the following plugins: @upit/tween.v1
/****
* 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.lastAttackTime = 0;
self.attackCooldown = 2000; // 2 second cooldown between attacks
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.006; // Slightly reduced from 0.008 to slow down spinning a bit
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 background music
LK.playMusic('dungeon');
}
function generateMap() {
// Clear existing map
miniMap.removeChildren();
map = [];
// Remove existing monsters and treasures
for (var i = 0; i < monsters.length; i++) {
monsters[i].destroy();
}
monsters = [];
for (var i = 0; i < treasures.length; i++) {
treasures[i].destroy();
}
treasures = [];
// Clear projectiles
for (var i = 0; i < projectiles.length; i++) {
projectiles[i].destroy();
}
projectiles = [];
// Generate base map with borders
for (var y = 0; y < MAP_SIZE; y++) {
map[y] = [];
for (var x = 0; x < MAP_SIZE; x++) {
var cell = new MapCell();
cell.x = x * CELL_SIZE * MINI_MAP_SCALE;
cell.y = y * CELL_SIZE * MINI_MAP_SCALE;
// Create outer walls
if (x === 0 || y === 0 || x === MAP_SIZE - 1 || y === MAP_SIZE - 1) {
cell.setType(1); // Wall
} else {
// Random interior walls based on level difficulty
var wallChance = 0.2 + player.level * 0.03;
if (Math.random() < wallChance && !(x === 1 && y === 1)) {
// Ensure starting position is clear
cell.setType(1); // Wall
} else {
cell.setType(0); // Floor
}
}
map[y][x] = cell;
miniMap.addChild(cell);
}
}
// Check map connectivity and fix walled-off areas
ensureMapConnectivity();
// Create monsters
var monstersToPlace = MONSTER_COUNT + Math.floor(player.level * 0.5);
for (var i = 0; i < monstersToPlace; i++) {
placeMonster();
}
// Create treasures
var treasuresToPlace = TREASURE_COUNT;
for (var i = 0; i < treasuresToPlace; i++) {
placeTreasure();
}
// Reset player position
player.x = 1.5;
player.y = 1.5;
player.dir = 0;
// Place exit gate
gate = placeGate();
}
function placeMonster() {
// Find a random empty cell
var x, y;
var attempts = 0;
do {
x = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
y = Math.floor(Math.random() * (MAP_SIZE - 2)) + 1;
attempts++;
// Make sure it's not too close to the player
var distToPlayer = Math.sqrt(Math.pow(x - player.x, 2) + Math.pow(y - player.y, 2));
if (attempts > 100) {
break;
} // Prevent infinite loop
} while (map[y][x].type !== 0 || map[y][x].monster || map[y][x].treasure || distToPlayer < 3);
if (attempts <= 100) {
map[y][x].addMonster();
var monster = new Monster();
monster.mapX = x;
monster.mapY = y;
monster.health = 2 + Math.floor(player.level / 3); // Monsters get tougher with level
monsters.push(monster);
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() {
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);
if (dist < 0.5) {
// Check if monster can attack (cooldown elapsed)
if (currentTime - monster.lastAttackTime >= monster.attackCooldown) {
// Player hit by monster
player.health--;
monster.lastAttackTime = currentTime;
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 current position
var monsterX = monster.mapX;
var monsterY = monster.mapY;
// Calculate distance to player
var dx = playerX - monsterX;
var dy = playerY - monsterY;
var dist = Math.sqrt(dx * dx + dy * dy);
// Only move if monster is within detection range
if (dist > 12 || dist < 0.4) {
return;
} // Extended detection range, closer minimum distance
// Determine movement direction with improved pathfinding
var moveSpeed = 0.12; // Increased movement speed
var newX = monsterX;
var newY = monsterY;
// Try to move directly towards player first
var moveX = dx > 0 ? moveSpeed : -moveSpeed;
var moveY = dy > 0 ? moveSpeed : -moveSpeed;
// Attempt diagonal movement if both axes have significant distance
if (Math.abs(dx) > 0.2 && Math.abs(dy) > 0.2) {
// Try diagonal movement
var testX = monsterX + moveX * 0.7;
var testY = monsterY + moveY * 0.7;
var cellX = Math.floor(testX);
var cellY = Math.floor(testY);
if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) {
if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].type === 0) {
// Check if there's another monster in the target cell
var blocked = false;
for (var i = 0; i < monsters.length; i++) {
var otherMonster = monsters[i];
if (otherMonster !== monster) {
var otherCellX = Math.floor(otherMonster.mapX);
var otherCellY = Math.floor(otherMonster.mapY);
if (otherCellX === cellX && otherCellY === cellY) {
blocked = true;
break;
}
}
}
if (!blocked) {
newX = testX;
newY = testY;
}
}
}
} else {
// Move along the axis with the largest difference
if (Math.abs(dx) > Math.abs(dy)) {
// Move horizontally towards player
if (dx > 0) {
newX = monsterX + moveSpeed;
} else {
newX = monsterX - moveSpeed;
}
} else {
// Move vertically towards player
if (dy > 0) {
newY = monsterY + moveSpeed;
} else {
newY = monsterY - moveSpeed;
}
}
}
// Validate the new position if we haven't moved diagonally
if (newX === monsterX && newY === monsterY) {
// Check if new position is valid (within bounds and not a wall)
var cellX = Math.floor(newX);
var cellY = Math.floor(newY);
if (cellX >= 0 && cellX < MAP_SIZE && cellY >= 0 && cellY < MAP_SIZE) {
if (map[cellY] && map[cellY][cellX] && map[cellY][cellX].type === 0) {
// Check if there's another monster in the target cell
var blocked = false;
for (var i = 0; i < monsters.length; i++) {
var otherMonster = monsters[i];
if (otherMonster !== monster) {
var otherCellX = Math.floor(otherMonster.mapX);
var otherCellY = Math.floor(otherMonster.mapY);
if (otherCellX === cellX && otherCellY === cellY) {
blocked = true;
break;
}
}
}
if (!blocked) {
// Update map cell references only if crossing cell boundaries
var oldCellX = Math.floor(monsterX);
var oldCellY = Math.floor(monsterY);
if (oldCellX !== cellX || oldCellY !== cellY) {
if (map[oldCellY] && map[oldCellY][oldCellX]) {
map[oldCellY][oldCellX].removeMonster();
}
if (map[cellY] && map[cellY][cellX]) {
map[cellY][cellX].addMonster();
}
}
}
}
}
}
// Update monster position if we have a valid new position
if (newX !== monsterX || newY !== monsterY) {
// Update map cell references only if crossing cell boundaries
var oldCellX = Math.floor(monsterX);
var oldCellY = Math.floor(monsterY);
var newCellX = Math.floor(newX);
var newCellY = Math.floor(newY);
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();
}
}
// Update monster position smoothly
monster.mapX = newX;
monster.mapY = newY;
}
}
};
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
@@ -374,9 +374,9 @@
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.008; // Increased from 0.002 to make turning faster
+var PLAYER_TURN_SPEED = 0.006; // Slightly reduced from 0.008 to slow down spinning a bit
var WALL_HEIGHT_FACTOR = 600;
var MAX_RENDER_DISTANCE = 16;
var MONSTER_COUNT = 5;
var TREASURE_COUNT = 10;
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!
Sword. In-Game asset. 2d. High contrast. No shadows
Tan wall. In-Game asset. 2d. High contrast. No shadows
Make a bunch of empty space above it.