diff --git a/index.html b/index.html
index 0bdd0df..c269d8a 100644
--- a/index.html
+++ b/index.html
@@ -48,6 +48,12 @@
+
diff --git a/src/Entity.js b/src/Entity.js
index 53ab1f1..ec59083 100644
--- a/src/Entity.js
+++ b/src/Entity.js
@@ -21,8 +21,12 @@ export class Player extends Entity {
};
this.stats = {
str: 10,
- dex: 10
+ dex: 10,
+ int: 10
};
+ this.mana = 10;
+ this.maxMana = 10;
+ this.spells = ['Magic Arrow', 'Fireball'];
this.level = 1;
this.xp = 0;
this.maxXp = 10;
@@ -37,7 +41,10 @@ export class Player extends Entity {
this.maxXp = Math.floor(this.maxXp * 1.5);
this.maxHp += 5;
this.hp = this.maxHp;
- this.stats.str += 2;
+ this.maxMana += 5;
+ this.mana = this.maxMana;
+ this.stats.str += 1;
+ this.stats.int += 1;
return true; // Leveled up
}
return false;
@@ -74,3 +81,11 @@ export class Monster extends Entity {
return Math.floor(this.level / 2); // Slight natural armor for higher level monsters
}
}
+
+export class Shopkeeper extends Entity {
+ constructor(x, y, name, shopName, inventory) {
+ super(x, y, name, '$', '#ffd700');
+ this.shopName = shopName;
+ this.inventory = inventory; // Array of { item: Item, price: number }
+ }
+}
diff --git a/src/Game.js b/src/Game.js
index 8839641..666b489 100644
--- a/src/Game.js
+++ b/src/Game.js
@@ -2,7 +2,7 @@ import { Map } from './Map.js';
import { UI } from './UI.js';
import { Player } from './Entity.js';
-import { Monster } from './Entity.js';
+import { Monster, Shopkeeper } from './Entity.js';
import { Item } from './Item.js';
export class Game {
@@ -23,32 +23,52 @@ export class Game {
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();
+ this.generateLevel('down');
this.ui.log("Welcome to the Town of Oakhaven!");
}
- generateLevel() {
+ generateLevel(direction = 'down') {
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);
+ // 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 in first room
+ // Spawn Player
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);
+ 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);
+ }
}
}
@@ -59,6 +79,7 @@ export class Game {
this.render();
this.ui.updateStats(this.player, this.depth);
this.ui.updateInventory(this.player);
+ this.ui.updateSpells(this.player);
}
nextLevel() {
@@ -70,27 +91,49 @@ export class Game {
this.depth++;
this.ui.log(`You descend to level ${this.depth}...`);
- this.generateLevel();
+ 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) {
- // 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] = '>';
- }
+ // Town: Stairs Down in the Dungeon Entrance building
+ const sx = 43; // Center X
+ const sy = 8; // Center Y
+ this.map.tiles[sy][sx] = '>';
return;
}
- // Spawn stairs in the last room
+ // Dungeon Levels
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] = '>';
+ // 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] = '>';
+ }
}
}
@@ -105,6 +148,25 @@ export class Game {
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;
}
@@ -193,33 +255,61 @@ export class Game {
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; 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 '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 '>':
- 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 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 'g': this.pickupItem(); 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') {
@@ -228,6 +318,64 @@ export class Game {
}
}
+ 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;
@@ -235,7 +383,11 @@ export class Game {
// Check for monster
const targetMonster = this.monsters.find(m => m.x === newX && m.y === newY);
if (targetMonster) {
- this.attack(this.player, 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;
@@ -246,13 +398,174 @@ export class Game {
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) {
@@ -292,6 +605,9 @@ export class Game {
}
attack(attacker, defender) {
+ // Shopkeepers are invincible/pacifist
+ if (defender instanceof Shopkeeper) return;
+
// Peasants are invincible
if (defender.name === "Peasant") {
const quotes = [
@@ -355,6 +671,8 @@ export class Game {
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) {
@@ -403,7 +721,12 @@ export class Game {
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.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);
}
}
diff --git a/src/Map.js b/src/Map.js
index f0d2bc2..d3c2772 100644
--- a/src/Map.js
+++ b/src/Map.js
@@ -70,18 +70,9 @@ export class Map {
}
}
- // Create buildings
- const numBuildings = 5;
- for (let i = 0; i < numBuildings; i++) {
- const w = Math.floor(Math.random() * 6) + 5;
- const h = Math.floor(Math.random() * 6) + 5;
- const x = Math.floor(Math.random() * (this.width - w - 2)) + 1;
- const y = Math.floor(Math.random() * (this.height - h - 2)) + 1;
-
- // Check overlap (simple check against other buildings)
- // For now, just place them. If they overlap, they merge.
-
- // Draw Walls
+ // Helper to draw a building
+ const drawBuilding = (x, y, w, h, doorSide) => {
+ // Walls
for (let by = y; by < y + h; by++) {
for (let bx = x; bx < x + w; bx++) {
if (by === y || by === y + h - 1 || bx === x || bx === x + w - 1) {
@@ -92,22 +83,37 @@ export class Map {
}
}
- // Add Door (randomly on one side)
- if (Math.random() < 0.5) {
- // Horizontal wall door
- const doorX = Math.floor(Math.random() * (w - 2)) + x + 1;
- const doorY = Math.random() < 0.5 ? y : y + h - 1;
- this.tiles[doorY][doorX] = '.';
- } else {
- // Vertical wall door
- const doorY = Math.floor(Math.random() * (h - 2)) + y + 1;
- const doorX = Math.random() < 0.5 ? x : x + w - 1;
- this.tiles[doorY][doorX] = '.';
+ // Door
+ if (doorSide === 'bottom') {
+ this.tiles[y + h - 1][Math.floor(x + w / 2)] = '.';
+ } else if (doorSide === 'top') {
+ this.tiles[y][Math.floor(x + w / 2)] = '.';
+ } else if (doorSide === 'left') {
+ this.tiles[Math.floor(y + h / 2)][x] = '.';
+ } else if (doorSide === 'right') {
+ this.tiles[Math.floor(y + h / 2)][x + w - 1] = '.';
}
- // Add to rooms list so we can spawn things inside if we want
+ // Add to rooms for reference
this.rooms.push({ x, y, w, h });
- }
+ };
+
+ // 1. Temple (North Center)
+ drawBuilding(20, 5, 10, 8, 'bottom');
+
+ // 2. General Store (West)
+ drawBuilding(5, 15, 8, 6, 'right');
+
+ // 3. Weapon Shop (East)
+ drawBuilding(37, 15, 8, 6, 'left');
+
+ // 4. Dungeon Entrance (North East)
+ drawBuilding(40, 5, 6, 6, 'bottom');
+
+ // 5. Houses (South)
+ drawBuilding(10, 30, 6, 5, 'top');
+ drawBuilding(22, 30, 6, 5, 'top');
+ drawBuilding(34, 30, 6, 5, 'top');
}
@@ -133,6 +139,6 @@ export class Map {
isWalkable(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false;
- return this.tiles[y][x] === '.' || this.tiles[y][x] === '>';
+ return this.tiles[y][x] === '.' || this.tiles[y][x] === '>' || this.tiles[y][x] === '<';
}
}
diff --git a/src/UI.js b/src/UI.js
index 078c60b..c70517e 100644
--- a/src/UI.js
+++ b/src/UI.js
@@ -3,7 +3,9 @@ export class UI {
this.ctx = ctx;
this.game = game;
this.messageLog = document.getElementById('message-log');
+ this.messageLog = document.getElementById('message-log');
this.inventoryList = document.getElementById('inventory-list');
+ this.spellsList = document.getElementById('spells-list');
this.popup = null;
this.popupTimeout = null;
}
@@ -16,7 +18,7 @@ export class UI {
this.messageLog.scrollTop = this.messageLog.scrollHeight;
}
- renderMap(map, player, monsters, items, tileSize) {
+ renderMap(map, player, monsters, items, projectiles, tileSize) {
// Fill background
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
@@ -66,6 +68,18 @@ export class UI {
}
}
+ // Draw Projectiles
+ if (projectiles) {
+ for (const proj of projectiles) {
+ const screenX = (proj.x - startX) * tileSize;
+ const screenY = (proj.y - startY) * tileSize;
+ if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
+ screenY >= -tileSize && screenY < this.ctx.canvas.height) {
+ this.drawEntity(screenX, screenY, proj, tileSize);
+ }
+ }
+ }
+
// Draw Player
const playerScreenX = (player.x - startX) * tileSize;
const playerScreenY = (player.y - startY) * tileSize;
@@ -85,6 +99,9 @@ export class UI {
document.getElementById('stat-ac').textContent = player.getDefense();
document.getElementById('stat-floor').textContent = depth;
document.getElementById('stat-gold').textContent = player.gold;
+ document.getElementById('stat-gold').textContent = player.gold;
+ document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`;
+ document.getElementById('stat-int').textContent = player.stats.int;
}
updateInventory(player) {
@@ -103,6 +120,16 @@ export class UI {
});
}
+ updateSpells(player) {
+ if (!this.spellsList) return;
+ this.spellsList.innerHTML = '';
+ player.spells.forEach((spell, index) => {
+ const li = document.createElement('li');
+ li.textContent = `${index + 1}. ${spell}`;
+ this.spellsList.appendChild(li);
+ });
+ }
+
drawTile(screenX, screenY, tile, size) {
if (tile === '#') {
this.ctx.fillStyle = '#808080'; // Gray wall
@@ -124,11 +151,17 @@ export class UI {
this.ctx.stroke();
} else if (tile === '>') {
- // Stairs
+ // Stairs Down
this.ctx.fillStyle = '#202020';
this.ctx.fillRect(screenX, screenY, size, size);
this.ctx.fillStyle = '#ffffff';
this.ctx.fillText('>', screenX + size / 4, screenY);
+ } else if (tile === '<') {
+ // Stairs Up
+ this.ctx.fillStyle = '#202020';
+ this.ctx.fillRect(screenX, screenY, size, size);
+ this.ctx.fillStyle = '#ffffff';
+ this.ctx.fillText('<', screenX + size / 4, screenY);
} else {
// Floor
this.ctx.fillStyle = '#202020';
@@ -186,4 +219,65 @@ export class UI {
this.ctx.fillStyle = entity.color;
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
}
+
+ drawShop(shopkeeper, player, selectedIndex) {
+ const ctx = this.ctx;
+ const width = 400;
+ const height = 300;
+ const x = (ctx.canvas.width - width) / 2;
+ const y = (ctx.canvas.height - height) / 2;
+
+ // Background
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(x, y, width, height);
+ ctx.strokeStyle = '#ffffff';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(x, y, width, height);
+
+ // Title
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '20px monospace';
+ ctx.textAlign = 'center';
+ ctx.fillText(shopkeeper.shopName, x + width / 2, y + 30);
+
+ // Items
+ ctx.font = '16px monospace';
+ ctx.textAlign = 'left';
+ let itemY = y + 70;
+
+ shopkeeper.inventory.forEach((entry, index) => {
+ const item = entry.item;
+ const price = entry.price;
+ const isSelected = index === selectedIndex;
+
+ if (isSelected) {
+ ctx.fillStyle = '#333333';
+ ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
+ ctx.fillStyle = '#ffff00';
+ ctx.fillText('>', x + 25, itemY + 10);
+ } else {
+ ctx.fillStyle = '#aaaaaa';
+ }
+
+ ctx.fillText(`${item.name}`, x + 50, itemY + 10);
+ ctx.textAlign = 'right';
+ ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
+ ctx.textAlign = 'left';
+
+ itemY += 30;
+ });
+
+ // Player Gold
+ ctx.fillStyle = '#ffd700';
+ ctx.textAlign = 'center';
+ ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 30);
+
+ // Instructions
+ ctx.fillStyle = '#888888';
+ ctx.font = '12px monospace';
+ ctx.fillText('Up/Down: Select | Enter: Buy | Esc: Exit', x + width / 2, y + height - 10);
+
+ // Reset
+ ctx.textAlign = 'start';
+ }
}
diff --git a/src/main.js b/src/main.js
index b3fa6fd..b5239b1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -2,5 +2,6 @@ import { Game } from './Game.js';
window.addEventListener('DOMContentLoaded', () => {
const game = new Game();
+ window.game = game;
game.start();
});