import { CONFIG, COLORS } from './config.js'; const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); let tileSize = canvas.width / CONFIG.GRID_SIZE; let highScore = localStorage.getItem('highScore') || 0; const player = { x: 0, y: 0, health: CONFIG.PLAYER_HEALTH, score: 0, damage: CONFIG.PLAYER_BASE_DAMAGE, totalDamageDone: 0, totalDamageTaken: 0, cellsTraveled: 0, killCount: 0, itemsCollected: 0, }; const exit = { x: Math.floor(Math.random() * CONFIG.GRID_SIZE), y: Math.floor(Math.random() * CONFIG.GRID_SIZE) }; let walls = []; let enemies = []; let items = []; let combatDots = {}; function isValidMove(newX, newY) { return ( newX >= 0 && newX < CONFIG.GRID_SIZE && newY >= 0 && newY < CONFIG.GRID_SIZE && !walls.some(wall => wall.x === newX && wall.y === newY) ); } function generateExit() { let distance = 0; do { exit.x = Math.floor(Math.random() * CONFIG.GRID_SIZE); exit.y = Math.floor(Math.random() * CONFIG.GRID_SIZE); distance = Math.abs(exit.x - player.x) + Math.abs(exit.y - player.y); } while (distance < 4); } function generateEnemies() { enemies = []; // Generate between 0 and MAX_ENEMIES_ON_LEVEL enemies if the player's score is 4 or lower // Generate between MIN_ENEMIES_ON_LEVEL and MAX_ENEMIES_ON_LEVEL enemies if the player's score is 5 or higher const numEnemies = player.score > 4 ? Math.floor(Math.random() * (CONFIG.MAX_ENEMIES_ON_LEVEL - CONFIG.MIN_ENEMIES_ON_LEVEL + 1)) + CONFIG.MIN_ENEMIES_ON_LEVEL : Math.floor(Math.random() * (CONFIG.MAX_ENEMIES_ON_LEVEL + 1)); for (let i = 0; i < numEnemies; i++) { let enemyX, enemyY; do { enemyX = Math.floor(Math.random() * CONFIG.GRID_SIZE); enemyY = Math.floor(Math.random() * CONFIG.GRID_SIZE); } while ( (enemyX === player.x && enemyY === player.y) || (enemyX === exit.x && enemyY === exit.y) || walls.some(wall => wall.x === enemyX && wall.y === enemyY) || (Math.abs(enemyX - player.x) + Math.abs(enemyY - player.y) < 2) // Ensure enemy is at least 2 spaces away from player ); enemies.push({ color: COLORS.enemy, x: enemyX, y: enemyY, health: Math.floor(Math.random() * (CONFIG.MAX_ENEMY_HEALTH - CONFIG.MIN_ENEMY_HEALTH + 1)) + CONFIG.MIN_ENEMY_HEALTH }); } // Generate a boss enemy every ENEMY_BOSS_OCCURRENCE levels if (player.score % CONFIG.ENEMY_BOSS_OCCURRENCE === 0 && player.score > 0) { let bossX, bossY; do { bossX = exit.x; // Boss enemies always appear at the exit bossY = exit.y; // This ensures that they're not in little rooms that the player can't reach } while ( (Math.abs(bossX - player.x) + Math.abs(bossY - player.y) < 2) // Ensure boss is at least 2 spaces away from player ); enemies.push({ isBoss: true, color: COLORS.boss, x: bossX, y: bossY, health: CONFIG.MAX_ENEMY_HEALTH + 2 }); } } function generateWallsNaive() { walls = []; let numWalls = Math.floor(Math.random() * (CONFIG.WALLS_MAX - CONFIG.WALLS_MIN + 1)) + CONFIG.WALLS_MIN; while (walls.length < numWalls) { const wallX = Math.floor(Math.random() * CONFIG.GRID_SIZE); const wallY = Math.floor(Math.random() * CONFIG.GRID_SIZE); if ( (wallX === player.x && wallY === player.y) || // Check that a wall is not placed on the starting position (wallX === exit.x && wallY === exit.y) || // Check that a wall is not placed on the exit enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) || // Check that a wall is not placed on any enemies items.some(item => item.x === wallX && item.y === wallY) // Check that a wall is not placed on any items ) continue; if (!walls.some(wall => wall.x === wallX && wall.y === wallY)) { walls.push({ x: wallX, y: wallY }); } } if (!isPassable()) { generateWallsNaive(); } } function generateWallsDrunkardsWalk() { walls = []; const numWalls = Math.floor(Math.random() * (CONFIG.WALLS_MAX - CONFIG.WALLS_MIN + 1)) + CONFIG.WALLS_MIN; let wallX = Math.floor(Math.random() * CONFIG.GRID_SIZE); let wallY = Math.floor(Math.random() * CONFIG.GRID_SIZE); while (walls.length < numWalls) { if ( (wallX !== player.x || wallY !== player.y) && // Check that a wall is not placed on the starting position (wallX !== exit.x || wallY !== exit.y) && // Check that a wall is not placed on the exit !enemies.some(enemy => enemy.x === wallX && enemy.y === wallY) && // Check that a wall is not placed on any enemies !items.some(item => item.x === wallX && item.y === wallY) && // Check that a wall is not placed on any items !walls.some(wall => wall.x === wallX && wall.y === wallY) // Check that a wall is not placed on an existing wall ) { walls.push({ x: wallX, y: wallY }); } // Randomly move to a neighboring cell const direction = Math.floor(Math.random() * 4); switch (direction) { case 0: wallX = Math.max(0, wallX - 1); break; // Move left case 1: wallX = Math.min(CONFIG.GRID_SIZE - 1, wallX + 1); break; // Move right case 2: wallY = Math.max(0, wallY - 1); break; // Move up case 3: wallY = Math.min(CONFIG.GRID_SIZE - 1, wallY + 1); break; // Move down } } if (!isPassable()) { generateWallsDrunkardsWalk(); } } function generateWallsCellularAutomata() { walls = []; const map = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(0)); // Initialize a map with random walls for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { if (Math.random() < 0.4) { map[x][y] = 1; } } } for (let i = 0; i < 4; i++) { // Create a new map to store the next state of the cellular automata const newMap = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(0)); // Iterate over each cell in the grid for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { // Count the number of neighboring walls const neighbors = countNeighbors(map, x, y); if (map[x][y] === 1) { // If the cell is a wall, it stays a wall if it has 4 or more neighbors newMap[x][y] = neighbors >= 4 ? 1 : 0; } else { // If the cell is empty, it turn into a wall if it has 5 or more neighbors newMap[x][y] = neighbors >= 5 ? 1 : 0; } } } // Update the original map with the new state map.forEach((row, x) => row.forEach((cell, y) => map[x][y] = newMap[x][y])); } // Convert map to walls array for (let x = 0; x < CONFIG.GRID_SIZE; x++) { for (let y = 0; y < CONFIG.GRID_SIZE; y++) { if (map[x][y] === 1 && (x !== player.x || y !== player.y) && (x !== exit.x || y !== exit.y) && !enemies.some(enemy => enemy.x === x && enemy.y === y) && !items.some(item => item.x === x && item.y === y)) { walls.push({ x, y }); } } } if (!isPassable()) { generateWallsCellularAutomata(); } } function countNeighbors(map, x, y) { let count = 0; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < CONFIG.GRID_SIZE && ny >= 0 && ny < CONFIG.GRID_SIZE) { count += map[nx][ny]; } else { count++; // Consider out-of-bounds bits as walls } } } return count; } function generateWallsRSP() { walls = []; function splitNode(x, y, width, height) { const splitHorizontally = Math.random() > 0.5; const max = (splitHorizontally ? height : width) - CONFIG.MIN_ROOM_SIZE; if (max <= CONFIG.MIN_ROOM_SIZE) return [{ x, y, width, height }]; const split = Math.floor(Math.random() * (max - CONFIG.MIN_ROOM_SIZE)) + CONFIG.MIN_ROOM_SIZE; if (splitHorizontally) { return [ ...splitNode(x, y, width, split), ...splitNode(x, y + split, width, height - split) ]; } else { return [ ...splitNode(x, y, split, height), ...splitNode(x + split, y, width - split, height) ]; } } function createRoom(node) { const roomWidth = Math.floor(Math.random() * (node.width - 1)) + 1; const roomHeight = Math.floor(Math.random() * (node.height - 1)) + 1; const roomX = node.x + Math.floor(Math.random() * (node.width - roomWidth)); const roomY = node.y + Math.floor(Math.random() * (node.height - roomHeight)); return { x: roomX, y: roomY, width: roomWidth, height: roomHeight }; } const nodes = splitNode(0, 0, CONFIG.GRID_SIZE, CONFIG.GRID_SIZE); const rooms = nodes.map(createRoom); rooms.forEach(room => { for (let x = room.x; x < room.x + room.width; x++) { for (let y = room.y; y < room.y + room.height; y++) { if ( (x !== player.x || y !== player.y) && (x !== exit.x || y !== exit.y) && !enemies.some(enemy => enemy.x === x && enemy.y === y) && !items.some(item => item.x === x && item.y === y) ) { walls.push({ x, y }); } } }}); if (!isPassable()) { generateWallsRSP(); } } function generateWalls() { const wallGenerators = [ { name: 'RSP Tree', func: generateWallsRSP }, { name: 'Naive', func: generateWallsNaive }, { name: 'Cellular Automata', func: generateWallsCellularAutomata }, { name: 'Drunkard\'s Walk', func: generateWallsDrunkardsWalk } ]; const randomIndex = Math.floor(Math.random() * wallGenerators.length); const selectedGenerator = wallGenerators[randomIndex]; console.log(`Wall generator: ${selectedGenerator.name}`); selectedGenerator.func(); } function generateItems() { items = []; const numItems = Math.floor(Math.random() * (CONFIG.ITEMS_MAX - CONFIG.ITEMS_MIN + 1)) + CONFIG.ITEMS_MIN; for (let i = 0; i < numItems; i++) { let itemX, itemY; do { itemX = Math.floor(Math.random() * CONFIG.GRID_SIZE); itemY = Math.floor(Math.random() * CONFIG.GRID_SIZE); } while ( (itemX === player.x && itemY === player.y) || (itemX === exit.x && itemY === exit.y) || walls.some(wall => wall.x === itemX && wall.y === itemY) || enemies.some(enemy => enemy.x === itemX && enemy.y === itemY) || items.some(item => item.x === itemX && item.y === itemY) ); const itemType = Math.random() < 0.5 ? 'diamond' : 'pentagon'; // 50% chance for each type items.push({ x: itemX, y: itemY, type: itemType }); } } function isPassable() { const visited = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(false)); function dfs(x, y) { if (x < 0 || x >= CONFIG.GRID_SIZE || y < 0 || y >= CONFIG.GRID_SIZE) return false; // Are the coordinates in bounds? if (visited[x][y]) return false; if (walls.some(wall => wall.x === x && wall.y === y)) return false; visited[x][y] = true; if (x === exit.x && y === exit.y) return true; return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1); } return dfs(player.x, player.y); } function drawGrid() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = COLORS.grid; for (let row = 0; row < CONFIG.GRID_SIZE; row++) { for (let col = 0; col < CONFIG.GRID_SIZE; col++) { ctx.strokeRect(col * tileSize, row * tileSize, tileSize, tileSize); } } } function drawExit() { const x = exit.x * tileSize + tileSize / 2; const y = exit.y * tileSize + tileSize / 2; ctx.beginPath(); ctx.moveTo(x, y - tileSize / 3); ctx.lineTo(x + tileSize / 3, y + tileSize / 3); ctx.lineTo(x - tileSize / 3, y + tileSize / 3); ctx.closePath(); ctx.fillStyle = COLORS.exit; ctx.fill(); } function drawWalls() { ctx.fillStyle = COLORS.walls; walls.forEach(wall => { ctx.fillRect(wall.x * tileSize, wall.y * tileSize, tileSize, tileSize); }); } function drawItems() { items.forEach(item => { const x = item.x * tileSize + tileSize / 2; const y = item.y * tileSize + tileSize / 2; ctx.fillStyle = item.type === 'diamond' ? COLORS.diamond : COLORS.pentagon; ctx.beginPath(); if (item.type === 'diamond') { ctx.moveTo(x, y - tileSize / 4); ctx.lineTo(x + tileSize / 4, y); ctx.lineTo(x, y + tileSize / 4); ctx.lineTo(x - tileSize / 4, y); } else { const sides = 5; const radius = tileSize / 4; for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; const pointX = x + radius * Math.cos(angle); const pointY = y + radius * Math.sin(angle); if (i === 0) ctx.moveTo(pointX, pointY); else ctx.lineTo(pointX, pointY); } } ctx.closePath(); ctx.fill(); }); } function drawCharacterBorder(x, y, radius, damageTaken) { const dashLength = 5; const gapLength = Math.max(1, damageTaken * 2); // More damage, larger gaps ctx.lineWidth = 2; ctx.strokeStyle = '#2d2d2d'; ctx.setLineDash([dashLength, gapLength]); ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.stroke(); ctx.setLineDash([]); // Reset to a solid line } function drawEnemies() { enemies.forEach(enemy => { const x = enemy.x * tileSize + tileSize / 2; const y = enemy.y * tileSize + tileSize / 2; const opacity = enemy.health / CONFIG.MAX_ENEMY_HEALTH; // Opacity based on health const radius = tileSize / 3; const damageTaken = CONFIG.MAX_ENEMY_HEALTH - enemy.health; ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.fillStyle = `${enemy.color}${opacity})`; ctx.fill(); drawCharacterBorder(x, y, radius, damageTaken); }); } function drawPlayer() { const x = player.x * tileSize + tileSize / 2; const y = player.y * tileSize + tileSize / 2; const radius = tileSize / 3; const playerOpacity = player.health / CONFIG.PLAYER_HEALTH; // Opacity based on health ctx.beginPath(); for (let i = 0; i < 6; i++) { const angle = (Math.PI / 3) * i; const hexX = x + radius * Math.cos(angle); const hexY = y + radius * Math.sin(angle); if (i === 0) { ctx.moveTo(hexX, hexY); } else { ctx.lineTo(hexX, hexY); } } ctx.closePath(); ctx.fillStyle = `${COLORS.player}${playerOpacity})`; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#2d2d2d'; ctx.stroke(); } function drawCombatDots() { for (const key in combatDots) { const [cellX, cellY] = key.split(',').map(Number); const dots = combatDots[key]; dots.forEach(dot => { ctx.beginPath(); ctx.arc(cellX * tileSize + dot.x, cellY * tileSize + dot.y, 2, 0, Math.PI * 2); ctx.fillStyle = dot.color; ctx.fill(); ctx.closePath(); }); } } function handleItemCollection() { const collectedItem = items.find(item => item.x === player.x && item.y === player.y); if (collectedItem) { player.itemsCollected++; if (collectedItem.type === 'diamond') { player.damage += 3; console.log("Collected diamond! +3 damage on this level."); } else if (collectedItem.type === 'pentagon') { const healAmount = Math.floor(Math.random() * 2) + 1; player.health = Math.min(player.health + healAmount, CONFIG.PLAYER_MAX_HEALTH); console.log("Collected pentagon! Healed " + healAmount + " health."); } items = items.filter(item => item !== collectedItem); // Remove collected item } } function addCombatDots(x, y, color) { const cellsToFill = [ [x, y], // Center [x - 1, y], // Left [x + 1, y], // Right [x, y - 1], // Up [x, y + 1], // Down [x - 1, y - 1], // Top-left [x + 1, y - 1], // Top-right [x - 1, y + 1], // Bottom-left [x + 1, y + 1] // Bottom-right ]; cellsToFill.forEach(([cellX, cellY]) => { if (cellX >= 0 && cellX < CONFIG.GRID_SIZE && cellY >= 0 && cellY < CONFIG.GRID_SIZE) { const key = `${cellX},${cellY}`; if (!combatDots[key]) { combatDots[key] = []; } for (let i = 0; i < CONFIG.DOTS_PER_HIT; i++) { combatDots[key].push({ x: Math.random() * tileSize, y: Math.random() * tileSize, color: color }); } } }); } function movePlayer(dx, dy) { const newX = player.x + dx; const newY = player.y + dy; if (isValidMove(newX, newY) && !enemies.some(enemy => enemy.x === newX && enemy.y === newY)) { if (newX !== player.x || newY !== player.y) player.cellsTraveled++; player.x = newX; player.y = newY; handleItemCollection(); // Did the player collect an item? checkPlayerAtExit(); // Did the player get to the exit after moving? } else { // If an enemy is in the target cell, player stays put and does combat const enemyInTargetCell = enemies.find(enemy => enemy.x === newX && enemy.y === newY); if (enemyInTargetCell) { handleDamage(player, enemyInTargetCell); } } moveEnemies(); render(); } function moveEnemies() { enemies.forEach(enemy => { const distanceToPlayer = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y); const distanceToExit = Math.abs(enemy.x - exit.x) + Math.abs(enemy.y - exit.y); // FIXME: necessary? // If the enemy is closer to the exit than the player, move towards the exit // Bosses are more aggressive about chasing the player const target = distanceToPlayer <= (enemy.isBoss ? CONFIG.ENEMY_CHASE_DISTANCE + 2 : CONFIG.ENEMY_CHASE_DISTANCE) ? player : exit; const path = findPath(enemy, target); if (path.length > 1) { const nextStep = path[1]; const enemyInNextStep = enemies.find(e => e.x === nextStep.x && e.y === nextStep.y); // Is the next step occupied by an enemy? if (!enemyInNextStep && !(nextStep.x === player.x && nextStep.y === player.y)) { // Move to the next place enemy.x = nextStep.x; enemy.y = nextStep.y; } else if (nextStep.x === player.x && nextStep.y === player.y) { // If the player is in the next step, stay put and do combat handleDamage(player, enemy); } } }); } function findPath(start, goal) { const queue = [{ x: start.x, y: start.y, path: [] }]; const visited = new Set(); visited.add(`${start.x},${start.y}`); while (queue.length > 0) { const { x, y, path } = queue.shift(); const newPath = [...path, { x, y }]; if (x === goal.x && y === goal.y) { return newPath; } const directions = [ { dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 } ]; directions.forEach(({ dx, dy }) => { const newX = x + dx; const newY = y + dy; const key = `${newX},${newY}`; // Check if the new position is within the level and if it is passable if ( newX >= 0 && newX < CONFIG.GRID_SIZE && newY >= 0 && newY < CONFIG.GRID_SIZE && // Have we already been here? !visited.has(key) && // Is it a wall? !walls.some(wall => wall.x === newX && wall.y === newY) && // Is there an enemy already there? !enemies.some(enemy => enemy.x === newX && enemy.y === newY) ) { queue.push({ x: newX, y: newY, path: newPath }); visited.add(key); } }); } return []; } let combatAnimationEnabled = localStorage.getItem('combatAnimationEnabled'); if (combatAnimationEnabled === null) { combatAnimationEnabled = true; // default to on...is that a good idea? localStorage.setItem('combatAnimationEnabled', combatAnimationEnabled); } else { combatAnimationEnabled = combatAnimationEnabled === 'true'; } document.getElementById('toggleShake').textContent = combatAnimationEnabled ? 'Turn Shake Off' : 'Turn Shake On'; function toggleShake() { combatAnimationEnabled = !combatAnimationEnabled; localStorage.setItem('combatAnimationEnabled', combatAnimationEnabled); document.getElementById('toggleShake').textContent = combatAnimationEnabled ? 'Turn Shake Off' : 'Turn Shake On'; } function combatAnimation() { const canvas = document.getElementById('gameCanvas'); canvas.classList.add('shake'); canvas.addEventListener('animationend', () => { canvas.classList.remove('shake'); }, { once: true }); } function handleDamage(player, enemy) { const enemyMisses = Math.random() < 0.5; // 50% chance the enemy misses you const cellX = player.x; const cellY = player.y; if (!enemyMisses) { player.health--; player.totalDamageTaken++; addCombatDots(cellX, cellY, COLORS.combatDotPlayer); // Add dots for player damage console.log("Enemy hit! Player health: " + player.health); } else { console.log("Enemy missed!"); } enemy.health = enemy.health - player.damage; player.totalDamageDone++; addCombatDots(cellX, cellY, enemy.isBoss ? COLORS.combatDotBoss : COLORS.combatDotEnemy); // Add dots for enemy damage console.log("Player hit! Enemy health: " + enemy.health); if (combatAnimationEnabled) { combatAnimation(); // Trigger the shake animation } if (enemy.health <= 0) { player.killCount++; enemies = enemies.filter(e => e !== enemy); if (enemy.isBoss) { // Defeating a boss restores 3 player health player.health = Math.min(player.health + 3, CONFIG.PLAYER_MAX_HEALTH); console.log("Defeated a boss! Healed " + 3 + " health."); } } if (player.health <= 0) { if (player.score > highScore) { highScore = player.score; localStorage.setItem('highScore', highScore); } alert(`Score: ${player.score}\nDistance Traveled: ${player.cellsTraveled}\nTotal Damage Dealt: ${player.totalDamageDone}\nTotal Damage Received: ${player.totalDamageTaken}\nCircles Vanquished: ${player.killCount}\n\nHigh Score: ${highScore}`); resetGame(); } } function resetGame() { const canvas = document.getElementById('gameCanvas'); if (canvas.classList.contains('shake')) { canvas.classList.remove('shake'); } player.health = CONFIG.PLAYER_HEALTH; player.damage = CONFIG.PLAYER_BASE_DAMAGE; player.bonusDamageTurns = 0; player.totalDamageDone = 0; player.totalDamageTaken = 0; player.cellsTraveled = 0; player.score = 0; player.killCount = 0; player.itemsCollected = 0; player.x = 0; player.y = 0; combatDots = {}; generateExit(); generateEnemies(); generateItems(); generateWalls(); render(); } function checkPlayerAtExit() { if (player.x === exit.x && player.y === exit.y) { player.score += 1; player.damage = CONFIG.PLAYER_BASE_DAMAGE; console.groupCollapsed("Level complete! " + player.score); console.log("Score: " + player.score); console.log("Current health: " + player.health); console.log("Distance Traveled: " + player.cellsTraveled); console.log("Total Damage Dealt: " + player.totalDamageDone); console.log("Total Damage Received: " + player.totalDamageTaken); console.log("Circles Vanquished: " + player.killCount); console.log("Items Collected: " + player.itemsCollected); console.log("High Score: " + highScore); console.groupEnd(); combatDots = {}; generateExit(); generateEnemies(); generateItems(); generateWalls(); render(); } } function render() { drawGrid(); drawPlayer(); drawExit(); drawItems(); drawEnemies(); drawWalls(); drawCombatDots(); } const directionMap = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0], w: [0, -1], s: [0, 1], a: [-1, 0], d: [1, 0], h: [-1, 0], j: [0, 1], k: [0, -1], l: [1, 0] }; document.addEventListener('keydown', (e) => { const direction = directionMap[e.key]; if (direction) { movePlayer(...direction); checkPlayerAtExit(); render(); } }); let touchStartX = 0; let touchStartY = 0; canvas.addEventListener('touchstart', (e) => { e.preventDefault(); // Prevent scrolling on touchstart touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }); canvas.addEventListener('touchend', (e) => { e.preventDefault(); // Prevent scrolling on touchend const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const dx = touchEndX - touchStartX; const dy = touchEndY - touchStartY; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal swipe if (dx > 0) { movePlayer(1, 0); // Swipe right } else { movePlayer(-1, 0); // Swipe left } } else { // Vertical swipe if (dy > 0) { movePlayer(0, 1); // Swipe down } else { movePlayer(0, -1); // Swipe up } } render(); }, { passive: false }); // TIL you can use passive set to false to help make preventDefault actually work? Feels like superstition const resizeCanvas = () => { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; tileSize = canvas.width / CONFIG.GRID_SIZE; // Update tile size based on canvas dimensions render(); }; window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Initial level setup generateExit(); generateEnemies(); generateItems(); generateWalls(); render();