Code edit (1 edits merged)
Please save this source code
User prompt
use projectile scale and monster scale comparison to appropriately detect a projectile hit on the monster
User prompt
that fix didnt work, the projectiles need to travel far enough in real space. currently they don't work properly. refactor projectiles to maintain current functionality but actually hit the enemies
User prompt
adjust projectile monster hit collision detection to correctly use a combination of scale and real world coordinates to detect a hit rather than just overlapping assets
Code edit (5 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: wall.tilePosition is undefined' in or related to this line: 'wall.tilePosition.x = -textureX; // Offset to show correct part' Line Number: 422
Code edit (3 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: strip.updateStrip is not a function' in or related to this line: 'strip.updateStrip(STRIP_WIDTH, wallHeight, rayIdx, 1, distToWall);' Line Number: 1005
Code edit (5 edits merged)
Please save this source code
User prompt
Please fix the bug: 'ReferenceError: floor is not defined' in or related to this line: 'floor.width = stripWidth;' Line Number: 418
Code edit (1 edits merged)
Please save this source code
User prompt
add a tiny bit of random left and right movement to the hand animation
Code edit (16 edits merged)
Please save this source code
User prompt
give a little bit of slow scale pulse and slight float to the hand to give it some life. the current Y value should be the highest it comes ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
give a little bit of slow scale pulse and slight float to the hand to give it some life. the current Y value should be the highest it comes ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (5 edits merged)
Please save this source code
User prompt
I want the projectile to travel from center of the global hand X to 2048/2. Currently the projectile is spawning at 2048/2 because of how the world coordinate tracking works for projectiles. Fix it so that the starting X of projectiles is as I outlined above
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
Change the x coordinates of the projectile spawn to match center x of righthand
User prompt
Move projectile spawn point 200 pixels right
User prompt
Move projectile spawn point 200 pixels right
User prompt
Move projectile spawn point 200 pixels right
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'var handCenterX = rightHand.x;' Line Number: 1135
User prompt
Set projectile spawn point to center X of hand
/****
* 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) {
// Stop propagation to prevent the event from affecting the joystick
obj.stopPropagation = true;
// If joystick is active, store its current state for override
if (controlButtons && controlButtons.joystick && controlButtons.joystick.active) {
joystickOverrideActive = true;
joystickOverrideX = controlButtons.joystick.normalizedX;
joystickOverrideY = controlButtons.joystick.normalizedY;
} else {
// Ensure override is not active if joystick wasn't active at press time
joystickOverrideActive = false;
}
attackAction();
}
};
self.up = function (x, y, obj) {
self.pressed = false;
if (self.direction === 'attack') {
// Clear joystick override when attack button is released
joystickOverrideActive = false;
if (!canAttack) {
buttonSprite.alpha = 0.3; // Show as disabled
} 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
});
self.speed = 20;
self.startScale = 2;
self.endScale = 0.1;
self.distance = 0;
self.maxDistance = 2048;
self.visible = false;
self.active = false;
// Store world position and player state when fired
self.playerDirAtFire = 0;
self.fireWorldX = 0;
self.fireWorldY = 0;
self.fireScreenX = 0;
// Add these properties to track world position
self.worldX = 0;
self.worldY = 0;
self.worldDistance = 0;
self.update = function (deltaTime) {
if (!self.active) {
return false;
}
// Calculate center point of screen height
var centerY = 2732 / 2;
var centerX = 2048 / 2;
// calculate movement progress based on distance
self.distance += self.speed;
var progress = self.distance / self.maxDistance;
// Calculate angle difference between current player direction and fire direction
var dirDifference = player.dir - self.playerDirAtFire;
// Calculate world position of projectile
var projectileWorldDistance = self.distance * 0.05; // Convert pixel distance to world units
self.worldDistance = projectileWorldDistance; // Store for collision detection
self.worldX = self.fireWorldX + Math.cos(self.playerDirAtFire) * projectileWorldDistance;
self.worldY = self.fireWorldY + Math.sin(self.playerDirAtFire) * projectileWorldDistance;
// Existing code for calculating screen position remains the same...
var relativeX = self.worldX - player.x;
var relativeY = self.worldY - player.y;
var angleToProjectile = Math.atan2(relativeY, relativeX);
var screenAngle = angleToProjectile - player.dir;
// Normalize angle to -PI to PI range
while (screenAngle < -Math.PI) {
screenAngle += Math.PI * 2;
}
while (screenAngle > Math.PI) {
screenAngle -= Math.PI * 2;
}
var distanceToProjectile = Math.sqrt(relativeX * relativeX + relativeY * relativeY);
if (Math.abs(screenAngle) < HALF_FOV) {
self.x = centerX + screenAngle / HALF_FOV * (centerX * 0.9);
var startY = 2732 - 500;
self.y = startY * (1 - progress) + centerY * progress;
self.visible = true;
} else {
self.visible = false;
}
var distanceFactor = Math.max(0.3, 1 / (distanceToProjectile + 1.5));
var currentScale = self.startScale * (1 - progress * 0.6) * distanceFactor;
projectileSprite.scale.set(currentScale, currentScale);
return self.distance > self.maxDistance;
};
self.fire = function (screenX, screenY) {
// These values don't actually matter much since they're immediately recalculated
self.x = screenX || 2048 / 2;
self.y = 2732 - 300;
self.distance = 0;
projectileSprite.scale.set(self.startScale, self.startScale);
self.visible = true;
self.active = true;
// Store player's direction at fire time
self.playerDirAtFire = player.dir;
// THIS IS THE IMPORTANT PART - adjust the world starting position
// Add an offset perpendicular to player direction for X adjustment
var perpAngle = player.dir + Math.PI / 2;
var xOffset = 0.4; // Adjust this value to move left/right (negative values move left)
// Add offset behind player for Y adjustment (raising the starting point)
var yOffset = 0.2; // Adjust this value to move up/down (higher values move projectile up)
// Apply both offsets to create the starting world position
self.fireWorldX = player.x + xOffset * Math.cos(perpAngle);
self.fireWorldY = player.y + xOffset * Math.sin(perpAngle) - yOffset;
self.fireScreenX = screenX || 2048 / 2;
};
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 = 500; // 500 millisecond cooldown
// Joystick override state
var joystickOverrideActive = false;
var joystickOverrideX = 0;
var joystickOverrideY = 0;
// UI elements
var miniMap;
var rayCastView;
var healthText;
var scoreText;
var levelText;
var monsterText;
var controlButtons = {};
var playerMarker;
var globalRightHand; // Stores the right hand game object
var activeControlForGlobalHandlers = null; // Tracks which control is active for game.down/up/move
// Create layer variables at the global scope
var gameLayer = new Container();
var projectileLayer = new Container();
var handLayer = new Container();
// Setup game
function setupGame() {
// Set up the layer system using the globally defined variables
// Add layers in the correct order (projectiles below hand)
game.addChild(gameLayer);
game.addChild(projectileLayer);
game.addChild(handLayer);
// Create the rayCast view container
rayCastView = new Container();
gameLayer.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
gameLayer.addChild(miniMap);
// Generate map
generateMap();
// Create player marker
playerMarker = gameLayer.addChild(LK.getAsset('player', {
anchorX: 0.5,
anchorY: 0.5
}));
// Create UI elements
createUI();
// Create control buttons
createControlButtons();
// Start background music
LK.playMusic('dungeon');
}
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);
gameLayer.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);
gameLayer.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;
gameLayer.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;
gameLayer.addChild(controlButtons.joystick);
// Using the global layer system - no need to recreate layers here
// Create attack button more centered on the right side
controlButtons.attack = new ControlButton('attack');
controlButtons.attack.x = 2048 - 400;
controlButtons.attack.y = 2732 - 500;
gameLayer.addChild(controlButtons.attack);
// Add right hand at the bottom right for projectile firing
globalRightHand = LK.getAsset('rightHand', {
anchorX: 0.5,
anchorY: 0.9,
scaleX: 1.2,
scaleY: 1.2
});
globalRightHand.x = 2048 * 0.6; // Position just off right of middle
globalRightHand.y = 2732; // Bottom of the screen
handLayer.addChild(globalRightHand);
// Add pulse and float animation to give the hand some life
var _animateHand = function animateHand() {
// Get a small random x offset between -40 and 40 pixels
var randomXOffset = Math.random() * 80 - 40;
// Store original X position if not set yet
if (globalRightHand.originalX === undefined) {
globalRightHand.originalX = globalRightHand.x;
}
// Slow scale pulse animation with random left/right movement
tween(globalRightHand, {
scaleX: 1.1,
scaleY: 1.1,
x: globalRightHand.originalX + randomXOffset,
y: 2732 - 15 // Float up slightly from base position
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Get another random x offset for the return animation
var returnRandomXOffset = Math.random() * 40 - 20;
tween(globalRightHand, {
scaleX: 1.1,
scaleY: 1.1,
x: globalRightHand.originalX + returnRandomXOffset,
y: 2732 // Return to original position
}, {
duration: 1800,
easing: tween.easeInOut,
onFinish: _animateHand
});
}
});
};
// Start the hand animation
_animateHand();
}
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() {
var joystick = controlButtons.joystick;
var currentJoystickX, currentJoystickY, isJoystickConsideredActive;
if (joystickOverrideActive) {
currentJoystickX = joystickOverrideX;
currentJoystickY = joystickOverrideY;
isJoystickConsideredActive = true; // Movement continues based on pre-attack state
} else if (joystick && joystick.active) {
currentJoystickX = joystick.normalizedX;
currentJoystickY = joystick.normalizedY;
isJoystickConsideredActive = true;
} else {
currentJoystickX = 0;
currentJoystickY = 0;
isJoystickConsideredActive = false;
}
if (isJoystickConsideredActive) {
controls.forward = currentJoystickY < -0.3;
controls.backward = currentJoystickY > 0.3;
controls.left = currentJoystickX < -0.3;
controls.right = currentJoystickX > 0.3;
} else {
controls.forward = false;
controls.backward = false;
controls.left = false;
controls.right = false;
}
// 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;
var currentJoystickX, currentJoystickY, isJoystickConsideredActiveForMovement;
if (joystickOverrideActive) {
currentJoystickX = joystickOverrideX;
currentJoystickY = joystickOverrideY;
isJoystickConsideredActiveForMovement = true;
} else if (joystick && joystick.active) {
currentJoystickX = joystick.normalizedX;
currentJoystickY = joystick.normalizedY;
isJoystickConsideredActiveForMovement = true;
} else {
currentJoystickX = 0;
currentJoystickY = 0;
isJoystickConsideredActiveForMovement = false;
}
// Handle rotation - use x-axis for turning
if (isJoystickConsideredActiveForMovement) {
turnAmount = currentJoystickX * 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 (these are now secondary if joystick was primary)
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 (isJoystickConsideredActiveForMovement) {
moveAmount = -currentJoystickY * 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 (secondary)
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 with sharper angle)
var projectile = new Projectile();
// Get the center x position of the hand
var handCenterX = globalRightHand.x; // Projectiles spawn from the hand's current x-position (center, as anchorX is 0.5)
// Fire the projectile from the hand center aimed toward center of screen
projectile.fire(handCenterX + 400, 2732 - 500);
// 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 projectile layer (below hand)
projectiles.push(projectile);
projectileLayer.addChild(projectile);
// 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];
var remove = projectile.update(deltaTime);
if (remove) {
projectile.destroy();
projectiles.splice(i, 1);
continue;
}
// Improved collision detection
var hitMonster = false;
var hitMonsterIndex = -1;
for (var j = 0; j < monsters.length; j++) {
var monster = monsters[j];
if (monster.visible) {
// Calculate world-space distance between projectile and monster
var worldDx = projectile.worldX - monster.mapX;
var worldDy = projectile.worldY - monster.mapY;
var worldDist = Math.sqrt(worldDx * worldDx + worldDy * worldDy);
// Only consider hits when projectile is close enough in world space
var hitThreshold = 0.75; // Adjust this value to control hit precision
if (worldDist < hitThreshold) {
// Screen-space hit detection for visual accuracy
var distanceFactor = 1 - projectile.y / 2732;
var centerFactor = 1 - Math.abs(monster.x - 2048 / 2) / (2048 / 2);
// Reduced hit window for more accuracy
var hitWindowX = 200 * distanceFactor * (0.7 + centerFactor * 0.3);
if (Math.abs(projectile.x - monster.x) < hitWindowX) {
hitMonster = true;
hitMonsterIndex = j;
break;
}
}
}
}
// Handle monster hit - unchanged
if (hitMonster && hitMonsterIndex !== -1) {
var monster = monsters[hitMonsterIndex];
var killed = monster.takeDamage();
if (killed) {
map[Math.floor(monster.mapY)][Math.floor(monster.mapX)].removeMonster();
monster.destroy();
monsters.splice(hitMonsterIndex, 1);
player.score += 10;
updateUI();
checkLevelCompletion();
}
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;
gameLayer.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;
gameLayer.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;
gameLayer.addChild(gateHintText);
// Remove hint after a few seconds
LK.setTimeout(function () {
gateHintText.destroy();
}, 3000);
}
} else {
// Show hint text about defeating monsters first
if (gate && gate.visible && LK.ticks % 300 === 0) {
var monsterHintText = new Text2('Defeat all monsters to activate the exit!', {
size: 60,
fill: 0xFF5555
});
monsterHintText.anchor.set(0.5, 0.5);
monsterHintText.x = 2048 / 2;
monsterHintText.y = 200;
gameLayer.addChild(monsterHintText);
// Remove hint after a few seconds
LK.setTimeout(function () {
monsterHintText.destroy();
}, 3000);
}
}
}
function updateMiniMap() {
// Update player marker position on minimap
playerMarker.x = miniMap.x + player.x * CELL_SIZE * MINI_MAP_SCALE;
playerMarker.y = miniMap.y + player.y * CELL_SIZE * MINI_MAP_SCALE;
// Draw player direction indicator
var dirX = Math.cos(player.dir) * 15;
var dirY = Math.sin(player.dir) * 15;
}
// Game update method
game.update = function () {
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) {
activeControlForGlobalHandlers = null; // Reset at the start of a new touch
var eventObjForAttack = Object.assign({}, obj); // Fresh event object for attack button
eventObjForAttack.stopPropagation = false;
var attackBtn = controlButtons.attack;
var attackBtnLocalX = x - attackBtn.x;
var attackBtnLocalY = y - attackBtn.y;
// Use the same generous hit radius as in the original code
var attackBtnHitRadius = 150 * 3.5;
var attackBtnDistSq = attackBtnLocalX * attackBtnLocalX + attackBtnLocalY * attackBtnLocalY;
if (attackBtnDistSq <= attackBtnHitRadius * attackBtnHitRadius) {
activeControlForGlobalHandlers = attackBtn;
attackBtn.down(attackBtnLocalX, attackBtnLocalY, eventObjForAttack);
// If the touch is within the attack button's area, it's considered handled by the attack button,
// regardless of whether it internally decided to stop propagation (e.g. attack on cooldown).
// This prevents the joystick from also processing this touch.
return;
}
// If we reach here, the touch was NOT on the attack button.
// Now check for joystick interaction using original quadrant check.
var joystick = controlButtons.joystick;
if (x < 800 && y > 2732 - 800) {
activeControlForGlobalHandlers = joystick;
var eventObjForJoystick = Object.assign({}, obj); // Fresh event object for joystick
eventObjForJoystick.stopPropagation = false;
// Pass local coordinates to joystick.down
joystick.down(x - joystick.x, y - joystick.y, eventObjForJoystick);
}
};
game.up = function (x, y, obj) {
if (activeControlForGlobalHandlers === controlButtons.attack) {
var attackBtnLocalX = x - controlButtons.attack.x;
var attackBtnLocalY = y - controlButtons.attack.y;
controlButtons.attack.up(attackBtnLocalX, attackBtnLocalY, obj);
} else if (activeControlForGlobalHandlers === controlButtons.joystick) {
// Only call joystick.up if the joystick itself believes it's active
if (controlButtons.joystick.active) {
var joystickLocalX = x - controlButtons.joystick.x;
var joystickLocalY = y - controlButtons.joystick.y;
controlButtons.joystick.up(joystickLocalX, joystickLocalY, obj);
}
}
activeControlForGlobalHandlers = null; // Reset after touch ends
};
game.move = function (x, y, obj) {
// Handle general screen move
if (activeControlForGlobalHandlers === controlButtons.joystick) {
// Only call joystick.move if the joystick itself believes it's active
if (controlButtons.joystick.active) {
var joystickLocalX = x - controlButtons.joystick.x;
var joystickLocalY = y - controlButtons.joystick.y;
controlButtons.joystick.move(joystickLocalX, joystickLocalY, obj);
}
}
// Attack button doesn't typically have a .move action, so no changes needed for it here.
};
var MonsterAI = {
moveTowardsPlayer: function moveTowardsPlayer(monster, playerX, playerY, map) {
// If monster is not visible (no line of sight or out of FOV/range), it shouldn't attempt to move.
// monster.visible is set by the renderEntities function, which includes line-of-sight checks.
if (!monster.visible) {
return;
}
// 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 (aggro range)
// This check is in addition to the visibility/LoS check.
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 8) {
return;
} // Don't move if too far away (but still visible)
// 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
@@ -281,21 +281,24 @@
var projectileSprite = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
- self.sprite = projectileSprite; // Expose sprite for scale access
- self.speed = 20; // Increased speed for better effect
- self.startScale = 2; // 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.speed = 20;
+ self.startScale = 2;
+ self.endScale = 0.1;
+ self.distance = 0;
+ self.maxDistance = 2048;
+ self.visible = false;
+ self.active = false;
// Store world position and player state when fired
- self.playerDirAtFire = 0; // Player direction at time of firing
- self.fireWorldX = 0; // World X position at fire time
- self.fireWorldY = 0; // World Y position at fire time
- self.fireScreenX = 0; // Screen X position at fire time
+ self.playerDirAtFire = 0;
+ self.fireWorldX = 0;
+ self.fireWorldY = 0;
+ self.fireScreenX = 0;
+ // Add these properties to track world position
+ self.worldX = 0;
+ self.worldY = 0;
+ self.worldDistance = 0;
self.update = function (deltaTime) {
if (!self.active) {
return false;
}
@@ -308,46 +311,35 @@
// Calculate angle difference between current player direction and fire direction
var dirDifference = player.dir - self.playerDirAtFire;
// Calculate world position of projectile
var projectileWorldDistance = self.distance * 0.05; // Convert pixel distance to world units
- var projectileWorldX = self.fireWorldX + Math.cos(self.playerDirAtFire) * projectileWorldDistance;
- var projectileWorldY = self.fireWorldY + Math.sin(self.playerDirAtFire) * projectileWorldDistance;
- // Convert projectile world position to screen position based on player's current position and direction
- var relativeX = projectileWorldX - player.x;
- var relativeY = projectileWorldY - player.y;
- // Calculate angle from player to projectile in world space
+ self.worldDistance = projectileWorldDistance; // Store for collision detection
+ self.worldX = self.fireWorldX + Math.cos(self.playerDirAtFire) * projectileWorldDistance;
+ self.worldY = self.fireWorldY + Math.sin(self.playerDirAtFire) * projectileWorldDistance;
+ // Existing code for calculating screen position remains the same...
+ var relativeX = self.worldX - player.x;
+ var relativeY = self.worldY - player.y;
var angleToProjectile = Math.atan2(relativeY, relativeX);
- // Adjust angle based on player's current direction
var screenAngle = angleToProjectile - player.dir;
// Normalize angle to -PI to PI range
while (screenAngle < -Math.PI) {
screenAngle += Math.PI * 2;
}
while (screenAngle > Math.PI) {
screenAngle -= Math.PI * 2;
}
- // Calculate distance to projectile in world space
var distanceToProjectile = Math.sqrt(relativeX * relativeX + relativeY * relativeY);
- // Calculate screen position
if (Math.abs(screenAngle) < HALF_FOV) {
- // Only show if in field of view
- // Calculate x position based on angle in FOV
self.x = centerX + screenAngle / HALF_FOV * (centerX * 0.9);
- // Calculate y position (height) based on distance and progress
- // The further away, the closer to the center of the screen (horizon)
- var startY = 2732 - 500; // Starting position (bottom of screen)
+ var startY = 2732 - 500;
self.y = startY * (1 - progress) + centerY * progress;
self.visible = true;
} else {
self.visible = false;
}
- // Calculate scale based on both distance and progress
- // Reduce the impact of distance to make scaling more gradual
var distanceFactor = Math.max(0.3, 1 / (distanceToProjectile + 1.5));
- // Increase the progress factor from 0.4 to 0.6 to speed up scaling
var currentScale = self.startScale * (1 - progress * 0.6) * distanceFactor;
projectileSprite.scale.set(currentScale, currentScale);
- // Return true if projectile has gone too far based on distance
return self.distance > self.maxDistance;
};
self.fire = function (screenX, screenY) {
// These values don't actually matter much since they're immediately recalculated
@@ -436,12 +428,8 @@
/****
* Game Code
****/
// Game constants
-var PROJECTILE_ORIGINAL_WIDTH = 500;
-var PROJECTILE_ORIGINAL_HEIGHT = 535.56;
-var MONSTER_ORIGINAL_WIDTH = 100;
-var MONSTER_ORIGINAL_HEIGHT = 170;
var MAP_SIZE = 16;
var CELL_SIZE = 20;
var MINI_MAP_SCALE = 1;
var STRIP_WIDTH = 8;
@@ -1204,66 +1192,52 @@
}
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
+ // Improved collision detection
var hitMonster = false;
var hitMonsterIndex = -1;
for (var j = 0; j < monsters.length; j++) {
var monster = monsters[j];
- // Collision check based on visual scales of projectile and monster
- if (monster.visible && projectile.sprite) {
- // Ensure sprite is accessible
- var projectileVisualHalfWidth = PROJECTILE_ORIGINAL_WIDTH * projectile.sprite.scale.x / 2;
- var monsterVisualHalfWidth = MONSTER_ORIGINAL_WIDTH * monster.scale.x / 2;
- // Use a leniency factor to adjust hit difficulty.
- // 1.0 means exact overlap of their visual edges.
- // Values < 1.0 make it harder (hitbox smaller than visual).
- // Values > 1.0 make it easier (hitbox larger than visual).
- var collisionLeniencyFactor = 0.8; // Tuned for gameplay feel, e.g., 80% of combined half-widths.
- var requiredSeparation = (projectileVisualHalfWidth + monsterVisualHalfWidth) * collisionLeniencyFactor;
- if (Math.abs(projectile.x - monster.x) < requiredSeparation) {
- // Optional: A Y-axis check could be added for more precision,
- // especially if vertical aiming becomes more significant.
- // var projectileVisualHalfHeight = (PROJECTILE_ORIGINAL_HEIGHT * projectile.sprite.scale.y) / 2;
- // var monsterVisualHalfHeight = (MONSTER_ORIGINAL_HEIGHT * monster.scale.y) / 2;
- // var ySeparation = (projectileVisualHalfHeight + monsterVisualHalfHeight) * collisionLeniencyFactor;
- // if (Math.abs(projectile.y - monster.y) < ySeparation) {
- // hitMonster = true;
- // hitMonsterIndex = j;
- // break;
- // }
- hitMonster = true;
- hitMonsterIndex = j;
- break;
+ if (monster.visible) {
+ // Calculate world-space distance between projectile and monster
+ var worldDx = projectile.worldX - monster.mapX;
+ var worldDy = projectile.worldY - monster.mapY;
+ var worldDist = Math.sqrt(worldDx * worldDx + worldDy * worldDy);
+ // Only consider hits when projectile is close enough in world space
+ var hitThreshold = 0.75; // Adjust this value to control hit precision
+ if (worldDist < hitThreshold) {
+ // Screen-space hit detection for visual accuracy
+ var distanceFactor = 1 - projectile.y / 2732;
+ var centerFactor = 1 - Math.abs(monster.x - 2048 / 2) / (2048 / 2);
+ // Reduced hit window for more accuracy
+ var hitWindowX = 200 * distanceFactor * (0.7 + centerFactor * 0.3);
+ if (Math.abs(projectile.x - monster.x) < hitWindowX) {
+ hitMonster = true;
+ hitMonsterIndex = j;
+ break;
+ }
}
}
}
- // Handle monster hit
+ // Handle monster hit - unchanged
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);
}
}
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