import { CONFIG, COLORS, CANVAS } from './config.js';
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;) {
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 });
i++; // Only increment i if the item is reachable and actually placed on the board, this avoids levels with fewer items than ITEMS_MIN
}
}
// 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() {
CANVAS.ctx.clearRect(0, 0, CANVAS.canvas.width, CANVAS.canvas.height);
CANVAS.ctx.lineWidth = 2;
CANVAS.ctx.strokeStyle = COLORS.grid;
for (let row = 0; row < CONFIG.GRID_SIZE; row++) {
for (let col = 0; col < CONFIG.GRID_SIZE; col++) {
CANVAS.ctx.strokeRect(col * CANVAS.tileSize, row * CANVAS.tileSize, CANVAS.tileSize, CANVAS.tileSize);
}
}
}
function drawExit() {
const x = exit.x * CANVAS.tileSize + CANVAS.tileSize / 2;
const y = exit.y * CANVAS.tileSize + CANVAS.tileSize / 2;
CANVAS.ctx.beginPath();
CANVAS.ctx.moveTo(x, y - CANVAS.tileSize / 3);
CANVAS.ctx.lineTo(x + CANVAS.tileSize / 3, y + CANVAS.tileSize / 3);
CANVAS.ctx.lineTo(x - CANVAS.tileSize / 3, y + CANVAS.tileSize / 3);
CANVAS.ctx.closePath();
CANVAS.ctx.fillStyle = COLORS.exit;
CANVAS.ctx.fill();
}
function drawWalls() {
CANVAS.ctx.fillStyle = COLORS.walls;
walls.forEach(wall => {
CANVAS.ctx.fillRect(wall.x * CANVAS.tileSize, wall.y * CANVAS.tileSize, CANVAS.tileSize, CANVAS.tileSize);
});
}
function drawItems() {
items.forEach(item => {
const x = item.x * CANVAS.tileSize + CANVAS.tileSize / 2;
const y = item.y * CANVAS.tileSize + CANVAS.tileSize / 2;
CANVAS.ctx.fillStyle = item.type === 'diamond' ? COLORS.diamond : COLORS.pentagon;
CANVAS.ctx.beginPath();
if (item.type === 'diamond') {
CANVAS.ctx.moveTo(x, y - CANVAS.tileSize / 4);
CANVAS.ctx.lineTo(x + CANVAS.tileSize / 4, y);
CANVAS.ctx.lineTo(x, y + CANVAS.tileSize / 4);
CANVAS.ctx.lineTo(x - CANVAS.tileSize / 4, y);
} else {
const sides = 5;
const radius = CANVAS.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) CANVAS.ctx.moveTo(pointX, pointY);
else CANVAS.ctx.lineTo(pointX, pointY);
}
}
CANVAS.ctx.closePath();
CANVAS.ctx.fill();
});
}
function drawCharacterBorder(x, y, radius, damageTaken) {
const dashLength = 5;
const gapLength = Math.max(1, damageTaken * 2); // More damage, larger gaps
CANVAS.ctx.lineWidth = 2;
CANVAS.ctx.strokeStyle = '#2d2d2d';
CANVAS.ctx.setLineDash([dashLength, gapLength]);
CANVAS.ctx.beginPath();
CANVAS.ctx.arc(x, y, radius, 0, 2 * Math.PI);
CANVAS.ctx.stroke();
CANVAS.ctx.setLineDash([]); // Reset to a solid line
}
function drawEnemies() {
enemies.forEach(enemy => {
const x = enemy.x * CANVAS.tileSize + CANVAS.tileSize / 2;
const y = enemy.y * CANVAS.tileSize + CANVAS.tileSize / 2;
const opacity = enemy.health / CONFIG.MAX_ENEMY_HEALTH; // Opacity based on health
const radius = CANVAS.tileSize / 3;
const damageTaken = CONFIG.MAX_ENEMY_HEALTH - enemy.health;
CANVAS.ctx.beginPath();
CANVAS.ctx.arc(x, y, radius, 0, 2 * Math.PI);
CANVAS.ctx.fillStyle = `${enemy.color}${opacity})`;
CANVAS.ctx.fill();
drawCharacterBorder(x, y, radius, damageTaken);
});
}
function drawPlayer() {
const x = player.x * CANVAS.tileSize + CANVAS.tileSize / 2;
const y = player.y * CANVAS.tileSize + CANVAS.tileSize / 2;
const radius = CANVAS.tileSize / 3;
const playerOpacity = player.health / CONFIG.PLAYER_HEALTH; // Opacity based on health
CANVAS.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) {
CANVAS.ctx.moveTo(hexX, hexY);
} else {
CANVAS.ctx.lineTo(hexX, hexY);
}
}
CANVAS.ctx.closePath();
CANVAS.ctx.fillStyle = `${COLORS.player}${playerOpacity})`;
CANVAS.ctx.fill();
CANVAS.ctx.lineWidth = 2;
CANVAS.ctx.strokeStyle = '#2d2d2d';
CANVAS.ctx.stroke();
}
function drawCombatDots() {
for (const key in combatDots) {
const [cellX, cellY] = key.split(',').map(Number);
const dots = combatDots[key];
dots.forEach(dot => {
CANVAS.ctx.beginPath();
CANVAS.ctx.arc(cellX * CANVAS.tileSize + dot.x, cellY * CANVAS.tileSize + dot.y, 2, 0, Math.PI * 2);
CANVAS.ctx.fillStyle = dot.color;
CANVAS.ctx.fill();
CANVAS.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() * CANVAS.tileSize,
y: Math.random() * CANVAS.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.canvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevent scrolling on touchstart
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
});
CANVAS.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.canvas.getBoundingClientRect();
CANVAS.canvas.width = rect.width;
CANVAS.canvas.height = rect.height;
CANVAS.tileSize = CANVAS.updateTileSize(); // Update tile size based on canvas dimensions
render();
};
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Initial level setup
generateExit();
generateWalls();
generateEnemies();
generateItems();
render();
let AUTO_PLAY = false;
let autoPlayInterval = null;
function autoPlay() {
let lastPosition = { x: player.x, y: player.y };
let stuckCounter = 0;
const playerAtExit = () => player.x === exit.x && player.y === exit.y;
const checkIfStuck = () => {
if (lastPosition.x === player.x && lastPosition.y === player.y) {
stuckCounter++;
} else {
stuckCounter = 0;
lastPosition = { x: player.x, y: player.y };
}
return stuckCounter > 3;
};
const findSafestPath = (target) => {
const path = findPath(player, target);
if (path.length <= 1) {
// If you can't find a path, find the nearest enemy
const nearestEnemy = enemies.reduce((closest, enemy) => {
const distToCurrent = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y);
const distToClosest = closest ? Math.abs(closest.x - player.x) + Math.abs(closest.y - player.y) : Infinity;
return distToCurrent < distToClosest ? enemy : closest;
}, null);
if (nearestEnemy) {
return [{x: player.x, y: player.y}, {x: nearestEnemy.x, y: nearestEnemy.y}];
}
return null;
}
const nextStep = path[1];
const adjacentEnemies = enemies.filter(enemy =>
Math.abs(enemy.x - nextStep.x) + Math.abs(enemy.y - nextStep.y) <= 1
);
if (adjacentEnemies.length > 0 && player.health < 3) {
const alternativePaths = [
{dx: 1, dy: 0}, {dx: -1, dy: 0},
{dx: 0, dy: 1}, {dx: 0, dy: -1}
].filter(({dx, dy}) => {
const newX = player.x + dx;
const newY = player.y + dy;
return isValidMove(newX, newY) &&
!enemies.some(e => Math.abs(e.x - newX) + Math.abs(e.y - newY) <= 1);
});
if (alternativePaths.length > 0) {
const randomPath = alternativePaths[Math.floor(Math.random() * alternativePaths.length)];
return [{x: player.x, y: player.y}, {x: player.x + randomPath.dx, y: player.y + randomPath.dy}];
}
}
return path;
};
const moveTowardsTarget = (target) => {
const path = findSafestPath(target);
if (path && path.length > 1) {
const nextStep = path[1];
const dx = nextStep.x - player.x;
const dy = nextStep.y - player.y;
movePlayer(dx, dy);
return true;
}
return false;
};
const findBestTarget = () => {
// If health is low, prioritize healing items
if (player.health < 3) {
const healingItem = items.find(item => item.type === 'pentagon');
if (healingItem && findPath(player, healingItem).length > 0) {
return healingItem;
}
}
// If there's a nearby damage boost and we're healthy, grab it
const damageItem = items.find(item =>
item.type === 'diamond' &&
Math.abs(item.x - player.x) + Math.abs(item.y - player.y) < 5
);
if (damageItem && player.health > 2) {
return damageItem;
}
// Default to exit
return exit;
};
const play = () => {
if (!AUTO_PLAY) {
clearTimeout(autoPlayInterval);
autoPlayInterval = null;
return;
}
if (playerAtExit()) return;
if (checkIfStuck()) {
const directions = [{dx: 1, dy: 0}, {dx: -1, dy: 0}, {dx: 0, dy: 1}, {dx: 0, dy: -1}];
const validDirections = directions.filter(({dx, dy}) => isValidMove(player.x + dx, player.y + dy));
if (validDirections.length > 0) {
const {dx, dy} = validDirections[Math.floor(Math.random() * validDirections.length)];
movePlayer(dx, dy);
}
} else {
const target = findBestTarget();
moveTowardsTarget(target);
}
autoPlayInterval = setTimeout(play, 400);
};
play();
}
document.addEventListener('keydown', (e) => {
if (e.key === 'v') {
AUTO_PLAY = !AUTO_PLAY;
if (AUTO_PLAY) {
console.log("Auto-play on");
autoPlay();
} else {
console.log("Auto-play off");
}
}
});