733 lines
26 KiB
JavaScript
733 lines
26 KiB
JavaScript
import { Map } from './Map.js';
|
|
import { UI } from './UI.js';
|
|
import { Player } from './Entity.js';
|
|
|
|
import { Monster, Shopkeeper } 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;
|
|
|
|
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING'
|
|
this.currentShopkeeper = null;
|
|
this.shopSelection = 0;
|
|
this.targetingSpell = null;
|
|
this.projectiles = [];
|
|
|
|
// Bind input
|
|
window.addEventListener('keydown', (e) => this.handleInput(e));
|
|
}
|
|
|
|
start() {
|
|
console.log("Game Started");
|
|
this.generateLevel('down');
|
|
this.ui.log("Welcome to the Town of Oakhaven!");
|
|
}
|
|
|
|
generateLevel(direction = 'down') {
|
|
this.monsters = [];
|
|
this.items = [];
|
|
|
|
if (this.depth === 0) {
|
|
this.map.generateTown();
|
|
// Spawn player in center (default) or at stairs if coming up
|
|
if (direction === 'up') {
|
|
// Coming up from dungeon, spawn at dungeon entrance
|
|
this.player.x = 43;
|
|
this.player.y = 8;
|
|
} else {
|
|
this.player.x = Math.floor(this.map.width / 2);
|
|
this.player.y = Math.floor(this.map.height / 2);
|
|
}
|
|
} else {
|
|
this.map.generate();
|
|
// Spawn Player
|
|
if (this.map.rooms.length > 0) {
|
|
if (direction === 'down') {
|
|
// Start at first room (Stairs Up location)
|
|
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);
|
|
} else {
|
|
// Start at last room (Stairs Down location)
|
|
const endRoom = this.map.rooms[this.map.rooms.length - 1];
|
|
this.player.x = Math.floor(endRoom.x + endRoom.w / 2);
|
|
this.player.y = Math.floor(endRoom.y + endRoom.h / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.spawnMonsters();
|
|
this.spawnItems();
|
|
this.spawnStairs();
|
|
|
|
this.render();
|
|
this.ui.updateStats(this.player, this.depth);
|
|
this.ui.updateInventory(this.player);
|
|
this.ui.updateSpells(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('down');
|
|
}
|
|
|
|
prevLevel() {
|
|
if (this.depth === 0) {
|
|
this.ui.log("You are already in town.");
|
|
return;
|
|
}
|
|
|
|
this.depth--;
|
|
if (this.depth === 0) {
|
|
this.ui.log("You return to the Town of Oakhaven.");
|
|
} else {
|
|
this.ui.log(`You ascend to level ${this.depth}...`);
|
|
}
|
|
this.generateLevel('up');
|
|
}
|
|
|
|
spawnStairs() {
|
|
if (this.depth === 0) {
|
|
// Town: Stairs Down in the Dungeon Entrance building
|
|
const sx = 43; // Center X
|
|
const sy = 8; // Center Y
|
|
this.map.tiles[sy][sx] = '>';
|
|
return;
|
|
}
|
|
|
|
// Dungeon Levels
|
|
if (this.map.rooms.length > 0) {
|
|
// Stairs Up (<) in the first room (where you start when going down)
|
|
const startRoom = this.map.rooms[0];
|
|
const ux = Math.floor(startRoom.x + startRoom.w / 2);
|
|
const uy = Math.floor(startRoom.y + startRoom.h / 2);
|
|
// Ensure we don't overwrite player if they are standing there (visual only, logic handles it)
|
|
this.map.tiles[uy][ux] = '<';
|
|
|
|
// Stairs Down (>) in the last room (unless max depth)
|
|
if (this.depth < this.maxDepth) {
|
|
const endRoom = this.map.rooms[this.map.rooms.length - 1];
|
|
const dx = Math.floor(Math.random() * endRoom.w) + endRoom.x;
|
|
const dy = Math.floor(Math.random() * endRoom.h) + endRoom.y;
|
|
this.map.tiles[dy][dx] = '>';
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Spawn Shopkeepers
|
|
// Weapon Shop (East)
|
|
const weaponShopInventory = [
|
|
{ item: new Item("Dagger", "weapon", 1, { damage: 4 }), price: 50 },
|
|
{ item: new Item("Short Sword", "weapon", 2, { damage: 6 }), price: 100 },
|
|
{ item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 },
|
|
{ item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 }
|
|
];
|
|
const weaponSmith = new Shopkeeper(41, 18, "Smith", "Weapon Shop", weaponShopInventory);
|
|
this.monsters.push(weaponSmith);
|
|
|
|
// General Store (West)
|
|
const generalStoreInventory = [
|
|
{ item: new Item("Potion", "potion", 1, { heal: 5 }), price: 20 }
|
|
];
|
|
const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory);
|
|
this.monsters.push(merchant);
|
|
|
|
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;
|
|
|
|
if (this.gameState === 'SHOP') {
|
|
this.handleShopInput(e);
|
|
return;
|
|
}
|
|
|
|
if (this.gameState === 'TARGETING') {
|
|
this.handleTargetingInput(e);
|
|
return;
|
|
}
|
|
|
|
let dx = 0;
|
|
let dy = 0;
|
|
let handled = false;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowUp': dy = -1; handled = true; break;
|
|
case 'ArrowDown': dy = 1; handled = true; break;
|
|
case 'ArrowLeft': dx = -1; handled = true; break;
|
|
case 'ArrowRight': dx = 1; handled = true; break;
|
|
case 'NumPad8': dy = -1; handled = true; break;
|
|
case 'NumPad2': dy = 1; handled = true; break;
|
|
case 'NumPad4': dx = -1; handled = true; break;
|
|
case 'NumPad6': dx = 1; handled = true; break;
|
|
case 'NumPad7': dx = -1; dy = -1; handled = true; break;
|
|
case 'NumPad9': dx = 1; dy = -1; handled = true; break;
|
|
case 'NumPad1': dx = -1; dy = 1; handled = true; break;
|
|
case 'NumPad3': dx = 1; dy = 1; handled = true; break;
|
|
case '.': case 'NumPad5': handled = true; break; // Wait
|
|
case 'Enter':
|
|
case '>':
|
|
if (this.map.tiles[this.player.y][this.player.x] === '>') {
|
|
this.nextLevel();
|
|
} else if (this.map.tiles[this.player.y][this.player.x] === '<') {
|
|
this.prevLevel();
|
|
} else {
|
|
this.ui.log("There are no stairs here.");
|
|
}
|
|
handled = true;
|
|
break;
|
|
case '<':
|
|
case ',':
|
|
if (this.map.tiles[this.player.y][this.player.x] === '<') {
|
|
this.prevLevel();
|
|
} else {
|
|
this.ui.log("There are no stairs up here.");
|
|
}
|
|
handled = true;
|
|
break;
|
|
case 'g': this.pickupItem(); handled = true; break;
|
|
case '1': this.startTargeting('Magic Arrow'); handled = true; break;
|
|
case '2': this.startTargeting('Fireball'); handled = true; break;
|
|
}
|
|
|
|
if (handled) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
if (dx !== 0 || dy !== 0 || e.key === '.' || e.key === 'NumPad5') {
|
|
this.movePlayer(dx, dy);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
handleShopInput(e) {
|
|
e.preventDefault();
|
|
const inventory = this.currentShopkeeper.inventory;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowUp':
|
|
case 'NumPad8':
|
|
this.shopSelection--;
|
|
if (this.shopSelection < 0) this.shopSelection = inventory.length - 1;
|
|
break;
|
|
case 'ArrowDown':
|
|
case 'NumPad2':
|
|
this.shopSelection++;
|
|
if (this.shopSelection >= inventory.length) this.shopSelection = 0;
|
|
break;
|
|
case 'Enter':
|
|
case ' ':
|
|
this.buyItem(inventory[this.shopSelection]);
|
|
break;
|
|
case 'Escape':
|
|
this.closeShop();
|
|
break;
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
openShop(shopkeeper) {
|
|
this.gameState = 'SHOP';
|
|
this.currentShopkeeper = shopkeeper;
|
|
this.shopSelection = 0;
|
|
this.ui.log(`You talk to ${shopkeeper.name}.`);
|
|
this.render();
|
|
}
|
|
|
|
closeShop() {
|
|
this.gameState = 'PLAY';
|
|
this.currentShopkeeper = null;
|
|
this.render();
|
|
}
|
|
|
|
buyItem(entry) {
|
|
if (this.player.gold >= entry.price) {
|
|
this.player.gold -= entry.price;
|
|
// Clone item to avoid reference issues if buying multiple
|
|
const newItem = new Item(entry.item.name, entry.item.type, entry.item.level, entry.item.stats);
|
|
// Copy visual props
|
|
newItem.symbol = entry.item.symbol;
|
|
newItem.color = entry.item.color;
|
|
|
|
this.player.inventory.push(newItem);
|
|
this.ui.log(`You bought ${entry.item.name} for ${entry.price} gold.`);
|
|
this.ui.updateInventory(this.player);
|
|
this.ui.updateStats(this.player, this.depth);
|
|
} else {
|
|
this.ui.log("You don't have enough gold!");
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (targetMonster instanceof Shopkeeper) {
|
|
this.openShop(targetMonster);
|
|
} else {
|
|
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 {
|
|
console.log(`Blocked at ${newX},${newY}. Tile: '${this.map.tiles[newY][newX]}'`);
|
|
if (dx !== 0 || dy !== 0) this.ui.log("Blocked!");
|
|
}
|
|
|
|
// Temple Healing (Town only)
|
|
if (this.depth === 0 && this.player.x === 25 && this.player.y === 12) {
|
|
if (this.player.hp < this.player.maxHp) {
|
|
this.player.hp = this.player.maxHp;
|
|
this.ui.log("You enter the temple and feel refreshed. HP fully restored!");
|
|
this.ui.updateStats(this.player, this.depth);
|
|
} else {
|
|
this.ui.log("You enter the temple. It is peaceful here.");
|
|
}
|
|
}
|
|
|
|
// Monsters turn
|
|
this.updateMonsters();
|
|
}
|
|
|
|
handleTargetingInput(e) {
|
|
e.preventDefault();
|
|
let dx = 0;
|
|
let dy = 0;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowUp': case 'NumPad8': dy = -1; break;
|
|
case 'ArrowDown': case 'NumPad2': dy = 1; break;
|
|
case 'ArrowLeft': case 'NumPad4': dx = -1; break;
|
|
case 'ArrowRight': 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 'Escape':
|
|
this.gameState = 'PLAY';
|
|
this.ui.log("Canceled.");
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
if (dx !== 0 || dy !== 0) {
|
|
this.castSpell(this.targetingSpell, dx, dy);
|
|
this.gameState = 'PLAY';
|
|
this.targetingSpell = null;
|
|
}
|
|
}
|
|
|
|
startTargeting(spellName) {
|
|
this.targetingSpell = spellName;
|
|
this.gameState = 'TARGETING';
|
|
this.ui.log(`Select direction for ${spellName}...`);
|
|
}
|
|
|
|
castSpell(spellName, dx, dy) {
|
|
let manaCost = 0;
|
|
let damage = 0;
|
|
let symbol = '*';
|
|
let color = '#ffffff';
|
|
|
|
if (spellName === 'Magic Arrow') {
|
|
manaCost = 2;
|
|
damage = Math.floor(this.player.stats.int / 2) + Math.floor(Math.random() * 4) + 1;
|
|
symbol = '*';
|
|
color = '#aaaaff';
|
|
} else if (spellName === 'Fireball') {
|
|
manaCost = 5;
|
|
damage = this.player.stats.int + Math.floor(Math.random() * 6) + 1;
|
|
symbol = 'o';
|
|
color = '#ff4400';
|
|
}
|
|
|
|
if (this.player.mana < manaCost) {
|
|
this.ui.log("Not enough mana!");
|
|
return;
|
|
}
|
|
|
|
this.player.mana -= manaCost;
|
|
this.ui.updateStats(this.player, this.depth);
|
|
|
|
// Create Projectile
|
|
const projectile = {
|
|
x: this.player.x,
|
|
y: this.player.y,
|
|
dx: dx,
|
|
dy: dy,
|
|
symbol: symbol,
|
|
color: color,
|
|
damage: damage,
|
|
name: spellName
|
|
};
|
|
|
|
this.projectiles.push(projectile);
|
|
this.animateProjectile(projectile);
|
|
}
|
|
|
|
animateProjectile(projectile) {
|
|
const interval = setInterval(() => {
|
|
const newX = projectile.x + projectile.dx;
|
|
const newY = projectile.y + projectile.dy;
|
|
|
|
// Check collision
|
|
if (!this.map.isWalkable(newX, newY)) {
|
|
clearInterval(interval);
|
|
this.removeProjectile(projectile);
|
|
this.ui.log(`${projectile.name} hits the wall.`);
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
const monster = this.monsters.find(m => m.x === newX && m.y === newY);
|
|
if (monster) {
|
|
clearInterval(interval);
|
|
this.removeProjectile(projectile);
|
|
|
|
// Shopkeepers are immune
|
|
if (monster instanceof Shopkeeper) {
|
|
this.ui.log(`${projectile.name} hits ${monster.name} but does nothing.`);
|
|
} else {
|
|
// Deal damage
|
|
monster.hp -= projectile.damage;
|
|
this.ui.log(`${projectile.name} hits ${monster.name} for ${projectile.damage} damage.`);
|
|
if (monster.hp <= 0) {
|
|
this.killMonster(monster);
|
|
}
|
|
}
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
projectile.x = newX;
|
|
projectile.y = newY;
|
|
this.render();
|
|
|
|
// Range limit (optional, but good for safety)
|
|
if (Math.abs(projectile.x - this.player.x) > 20 || Math.abs(projectile.y - this.player.y) > 20) {
|
|
clearInterval(interval);
|
|
this.removeProjectile(projectile);
|
|
this.render();
|
|
}
|
|
|
|
}, 50); // Speed of projectile
|
|
}
|
|
|
|
removeProjectile(projectile) {
|
|
const index = this.projectiles.indexOf(projectile);
|
|
if (index > -1) {
|
|
this.projectiles.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
killMonster(monster) {
|
|
this.ui.log(`${monster.name} dies!`);
|
|
this.monsters = this.monsters.filter(m => m !== monster);
|
|
const gold = monster.level * (Math.floor(Math.random() * 5) + 1);
|
|
this.player.gold += gold;
|
|
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
|
|
if (monster.name === "The Dungeon Lord") {
|
|
const map = new Item("Ancient Map", "map", 1, {});
|
|
map.x = monster.x;
|
|
map.y = monster.y;
|
|
this.items.push(map);
|
|
this.ui.log("The Dungeon Lord drops an Ancient Map!");
|
|
}
|
|
if (this.player.gainXp(monster.level * 5)) {
|
|
this.ui.log(`You reached level ${this.player.level}!`);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Shopkeepers are invincible/pacifist
|
|
if (defender instanceof Shopkeeper) return;
|
|
|
|
// 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 instanceof Shopkeeper) continue; // Shopkeepers don't move
|
|
|
|
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.projectiles, this.tileSize);
|
|
|
|
if (this.gameState === 'SHOP') {
|
|
this.ui.drawShop(this.currentShopkeeper, this.player, this.shopSelection);
|
|
}
|
|
|
|
this.ui.updateStats(this.player, this.depth);
|
|
}
|
|
}
|