User prompt
Give the projectiles real world coordinates so that they move relative to player movement and give the impression of independent projectiles after firing
User prompt
Adjust projectile x position based on player movement after firing to give real world coordinates.
User prompt
Change projectiles to work how they do in Doom and other sprite based first person shooters. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Projectiles should move relative to the game world after being fired and not stay attached to player perspective.
User prompt
Fix the monster AI so that they move towards the player when they have line of sight. This should be accompanied by a scale increase to give the illusion of coming towards the player. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
That hasn’t worked. Do you understand what I mean? The player is moving forward, the attack button is pressed, continue moving the player forward and do not reset the joystick position if the game.down detects the touch on the attack button, then as soon as game.up is true, reinstate direct control to the player using the last known joystick position.
User prompt
The joystick movement is being interrupted by pressing the attack button. Can we store the joystick value, keep the movement going without input and then reinstate the joystick value immediately after the attack input is registered?
User prompt
Okay so. Is there a workaround for the joystick and attack button working without interrupting each other? Can we store joystick values and not interrupt movement when the attack button is pressed and then reinitialize movement with the last known values?
User prompt
I think that the second touch on the fire button is interfering with the joystick control. As soon as the attack button is pressed, the joystick snaps back to center.
User prompt
Okay now adjust the vertical movement of the projectiles to be a constant, in sync with scale change so that it doesn’t finish before the reaching true center. The scale change and vertical movement should finish at the same time.
User prompt
The point that we are traveling to should be 2732 / 2. Projectiles should not go higher than that. Only the scale should continue to change as they move “away”
User prompt
Getting closer. The angle can be even sharper. We are aiming for the center of the screen
User prompt
The game is using a first person perspective and I want the player to fire the projectile as if they are firing it out it towards the monsters. The projectiles should come from near the bottom of the screen and shrink in size as they travel away and into the game ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
When the player presses the attack button instead fire a projectile that travels outwards away from the player.
User prompt
Create a control stick set up for movement.
Remix started
Copy RayCaster Dungeon Crawler
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
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 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.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 = 20; // Increased speed for better effect
self.startScale = 0.8; // Starting scale for 3D effect
self.endScale = 0.1; // Ending scale (smaller as it goes away)
self.distance = 0; // Current travel distance
self.maxDistance = 2048; // Maximum travel distance
self.visible = false; // Start invisible
self.active = false; // Projectile state
self.update = function (deltaTime) {
if (!self.active) return false;
// Move projectile away from player (up the screen)
self.y -= self.speed;
self.distance += self.speed;
// Calculate scale based on distance (shrink as it moves away)
var progress = self.distance / self.maxDistance;
var currentScale = self.startScale * (1 - progress) + self.endScale * progress;
projectileSprite.scale.set(currentScale, currentScale);
// Return true if projectile has gone too far
return self.distance > self.maxDistance || self.y < -100;
};
self.fire = function (screenX, screenY) {
// Position at bottom of screen
self.x = screenX || 2048 / 2; // Center horizontally if no X provided
self.y = 2732 - 300; // Start near bottom of screen
self.distance = 0; // Reset distance
projectileSprite.scale.set(self.startScale, self.startScale); // Reset scale
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 = 8;
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
};
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 joystick control for movement
controlButtons.joystick = new JoystickController();
controlButtons.joystick.x = 400;
controlButtons.joystick.y = 2732 - 500;
game.addChild(controlButtons.joystick);
// 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);
}
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
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 joystick for movement
var joystick = controlButtons.joystick;
if (joystick.active) {
// Use joystick position for directional input
controls.forward = joystick.normalizedY < -0.3;
controls.backward = joystick.normalizedY > 0.3;
controls.left = joystick.normalizedX < -0.3;
controls.right = joystick.normalizedX > 0.3;
} else {
controls.forward = false;
controls.backward = false;
controls.left = false;
controls.right = false;
}
// Read from attack button
controls.attack = controlButtons.attack.pressed;
}
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;
// Get joystick values for analog control
var joystick = controlButtons.joystick;
var turnAmount = 0;
var moveAmount = 0;
// Handle rotation - use x-axis for turning
if (joystick.active) {
turnAmount = joystick.normalizedX * turnSpeed;
player.dir += turnAmount;
while (player.dir < 0) 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) {
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;
}
// Handle movement - use y-axis for forward/backward
if (joystick.active) {
moveAmount = -joystick.normalizedY * moveSpeed; // Negative because up is negative y
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) {
dx += Math.cos(player.dir) * moveSpeed;
dy += Math.sin(player.dir) * moveSpeed;
didMove = true;
} else 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 projectile from player perspective (bottom of screen)
var projectile = new Projectile();
// Fire the projectile from bottom center
projectile.fire(2048 / 2, 2732 - 300);
// Add visual flash effect for firing
tween(projectile, {
alpha: 0.7
}, {
duration: 200,
onFinish: function onFinish() {
tween(projectile, {
alpha: 1
}, {
duration: 200
});
}
});
// Add to the game
projectiles.push(projectile);
game.addChild(projectile);
// Create second projectile for more visual effect
var projectile2 = new Projectile();
// Fire from slightly offset position for wider attack
projectile2.fire(2048 / 2 - 200, 2732 - 300);
projectiles.push(projectile2);
game.addChild(projectile2);
// Create third projectile for even more visual effect
var projectile3 = new Projectile();
// Fire from slightly offset position on the other side
projectile3.fire(2048 / 2 + 200, 2732 - 300);
projectiles.push(projectile3);
game.addChild(projectile3);
// 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];
// 3D perspective collision check - distance-based collision window
// As projectile gets further away (higher Y position), collision window narrows
if (monster.visible) {
// Calculate collision window based on projectile's Y position
// The higher up the projectile (smaller Y), the smaller the hit window
var distanceFactor = 1 - projectile.y / 2732;
var hitWindowX = 300 * distanceFactor;
// More likely to hit monsters in the center of view
if (Math.abs(projectile.x - monster.x) < hitWindowX) {
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 % 20 === 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
// Check if press is in the lower left quadrant where the joystick is
if (x < 800 && y > 2732 - 800) {
controlButtons.joystick.down(x - controlButtons.joystick.x, y - controlButtons.joystick.y, obj);
}
};
game.up = function (x, y, obj) {
// Handle general screen release
if (controlButtons.joystick.active) {
controlButtons.joystick.up(x - controlButtons.joystick.x, y - controlButtons.joystick.y, obj);
}
};
game.move = function (x, y, obj) {
// Handle general screen move
if (controlButtons.joystick.active) {
controlButtons.joystick.move(x - controlButtons.joystick.x, y - controlButtons.joystick.y, obj);
}
};
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
tween(monster, {
mapX: newX,
mapY: newY
}, {
duration: 300,
// 300ms smooth animation
easing: function easing(t) {
return 1 - Math.pow(1 - t, 4);
} // quartOut implementation
});
}
}
}
};
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
@@ -269,58 +269,33 @@
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.speed = 20; // Increased speed for better effect
+ self.startScale = 0.8; // Starting scale for 3D effect
+ self.endScale = 0.1; // Ending scale (smaller as it goes away)
+ self.distance = 0; // Current travel distance
+ self.maxDistance = 2048; // Maximum travel distance
self.visible = false; // Start invisible
self.active = false; // Projectile state
self.update = function (deltaTime) {
if (!self.active) return false;
- // Move projectile based on direction
- if (self.speedX !== undefined && self.speedY !== undefined) {
- // Use directional movement if available
- self.x += self.speedX;
- self.y += self.speedY;
- } else {
- // Default left-to-right movement for backwards compatibility
- self.x -= self.speed;
- }
- // Create pulsating effect for better visibility
- if (LK.ticks % 20 === 0) {
- tween(projectileSprite, {
- scaleX: 0.6,
- scaleY: 0.6
- }, {
- duration: 200,
- onFinish: function onFinish() {
- tween(projectileSprite, {
- scaleX: 0.5,
- scaleY: 0.5
- }, {
- duration: 200
- });
- }
- });
- }
- // Check if projectile has gone off any edge of the screen
- if (self.x < -100 || self.x > 2048 + 100 || self.y < -100 || self.y > 2732 + 100) {
- return true;
- }
- return false;
+ // Move projectile away from player (up the screen)
+ self.y -= self.speed;
+ self.distance += self.speed;
+ // Calculate scale based on distance (shrink as it moves away)
+ var progress = self.distance / self.maxDistance;
+ var currentScale = self.startScale * (1 - progress) + self.endScale * progress;
+ projectileSprite.scale.set(currentScale, currentScale);
+ // Return true if projectile has gone too far
+ return self.distance > self.maxDistance || self.y < -100;
};
- self.fire = function (screenX, screenY, playerDir) {
- if (playerDir !== undefined) {
- // For directional firing from player
- self.x = screenX;
- self.y = screenY;
- self.rotation = playerDir || 0;
- } else {
- // Legacy behavior for backwards compatibility
- self.x = 2048 + 100; // Start off-screen to the right
- self.y = screenY || 2732 / 2; // Default to middle of screen if no Y provided
- }
+ self.fire = function (screenX, screenY) {
+ // Position at bottom of screen
+ self.x = screenX || 2048 / 2; // Center horizontally if no X provided
+ self.y = 2732 - 300; // Start near bottom of screen
+ self.distance = 0; // Reset distance
+ projectileSprite.scale.set(self.startScale, self.startScale); // Reset scale
self.visible = true;
self.active = true;
};
return self;
@@ -983,46 +958,40 @@
// Set attack on cooldown
canAttack = false;
// Play attack sound
LK.getSound('attack').play();
- // Create projectile from player position outward
+ // Create projectile from player perspective (bottom of screen)
var projectile = new Projectile();
- // Set scale for appropriate size
- var scale = 1.0;
- projectile.scale.set(scale, scale);
- // Position at player's position
- projectile.x = player.x * CELL_SIZE * MINI_MAP_SCALE + miniMap.x;
- projectile.y = player.y * CELL_SIZE * MINI_MAP_SCALE + miniMap.y;
- projectile.rotation = player.dir; // Orient in the direction player is facing
- projectile.visible = true;
- projectile.active = true;
- // Modify speed direction based on player's direction
- projectile.speed = -10; // Base speed
- // Calculate directional components
- var dirX = Math.cos(player.dir);
- var dirY = Math.sin(player.dir);
- projectile.speedX = dirX * Math.abs(projectile.speed);
- projectile.speedY = dirY * Math.abs(projectile.speed);
- // Add visual pulse effect to make projectile more visible
+ // Fire the projectile from bottom center
+ projectile.fire(2048 / 2, 2732 - 300);
+ // Add visual flash effect for firing
tween(projectile, {
- alpha: 0.7,
- scaleX: scale * 1.2,
- scaleY: scale * 1.2
+ alpha: 0.7
}, {
- duration: 500,
+ duration: 200,
onFinish: function onFinish() {
tween(projectile, {
- alpha: 1,
- scaleX: scale,
- scaleY: scale
+ alpha: 1
}, {
- duration: 500
+ duration: 200
});
}
});
// Add to the game
projectiles.push(projectile);
game.addChild(projectile);
+ // Create second projectile for more visual effect
+ var projectile2 = new Projectile();
+ // Fire from slightly offset position for wider attack
+ projectile2.fire(2048 / 2 - 200, 2732 - 300);
+ projectiles.push(projectile2);
+ game.addChild(projectile2);
+ // Create third projectile for even more visual effect
+ var projectile3 = new Projectile();
+ // Fire from slightly offset position on the other side
+ projectile3.fire(2048 / 2 + 200, 2732 - 300);
+ projectiles.push(projectile3);
+ game.addChild(projectile3);
// Start cooldown animation for attack button
var attackButton = controlButtons.attack;
attackButton.updateCooldown(0);
// Create a tween for the cooldown visual effect
@@ -1066,13 +1035,17 @@
var hitMonster = false;
var hitMonsterIndex = -1;
for (var j = 0; j < monsters.length; j++) {
var monster = monsters[j];
- // Simple screen space collision check for visible monsters
+ // 3D perspective collision check - distance-based collision window
+ // As projectile gets further away (higher Y position), collision window narrows
if (monster.visible) {
- // Increase hit detection area for directional projectiles
- var hitDistance = 150; // Increased hit detection range
- if (Math.abs(projectile.x - monster.x) < hitDistance && Math.abs(projectile.y - monster.y) < hitDistance) {
+ // Calculate collision window based on projectile's Y position
+ // The higher up the projectile (smaller Y), the smaller the hit window
+ var distanceFactor = 1 - projectile.y / 2732;
+ var hitWindowX = 300 * distanceFactor;
+ // More likely to hit monsters in the center of view
+ if (Math.abs(projectile.x - monster.x) < hitWindowX) {
hitMonster = true;
hitMonsterIndex = j;
break;
}
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