diff options
author | elioat <elioat@tilde.institute> | 2024-10-30 19:39:34 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-10-30 19:39:34 -0400 |
commit | c8a8b22d8749ccafd89f058a9c675bef1b6e54bd (patch) | |
tree | 45072eadf64f65f5311fdb112133a6432514821f /html/broughlike/broughlike.js | |
parent | bc572f84356a417f4714d723eeb1a498aa1d340f (diff) | |
download | tour-c8a8b22d8749ccafd89f058a9c675bef1b6e54bd.tar.gz |
*
Diffstat (limited to 'html/broughlike/broughlike.js')
-rw-r--r-- | html/broughlike/broughlike.js | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/html/broughlike/broughlike.js b/html/broughlike/broughlike.js new file mode 100644 index 0000000..e492a0c --- /dev/null +++ b/html/broughlike/broughlike.js @@ -0,0 +1,809 @@ +const COLORS = { + grid: '#2d2d2d', + walls: '#2d2d2d', + exit: 'teal', + diamond: 'gold', + pentagon: 'blueviolet', + player: 'rgba(0, 237, 209, ', + enemy: 'rgba(255, 155, 28, ', + boss: 'rgba(255, 14, 0, ', + combatDotPlayer: '#00edd1', + combatDotEnemy: '#ff731c', + combatDotBoss: '#b70030' +}; + +const GRID_SIZE = 6; +const PLAYER_HEALTH = 12; +const PLAYER_MAX_HEALTH = 16; +const PLAYER_BASE_DAMAGE = 1; +const ENEMY_CHASE_DISTANCE = 4; +const ENEMY_BOSS_OCCURRENCE = 10; +const MIN_ENEMIES_ON_LEVEL = 1; +const MAX_ENEMIES_ON_LEVEL = 4; +const MAX_ENEMY_HEALTH = 7; +const MIN_ENEMY_HEALTH = 2; +const WALLS_MIN = 7; +const WALLS_MAX = 20; +const ITEMS_MIN = 1; +const ITEMS_MAX = 3; +const DOTS_PER_HIT = 7; + +let highScore = localStorage.getItem('highScore') || 0; + +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +let tileSize = canvas.width / GRID_SIZE; + +const player = { + x: 0, + y: 0, + health: PLAYER_HEALTH, + score: 0, + damage: PLAYER_BASE_DAMAGE, + totalDamageDone: 0, + totalDamageTaken: 0, + cellsTraveled: 0, + killCount: 0, + itemsCollected: 0, +}; + +const exit = { x: Math.floor(Math.random() * GRID_SIZE), y: Math.floor(Math.random() * GRID_SIZE) }; +let walls = []; +let enemies = []; +let items = []; +let combatDots = {}; + +function isValidMove(newX, newY) { + return ( + newX >= 0 && newX < GRID_SIZE && + newY >= 0 && newY < GRID_SIZE && + !walls.some(wall => wall.x === newX && wall.y === newY) + ); +} + +function generateExit() { + let distance = 0; + do { + exit.x = Math.floor(Math.random() * GRID_SIZE); + exit.y = Math.floor(Math.random() * 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() * (MAX_ENEMIES_ON_LEVEL - MIN_ENEMIES_ON_LEVEL + 1)) + MIN_ENEMIES_ON_LEVEL + : Math.floor(Math.random() * (MAX_ENEMIES_ON_LEVEL + 1)); + for (let i = 0; i < numEnemies; i++) { + let enemyX, enemyY; + do { + enemyX = Math.floor(Math.random() * GRID_SIZE); + enemyY = Math.floor(Math.random() * 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() * (MAX_ENEMY_HEALTH - MIN_ENEMY_HEALTH + 1)) + MIN_ENEMY_HEALTH + }); + } + + // Generate a boss enemy every ENEMY_BOSS_OCCURRENCE levels + if (player.score % 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: MAX_ENEMY_HEALTH + 2 + }); + } +} + +function generateWallsNaive() { + walls = []; + let numWalls = Math.floor(Math.random() * (WALLS_MAX - WALLS_MIN + 1)) + WALLS_MIN; + while (walls.length < numWalls) { + const wallX = Math.floor(Math.random() * GRID_SIZE); + const wallY = Math.floor(Math.random() * 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() * (WALLS_MAX - WALLS_MIN + 1)) + WALLS_MIN; + let wallX = Math.floor(Math.random() * GRID_SIZE); + let wallY = Math.floor(Math.random() * 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(GRID_SIZE - 1, wallX + 1); break; // Move right + case 2: wallY = Math.max(0, wallY - 1); break; // Move up + case 3: wallY = Math.min(GRID_SIZE - 1, wallY + 1); break; // Move down + } + } + + if (!isPassable()) { + generateWallsDrunkardsWalk(); + } +} + +function generateWallsCellularAutomata() { + walls = []; + const map = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0)); + + // Initialize a map with random walls + for (let x = 0; x < GRID_SIZE; x++) { + for (let y = 0; y < 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(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0)); + + // Iterate over each cell in the grid + for (let x = 0; x < GRID_SIZE; x++) { + for (let y = 0; y < 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 < GRID_SIZE; x++) { + for (let y = 0; y < 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 < GRID_SIZE && ny >= 0 && ny < GRID_SIZE) { + count += map[nx][ny]; + } else { + count++; // Consider out-of-bounds bits as walls + } + } + } + return count; +} + +function generateWallsRSP() { + walls = []; + const MIN_ROOM_SIZE = 2; + + function splitNode(x, y, width, height) { + const splitHorizontally = Math.random() > 0.5; + const max = (splitHorizontally ? height : width) - MIN_ROOM_SIZE; + + if (max <= MIN_ROOM_SIZE) return [{ x, y, width, height }]; + + const split = Math.floor(Math.random() * (max - MIN_ROOM_SIZE)) + 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, GRID_SIZE, 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() * (ITEMS_MAX - ITEMS_MIN + 1)) + ITEMS_MIN; + for (let i = 0; i < numItems; i++) { + let itemX, itemY; + do { + itemX = Math.floor(Math.random() * GRID_SIZE); + itemY = Math.floor(Math.random() * 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(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(false)); + + function dfs(x, y) { + if (x < 0 || x >= GRID_SIZE || y < 0 || y >= 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 < GRID_SIZE; row++) { + for (let col = 0; col < 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 / MAX_ENEMY_HEALTH; // Opacity based on health + const radius = tileSize / 3; + const damageTaken = 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 / 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, 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 < GRID_SIZE && cellY >= 0 && cellY < GRID_SIZE) { + const key = `${cellX},${cellY}`; + if (!combatDots[key]) { + combatDots[key] = []; + } + for (let i = 0; i < 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); + + // 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 ? ENEMY_CHASE_DISTANCE + 2 : 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 < GRID_SIZE && + newY >= 0 && newY < 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, 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 = PLAYER_HEALTH; + player.damage = 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 = 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 / GRID_SIZE; // Update tile size based on canvas dimensions + render(); +}; + +window.addEventListener('resize', resizeCanvas); +resizeCanvas(); + +// Initial level setup +generateExit(); +generateEnemies(); +generateItems(); +generateWalls(); +render(); \ No newline at end of file |