about summary refs log blame commit diff stats
path: root/html/broughlike/broughlike.js
blob: 0397f9b78e971beeb42f7c90e5ceafab1b5a4044 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                                             
 








                                                                                                                             
 
                                                       


                
                                 
             
                                      






                        
                                                                                                                  






                                  

                                               






                                                               

                                                              








                                                                                                                  

                                                                                                                                
                                      

                           

                                                                  





                                                                                                                                 








                                                                                                                                                                


                                                               
                                                                                






                                                                                                                              








                                                            




                               
                                                                                                            
                                     

                                                                   












                                                                                                                                        
                                                           





                                       


                                                                                                              















                                                                                                                                     
                                                                                       
                                                                             
                                                                                      


     
                                                           





                                          
                                                                                          

                                         

                                                    







                                                                            
                                                                                                 

                                             

                                                        

















                                                                                              

                                                    









                                                                      
                                                           










                                               
                                                                                       










                                                                


                                                      
                                                                                
 
                                                                          
 
                                                                                                      





















                                                                                      
                                                                      















                                                                         
                                                           


















                                                                           
                                                                                                              


                                        

                                                                 







                                                                                                  

                                                               


     
                                                                      

                                                                                                                                           
                        
                                                                                                                             




                                                                                                                                 
     

                               
 

                                                     
                      
                                  

                                                          



































































                                                                                   
                                                                                          
                                    
                                                                   













                                                       
                                                                                          









































                                                                                           
                                                                                           



















                                                                                      
                                                                                               



                                            
                                                           
































                                                                                                  
                                                                                                            


                                                                                    
                                                                                                                                          













































                                                                                                

                                                       































































                                                                                                                          
                                                   

                                                        
                                                                                  


                                                                                                    

















                                                                                                                                                                                                                                                           

                                              










                                
                    

                      





                                                     
                                                  











                                                                         
                        

                          












































































                                                                                                                                  
                                                                                              







                                                
                

                  
        
import { CONFIG, COLORS } from './config.js';

// FIXME: canvas, ctx, and tileSize are all regularly accessed. 
// I'd like to refactor this to be more modular so that these can all be contained to the CONFIG object or something similar.
let { ctx, canvas, tileSize } = initializeCanvasContext();
function initializeCanvasContext() {
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    let tileSize = canvas.width / CONFIG.GRID_SIZE;
    return { ctx, canvas, tileSize };
}

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;) {
        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
        );
        if (isReachable(player.x, player.y, enemyX, enemyY)) {
            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
            });
            i++; // Only increment i if the enemy is reachable and actually placed on the board, this avoids levels with fewer enemies than MIN_ENEMIES_ON_LEVEL
        }
    }

    // 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
        );
        if (isReachable(player.x, player.y, bossX, bossY)) {
            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 (!isReachable(player.x, player.y, exit.x, exit.y)) {
        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 (!isReachable(player.x, player.y, exit.x, exit.y)) {
        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 (!isReachable(player.x, player.y, exit.x, exit.y)) {
        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 (!isReachable(player.x, player.y, exit.x, exit.y)) {
        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
        if (isReachable(player.x, player.y, itemX, itemY))
            items.push({ x: itemX, y: itemY, type: itemType });
    }
}

// Checks to see if there's a path between any two points on the level
function isReachable(startX, startY, targetX, targetY) {
    const visited = Array(CONFIG.GRID_SIZE).fill().map(() => Array(CONFIG.GRID_SIZE).fill(false)); // Initialize a 2D array of false values
    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;                                                    // Have we already visited this cell?
        if (walls.some(wall => wall.x === x && wall.y === y)) return false;                 // Is there a wall here?
        visited[x][y] = true;                                                               // Mark this cell as visited
        if (x === targetX && y === targetY) return true;                                    // Have we reached the target?
        return dfs(x + 1, y) || dfs(x - 1, y) || dfs(x, y + 1) || dfs(x, y - 1);            // Recursively check neighbors
    }
    return dfs(startX, startY);
}

function drawGrid() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.lineWidth = 2;
    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. Player health " + player.health);
        } else if (Math.random() < 0.25) {
            player.health = Math.min(player.health + 1, CONFIG.PLAYER_MAX_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();
    generateWalls();
    generateEnemies();
    generateItems();
    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();
        generateWalls();
        generateEnemies();
        generateItems();
        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();
generateWalls();
generateEnemies();
generateItems();
render();