import { Map } from './Map.js'; import { UI } from './UI.js'; import { Player } from './Entity.js'; import { Monster } from './Entity.js'; import { Item } from './Item.js'; export class Game { constructor() { this.canvas = document.getElementById('game-canvas'); this.ctx = this.canvas.getContext('2d'); this.ui = new UI(this.ctx, this); // Pass game instance to UI for callbacks this.map = new Map(50, 50); this.player = new Player(10, 10); this.monsters = []; this.items = []; this.gameOver = false; this.monsters = []; this.items = []; this.gameOver = false; this.depth = 0; // Start in Town this.maxDepth = 3; this.tileSize = 32; // Bind input window.addEventListener('keydown', (e) => this.handleInput(e)); } start() { console.log("Game Started"); this.generateLevel(); this.ui.log("Welcome to the Town of Oakhaven!"); } generateLevel() { this.monsters = []; this.items = []; if (this.depth === 0) { this.map.generateTown(); // Spawn player in center this.player.x = Math.floor(this.map.width / 2); this.player.y = Math.floor(this.map.height / 2); } else { this.map.generate(); // Spawn Player in first room if (this.map.rooms.length > 0) { const startRoom = this.map.rooms[0]; this.player.x = Math.floor(startRoom.x + startRoom.w / 2); this.player.y = Math.floor(startRoom.y + startRoom.h / 2); } } this.spawnMonsters(); this.spawnItems(); this.spawnStairs(); this.render(); this.ui.updateStats(this.player, this.depth); this.ui.updateInventory(this.player); } nextLevel() { if (this.depth >= this.maxDepth) { this.ui.log("You have reached the bottom! You Win!"); setTimeout(() => alert("You Win!"), 100); return; } this.depth++; this.ui.log(`You descend to level ${this.depth}...`); this.generateLevel(); } spawnStairs() { if (this.depth === 0) { // Stairs in a random building or just random spot if (this.map.rooms.length > 0) { const room = this.map.rooms[0]; const sx = Math.floor(Math.random() * room.w) + room.x; const sy = Math.floor(Math.random() * room.h) + room.y; this.map.tiles[sy][sx] = '>'; } return; } // Spawn stairs in the last room if (this.map.rooms.length > 0) { const room = this.map.rooms[this.map.rooms.length - 1]; const sx = Math.floor(Math.random() * room.w) + room.x; const sy = Math.floor(Math.random() * room.h) + room.y; this.map.tiles[sy][sx] = '>'; } } spawnMonsters() { if (this.depth === 0) { // Spawn Peasants in Town for (let i = 0; i < 10; i++) { const mx = Math.floor(Math.random() * this.map.width); const my = Math.floor(Math.random() * this.map.height); if (this.map.isWalkable(mx, my)) { const peasant = new Monster(mx, my, "Peasant", "p", "#00ffff", 1); this.monsters.push(peasant); } } return; } // Skip first room for (let i = 1; i < this.map.rooms.length; i++) { const room = this.map.rooms[i]; // Spawn 1-3 monsters per room const count = Math.floor(Math.random() * 3) + 1; for (let j = 0; j < count; j++) { const mx = Math.floor(Math.random() * room.w) + room.x; const my = Math.floor(Math.random() * room.h) + room.y; const type = Math.random(); let monster; // Difficulty scaling if (this.depth === 1) { if (type < 0.7) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1); else monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); } else if (this.depth === 2) { if (type < 0.4) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1); else if (type < 0.8) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); else monster = new Monster(mx, my, "Orc", "o", "#008000", 3); } else { if (type < 0.3) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); else if (type < 0.7) monster = new Monster(mx, my, "Orc", "o", "#008000", 3); else monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 4); } this.monsters.push(monster); } } // Spawn Boss on final level if (this.depth === this.maxDepth) { const room = this.map.rooms[this.map.rooms.length - 1]; // Last room const mx = Math.floor(Math.random() * room.w) + room.x; const my = Math.floor(Math.random() * room.h) + room.y; // Boss: Level 10, High HP const boss = new Monster(mx, my, "The Dungeon Lord", "D", "#ff00ff", 10); boss.hp = 100; boss.maxHp = 100; this.monsters.push(boss); } } spawnItems() { if (this.depth === 0) return; // No items in town // Spawn items in random rooms for (let i = 0; i < this.map.rooms.length; i++) { const room = this.map.rooms[i]; if (Math.random() < 0.5) { const ix = Math.floor(Math.random() * room.w) + room.x; const iy = Math.floor(Math.random() * room.h) + room.y; // Random item const type = Math.random(); let item; // Better loot deeper let tier = this.depth; if (Math.random() < 0.2) tier++; // Chance for higher tier if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 }); else if (type < 0.5) { if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 }); else if (tier === 2) item = new Item("Short Sword", "weapon", 2, { damage: 6 }); else item = new Item("Long Sword", "weapon", 3, { damage: 8 }); } else if (type < 0.7) { if (tier <= 1) item = new Item("Buckler", "shield", 1, { defense: 1 }); else if (tier === 2) item = new Item("Wooden Shield", "shield", 2, { defense: 2 }); else item = new Item("Kite Shield", "shield", 3, { defense: 3 }); } else { if (tier >= 3) item = new Item("Great Axe", "weapon", 4, { damage: 12 }); else item = new Item("Short Sword", "weapon", 2, { damage: 6 }); } item.x = ix; item.y = iy; this.items.push(item); } } } handleInput(e) { if (this.gameOver) return; let dx = 0; let dy = 0; switch (e.key) { case 'ArrowUp': dy = -1; break; case 'ArrowDown': dy = 1; break; case 'ArrowLeft': dx = -1; break; case 'ArrowRight': dx = 1; break; case 'NumPad8': dy = -1; break; case 'NumPad2': dy = 1; break; case 'NumPad4': dx = -1; break; case 'NumPad6': dx = 1; break; case 'NumPad7': dx = -1; dy = -1; break; case 'NumPad9': dx = 1; dy = -1; break; case 'NumPad1': dx = -1; dy = 1; break; case 'NumPad3': dx = 1; dy = 1; break; case '.': case 'NumPad5': break; // Wait case 'Enter': case '>': console.log(`Trying to descend. Player at ${this.player.x},${this.player.y}. Tile: '${this.map.tiles[this.player.y][this.player.x]}'`); if (this.map.tiles[this.player.y][this.player.x] === '>') { this.nextLevel(); } else { this.ui.log("There are no stairs here."); } break; case 'g': this.pickupItem(); break; } if (dx !== 0 || dy !== 0 || e.key === '.' || e.key === 'NumPad5') { this.movePlayer(dx, dy); this.render(); } } movePlayer(dx, dy) { const newX = this.player.x + dx; const newY = this.player.y + dy; // Check for monster const targetMonster = this.monsters.find(m => m.x === newX && m.y === newY); if (targetMonster) { this.attack(this.player, targetMonster); } else if (this.map.isWalkable(newX, newY)) { this.player.x = newX; this.player.y = newY; // Check for item const item = this.items.find(i => i.x === newX && i.y === newY); if (item) { this.ui.log(`You see a ${item.name}. (Press 'g' to get)`); } } else { if (dx !== 0 || dy !== 0) this.ui.log("Blocked!"); } // Monsters turn this.updateMonsters(); } pickupItem() { const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y); if (itemIndex !== -1) { const item = this.items[itemIndex]; this.items.splice(itemIndex, 1); this.player.inventory.push(item); this.ui.log(`You picked up ${item.name}.`); this.ui.updateInventory(this.player); this.render(); } else { this.ui.log("There is nothing here to pick up."); } } useItem(index) { const item = this.player.inventory[index]; if (!item) return; if (item.type === 'weapon') { this.player.equipment.weapon = item; this.ui.log(`You equipped ${item.name}.`); } else if (item.type === 'shield') { this.player.equipment.shield = item; this.ui.log(`You equipped ${item.name}.`); } else if (item.type === 'potion') { if (item.stats.heal) { this.player.hp = Math.min(this.player.hp + item.stats.heal, this.player.maxHp); this.ui.log(`You drank ${item.name} and recovered ${item.stats.heal} HP.`); // Remove potion this.player.inventory.splice(index, 1); } } this.ui.updateInventory(this.player); this.ui.updateStats(this.player, this.depth); this.render(); // Re-render to show equipped status if we add that } attack(attacker, defender) { // Peasants are invincible if (defender.name === "Peasant") { const quotes = [ "This is our town.", "The Dungeon Lord is oppressing us! Please help!", "You can get healing at the temple.", "There are things to buy in the shops.", "It's unpleasant for peasants at present.", "Hello, hero!" ]; const quote = quotes[Math.floor(Math.random() * quotes.length)]; this.ui.showPopup(quote, 1000); return; } let damage = 0; if (attacker === this.player) { damage = this.player.getDamage() + Math.floor(Math.random() * 2); } else { damage = 1 + Math.floor(Math.random() * 2); // Monster damage } // Apply defense const defense = defender.getDefense ? defender.getDefense() : 0; damage = Math.max(1, damage - defense); // Always take at least 1 damage defender.hp -= damage; this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`); if (defender.hp <= 0) { this.ui.log(`${defender.name} dies!`); if (defender === this.player) { this.ui.log("GAME OVER"); this.gameOver = true; setTimeout(() => alert("Game Over! Refresh to restart."), 100); } else { // Remove monster this.monsters = this.monsters.filter(m => m !== defender); // Gold Drop const gold = defender.level * (Math.floor(Math.random() * 5) + 1); this.player.gold += gold; this.ui.log(`You kill the ${defender.name}! It drops ${gold} gold.`); // Boss Drop if (defender.name === "The Dungeon Lord") { const map = new Item("Ancient Map", "map", 1, {}); map.x = defender.x; map.y = defender.y; this.items.push(map); this.ui.log("The Dungeon Lord drops an Ancient Map!"); } // Award XP if (this.player.gainXp(defender.level * 5)) { this.ui.log(`You reached level ${this.player.level}!`); } } } } updateMonsters() { for (const monster of this.monsters) { if (monster.name === "Peasant") { // Random walk (Slow: 10% chance to move) if (Math.random() < 0.1) { const dx = Math.floor(Math.random() * 3) - 1; const dy = Math.floor(Math.random() * 3) - 1; const newX = monster.x + dx; const newY = monster.y + dy; if (this.map.isWalkable(newX, newY) && !this.monsters.some(m => m.x === newX && m.y === newY) && (newX !== this.player.x || newY !== this.player.y)) { monster.x = newX; monster.y = newY; } } continue; } // Simple chase logic const dx = this.player.x - monster.x; const dy = this.player.y - monster.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 8) { // Aggro range let moveX = 0; let moveY = 0; if (Math.abs(dx) > Math.abs(dy)) { moveX = dx > 0 ? 1 : -1; } else { moveY = dy > 0 ? 1 : -1; } const newX = monster.x + moveX; const newY = monster.y + moveY; if (newX === this.player.x && newY === this.player.y) { this.attack(monster, this.player); } else if (this.map.isWalkable(newX, newY)) { // Check if occupied by another monster if (!this.monsters.some(m => m.x === newX && m.y === newY)) { monster.x = newX; monster.y = newY; } } } } } render() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ui.renderMap(this.map, this.player, this.monsters, this.items, this.tileSize); this.ui.updateStats(this.player, this.depth); } }