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 | |
parent | bc572f84356a417f4714d723eeb1a498aa1d340f (diff) | |
download | tour-c8a8b22d8749ccafd89f058a9c675bef1b6e54bd.tar.gz |
*
Diffstat (limited to 'html')
-rw-r--r-- | html/broughlike/broughlike.js | 809 | ||||
-rw-r--r-- | html/broughlike/index.html | 822 |
2 files changed, 810 insertions, 821 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 diff --git a/html/broughlike/index.html b/html/broughlike/index.html index b318def..62a43ac 100644 --- a/html/broughlike/index.html +++ b/html/broughlike/index.html @@ -56,829 +56,9 @@ <p><a href="about.html">About</a></p> <div> <button class="toggleButton" id="toggleShake" onclick="toggleShake()">Turn Shake Off</button> - <!-- <button class="toggleButton" id="toggleHardMode" onclick="toggleHardMode()">Hard Mode On</button> --> </div> </div> <canvas id="gameCanvas"></canvas> - <script> - - 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' - }; - - // let hardMode = localStorage.getItem('hardMode') || false; - // let hardModeModifier = hardMode ? 10 : 0; - // const toggleHardMode = () => { - // hardMode = !hardMode; - // localStorage.setItem('hardMode', hardMode); - // document.getElementById('toggleHardMode').textContent = hardMode ? 'Hard Mode Off' : 'Hard Mode On'; - // hardModeModifier = hardMode ? 10 : 0; - // resetGame(); - // }; - - 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, - lineSegments: 0, - inventory: [] - }; - - 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) - ); - 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 = exit.x; // Boss enemies always appear at the exit - let bossY = exit.y; // This ensures that they're not in little rooms that the player can't reach - 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) { - player.lineSegments += 2; - } else { - player.lineSegments++; - } - } - - 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; - player.lineSegments = 0; - player.inventory = []; - 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(); - </script> + <script src="./broughlike.js"></script> </body> </html> \ No newline at end of file |