From e082b12cc79c0e57df32294f6d0775c251b3b604 Mon Sep 17 00:00:00 2001 From: John Kenyon Date: Wed, 24 Dec 2025 17:29:57 -0800 Subject: [PATCH] Progress --- index.html | 6 + src/Entity.js | 19 ++- src/Game.js | 403 +++++++++++++++++++++++++++++++++++++++++++++----- src/Map.js | 58 ++++---- src/UI.js | 98 +++++++++++- src/main.js | 1 + 6 files changed, 515 insertions(+), 70 deletions(-) diff --git a/index.html b/index.html index 0bdd0df..c269d8a 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,12 @@ +
+
Spells
+ +
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(); });