Added more features

main
John Kenyon 2025-12-26 16:00:28 -08:00
parent e082b12cc7
commit 02edcd9d9e
6 changed files with 713 additions and 142 deletions

View File

@ -44,9 +44,9 @@
</div> </div>
<div id="inventory-panel" class="group-box"> <div id="inventory-panel" class="group-box">
<div class="group-box-label">Inventory</div> <div class="group-box-label">Inventory</div>
<ul id="inventory-list"> <div id="inventory-link-container" style="text-align: center; padding: 5px;">
<!-- Items will go here --> <a href="#" id="inventory-link" style="color: var(--win-blue); font-weight: bold;">[ View Inventory ]</a>
</ul> </div>
</div> </div>
<div id="spells-panel" class="group-box"> <div id="spells-panel" class="group-box">
<div class="group-box-label">Spells</div> <div class="group-box-label">Spells</div>

View File

@ -1,5 +1,6 @@
export class Entity { export class Entity {
constructor(x, y, name, symbol, color) { constructor(x, y, name, symbol, color) {
if (name === 'Player') console.log("Entity Version 1.2 Loaded");
this.x = x; this.x = x;
this.y = y; this.y = y;
this.name = name; this.name = name;
@ -26,7 +27,7 @@ export class Player extends Entity {
}; };
this.mana = 10; this.mana = 10;
this.maxMana = 10; this.maxMana = 10;
this.spells = ['Magic Arrow', 'Fireball']; this.spells = ['zappy laser'];
this.level = 1; this.level = 1;
this.xp = 0; this.xp = 0;
this.maxXp = 10; this.maxXp = 10;
@ -75,6 +76,7 @@ export class Monster extends Entity {
this.level = level; this.level = level;
this.hp = level * 5; this.hp = level * 5;
this.maxHp = this.hp; this.maxHp = this.hp;
this.speed = 1.0; // Default: move every turn
} }
getDefense() { getDefense() {
@ -82,6 +84,13 @@ export class Monster extends Entity {
} }
} }
export class Archer extends Monster {
constructor(x, y, name, symbol, color, level) {
super(x, y, name, symbol, color, level);
this.range = 5;
}
}
export class Shopkeeper extends Entity { export class Shopkeeper extends Entity {
constructor(x, y, name, shopName, inventory) { constructor(x, y, name, shopName, inventory) {
super(x, y, name, '$', '#ffd700'); super(x, y, name, '$', '#ffd700');
@ -89,3 +98,17 @@ export class Shopkeeper extends Entity {
this.inventory = inventory; // Array of { item: Item, price: number } this.inventory = inventory; // Array of { item: Item, price: number }
} }
} }
export class QuestGiver extends Entity {
constructor(x, y, name, message) {
super(x, y, name, 'Q', '#ff00ff');
this.message = message;
}
}
export class WanderingQuestGiver extends Entity {
constructor(x, y, name) {
super(x, y, name, 'Q', '#00ff00');
this.quest = null; // { target: string, required: number, current: number, completed: false }
}
}

View File

@ -2,33 +2,47 @@ import { Map } from './Map.js';
import { UI } from './UI.js'; import { UI } from './UI.js';
import { Player } from './Entity.js'; import { Player } from './Entity.js';
import { Monster, Shopkeeper } from './Entity.js'; import { Monster, Shopkeeper, QuestGiver, WanderingQuestGiver, Archer } from './Entity.js';
import { Item } from './Item.js'; import { Item } from './Item.js';
export class Game { export class Game {
constructor() { constructor() {
console.log("Game Version 1.3 Loaded - Persistent Levels");
this.canvas = document.getElementById('game-canvas'); this.canvas = document.getElementById('game-canvas');
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.ui = new UI(this.ctx, this); // Pass game instance to UI for callbacks 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.player = new Player(10, 10);
this.monsters = []; this.monsters = [];
this.items = []; this.items = [];
this.gameOver = false; this.gameOver = false;
this.monsters = [];
this.items = [];
this.gameOver = false;
this.depth = 0; // Start in Town this.depth = 0; // Start in Town
this.maxDepth = 3; this.maxDepth = 10;
this.deepestDepthReached = 0;
this.dungeonLevels = {}; // Persistent storage for each depth
this.tileSize = 32; this.tileSize = 32;
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING' this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY'
this.currentShopkeeper = null; this.currentShopkeeper = null;
this.shopSelection = 0; this.shopSelection = 0;
this.shopMode = 'BUY'; // 'BUY' or 'SELL'
this.inventorySelection = 0;
this.targetingSpell = null; this.targetingSpell = null;
this.projectiles = []; this.projectiles = [];
this.questItems = [
{ name: "Golden Locket", dropped: false, activated: false },
{ name: "Silver Chalice", dropped: false, activated: false },
{ name: "Ruby Ring", dropped: false, activated: false }
];
this.wanderingQuests = [
{ target: "Rat", required: 0, current: 0, completed: false, rewarded: false },
{ target: "Kobold", required: 0, current: 0, completed: false, rewarded: false },
{ target: "Orc", required: 0, current: 0, completed: false, rewarded: false }
];
this.wanderingQuests.forEach(q => q.required = Math.floor(Math.random() * 6) + 5);
// Bind input // Bind input
window.addEventListener('keydown', (e) => this.handleInput(e)); window.addEventListener('keydown', (e) => this.handleInput(e));
} }
@ -41,40 +55,70 @@ export class Game {
generateLevel(direction = 'down') { generateLevel(direction = 'down') {
this.monsters = []; this.monsters = [];
this.items = [];
if (this.depth === 0) { if (this.dungeonLevels[this.depth]) {
this.map.generateTown(); // Load existing level
// Spawn player in center (default) or at stairs if coming up const levelData = this.dungeonLevels[this.depth];
if (direction === 'up') { this.map = levelData.map;
// Coming up from dungeon, spawn at dungeon entrance this.items = levelData.items;
this.player.x = 43;
this.player.y = 8; // Reposition player
} else { if (this.depth === 0) {
this.player.x = Math.floor(this.map.width / 2); if (direction === 'up') {
this.player.y = Math.floor(this.map.height / 2); this.player.x = 43;
} this.player.y = 8;
} 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 { } else {
// Start at last room (Stairs Down location) this.player.x = Math.floor(this.map.width / 2);
const endRoom = this.map.rooms[this.map.rooms.length - 1]; this.player.y = Math.floor(this.map.height / 2);
this.player.x = Math.floor(endRoom.x + endRoom.w / 2); }
this.player.y = Math.floor(endRoom.y + endRoom.h / 2); } else {
if (this.map.rooms.length > 0) {
if (direction === 'down') {
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 {
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);
}
} }
} }
} else {
// Generate NEW level
this.items = [];
this.map = new Map(50, 50);
if (this.depth === 0) {
this.map.generateTown();
this.player.x = Math.floor(this.map.width / 2);
this.player.y = Math.floor(this.map.height / 2);
} else {
this.map.generate();
if (this.map.rooms.length > 0) {
if (direction === 'down') {
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 {
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.spawnItems();
this.spawnStairs();
// Save new level state
this.dungeonLevels[this.depth] = {
map: this.map,
items: this.items
};
} }
this.spawnMonsters(); this.spawnMonsters(); // Always respawn monsters
this.spawnItems();
this.spawnStairs();
this.render(); this.render();
this.ui.updateStats(this.player, this.depth); this.ui.updateStats(this.player, this.depth);
@ -89,7 +133,14 @@ export class Game {
return; return;
} }
// Save current level state
this.dungeonLevels[this.depth] = {
map: this.map,
items: this.items
};
this.depth++; this.depth++;
this.deepestDepthReached = Math.max(this.deepestDepthReached, this.depth);
this.ui.log(`You descend to level ${this.depth}...`); this.ui.log(`You descend to level ${this.depth}...`);
this.generateLevel('down'); this.generateLevel('down');
} }
@ -100,6 +151,12 @@ export class Game {
return; return;
} }
// Save current level state
this.dungeonLevels[this.depth] = {
map: this.map,
items: this.items
};
this.depth--; this.depth--;
if (this.depth === 0) { if (this.depth === 0) {
this.ui.log("You return to the Town of Oakhaven."); this.ui.log("You return to the Town of Oakhaven.");
@ -162,11 +219,34 @@ export class Game {
// General Store (West) // General Store (West)
const generalStoreInventory = [ const generalStoreInventory = [
{ item: new Item("Potion", "potion", 1, { heal: 5 }), price: 20 } { item: new Item("Potion", "potion", 1, { heal: 5 }), price: 20 },
{ item: new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" }), price: 300 },
{ item: new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" }), price: 200 },
{ item: new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }), price: 400 },
{ item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 }
]; ];
const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory); const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory);
this.monsters.push(merchant); this.monsters.push(merchant);
// Spawn Quest Givers in Houses
const q1 = new QuestGiver(13, 32, "Villager", "Please bring back my Golden Locket! It was stolen by an orc!");
const q2 = new QuestGiver(25, 32, "Old Man", "Please bring back my Silver Chalice! It was stolen by an orc!");
const q3 = new QuestGiver(37, 32, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!");
this.monsters.push(q1, q2, q3);
// Spawn Wandering Quest Givers
for (let i = 0; i < 3; i++) {
let wx, wy;
do {
wx = Math.floor(Math.random() * this.map.width);
wy = Math.floor(Math.random() * this.map.height);
} while (!this.map.isWalkable(wx, wy));
const wqg = new WanderingQuestGiver(wx, wy, `Traveler ${i + 1}`);
wqg.quest = this.wanderingQuests[i];
this.monsters.push(wqg);
}
return; return;
} }
@ -182,20 +262,37 @@ export class Game {
const type = Math.random(); const type = Math.random();
let monster; let monster;
// Difficulty scaling // Difficulty scaling for 10 levels
if (this.depth === 1) { if (this.depth <= 2) {
if (type < 0.7) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 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 monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
} else if (this.depth === 2) { } else if (this.depth <= 4) {
if (type < 0.4) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1); if (type < 0.3) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1);
else if (type < 0.8) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); else if (type < 0.6) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
else monster = new Monster(mx, my, "Orc", "o", "#008000", 3); else if (type < 0.8) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
else monster = new Archer(mx, my, "Skeleton Archer", "s", "#eeeeee", 3);
} else if (this.depth <= 7) {
if (type < 0.2) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
else if (type < 0.5) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
else if (type < 0.8) monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 5);
else monster = new Archer(mx, my, "Elite Archer", "S", "#ffffff", 5);
} else { } else {
if (type < 0.3) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); if (type < 0.2) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
else if (type < 0.7) monster = new Monster(mx, my, "Orc", "o", "#008000", 3); else if (type < 0.4) monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 5);
else monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 4); else if (type < 0.6) monster = new Monster(mx, my, "Troll", "T", "#0000ff", 7);
else if (type < 0.8) monster = new Archer(mx, my, "Sniper", "S", "#ff00ff", 8);
else monster = new Monster(mx, my, "Dragon", "W", "#ff0000", 10);
} }
// Set Monster Speeds
if (monster.name === "Rat") monster.speed = 1.0;
else if (monster.name === "Kobold") monster.speed = 1.0;
else if (monster.name === "Orc") monster.speed = 0.8;
else if (monster instanceof Archer) monster.speed = 0.7;
else if (monster.name === "Ogre") monster.speed = 0.5;
else if (monster.name === "Troll") monster.speed = 0.6;
else if (monster.name === "Dragon") monster.speed = 0.4;
this.monsters.push(monster); this.monsters.push(monster);
} }
} }
@ -209,6 +306,7 @@ export class Game {
const boss = new Monster(mx, my, "The Dungeon Lord", "D", "#ff00ff", 10); const boss = new Monster(mx, my, "The Dungeon Lord", "D", "#ff00ff", 10);
boss.hp = 100; boss.hp = 100;
boss.maxHp = 100; boss.maxHp = 100;
boss.speed = 0.6;
this.monsters.push(boss); this.monsters.push(boss);
} }
} }
@ -228,21 +326,39 @@ export class Game {
let item; let item;
// Better loot deeper // Better loot deeper
let tier = this.depth; let tier = Math.floor(this.depth / 2) + 1;
if (Math.random() < 0.2) tier++; // Chance for higher tier if (Math.random() < 0.2) tier++; // Chance for higher tier
if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 }); if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
else if (type < 0.5) { else if (type < 0.45) {
if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 }); 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 if (tier === 2) item = new Item("Short Sword", "weapon", 2, { damage: 6 });
else item = new Item("Long Sword", "weapon", 3, { damage: 8 }); else if (tier === 3) item = new Item("Long Sword", "weapon", 3, { damage: 8 });
} else if (type < 0.7) { else if (tier === 4) item = new Item("Great Sword", "weapon", 4, { damage: 12 });
else item = new Item("Dragon Blade", "weapon", 5, { damage: 20 });
} else if (type < 0.6) {
if (tier <= 1) item = new Item("Buckler", "shield", 1, { defense: 1 }); 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 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("Kite Shield", "shield", 3, { defense: 4 });
} else { else item = new Item("Tower Shield", "shield", 4, { defense: 6 });
if (tier >= 3) item = new Item("Great Axe", "weapon", 4, { damage: 12 }); } else if (type < 0.7) {
if (tier >= 5) item = new Item("Great Axe", "weapon", 5, { damage: 18 });
else if (tier >= 3) item = new Item("War Hammer", "weapon", 3, { damage: 10 });
else item = new Item("Short Sword", "weapon", 2, { damage: 6 }); else item = new Item("Short Sword", "weapon", 2, { damage: 6 });
} else if (type < 0.8) {
// Spellbooks
const spellRoll = Math.random();
if (spellRoll < 0.25) {
item = new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" });
} else if (spellRoll < 0.5) {
item = new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" });
} else if (spellRoll < 0.75) {
item = new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" });
} else {
item = new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" });
}
} else {
item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
} }
item.x = ix; item.x = ix;
@ -260,6 +376,11 @@ export class Game {
return; return;
} }
if (this.gameState === 'INVENTORY') {
this.handleInventoryInput(e);
return;
}
if (this.gameState === 'TARGETING') { if (this.gameState === 'TARGETING') {
this.handleTargetingInput(e); this.handleTargetingInput(e);
return; return;
@ -283,6 +404,7 @@ export class Game {
case 'NumPad1': 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 'NumPad3': dx = 1; dy = 1; handled = true; break;
case '.': case 'NumPad5': handled = true; break; // Wait case '.': case 'NumPad5': handled = true; break; // Wait
case 'i': case 'I': this.toggleInventory(); handled = true; break;
case 'Enter': case 'Enter':
case '>': case '>':
if (this.map.tiles[this.player.y][this.player.x] === '>') { if (this.map.tiles[this.player.y][this.player.x] === '>') {
@ -304,8 +426,31 @@ export class Game {
handled = true; handled = true;
break; break;
case 'g': this.pickupItem(); handled = true; break; case 'g': this.pickupItem(); handled = true; break;
case '1': this.startTargeting('Magic Arrow'); handled = true; break; case '1':
case '2': this.startTargeting('Fireball'); handled = true; break; if (this.player.spells.includes('zappy laser')) this.startTargeting('zappy laser');
else this.ui.log("You don't know that spell!");
handled = true;
break;
case '2':
if (this.player.spells.includes('Fireball')) this.startTargeting('Fireball');
else this.ui.log("You don't know that spell!");
handled = true;
break;
case '3':
if (this.player.spells.includes('Light')) this.castLightSpell();
else this.ui.log("You don't know that spell!");
handled = true;
break;
case '4':
if (this.player.spells.includes('Heal')) this.castHealSpell();
else this.ui.log("You don't know that spell!");
handled = true;
break;
case '5':
if (this.player.spells.includes('Return')) this.castReturnSpell();
else this.ui.log("You don't know that spell!");
handled = true;
break;
} }
if (handled) { if (handled) {
@ -320,7 +465,7 @@ export class Game {
handleShopInput(e) { handleShopInput(e) {
e.preventDefault(); e.preventDefault();
const inventory = this.currentShopkeeper.inventory; const inventory = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.player.inventory;
switch (e.key) { switch (e.key) {
case 'ArrowUp': case 'ArrowUp':
@ -333,9 +478,17 @@ export class Game {
this.shopSelection++; this.shopSelection++;
if (this.shopSelection >= inventory.length) this.shopSelection = 0; if (this.shopSelection >= inventory.length) this.shopSelection = 0;
break; break;
case 'Tab':
this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY';
this.shopSelection = 0;
break;
case 'Enter': case 'Enter':
case ' ': case ' ':
this.buyItem(inventory[this.shopSelection]); if (this.shopMode === 'BUY') {
this.buyItem(inventory[this.shopSelection]);
} else {
this.sellItem(this.shopSelection);
}
break; break;
case 'Escape': case 'Escape':
this.closeShop(); this.closeShop();
@ -348,6 +501,7 @@ export class Game {
this.gameState = 'SHOP'; this.gameState = 'SHOP';
this.currentShopkeeper = shopkeeper; this.currentShopkeeper = shopkeeper;
this.shopSelection = 0; this.shopSelection = 0;
this.shopMode = 'BUY';
this.ui.log(`You talk to ${shopkeeper.name}.`); this.ui.log(`You talk to ${shopkeeper.name}.`);
this.render(); this.render();
} }
@ -358,7 +512,48 @@ export class Game {
this.render(); this.render();
} }
toggleInventory() {
if (this.gameState === 'INVENTORY') {
this.gameState = 'PLAY';
} else {
this.gameState = 'INVENTORY';
this.inventorySelection = 0;
}
this.render();
}
handleInventoryInput(e) {
e.preventDefault();
const inventory = this.player.inventory;
switch (e.key) {
case 'ArrowUp':
case 'NumPad8':
this.inventorySelection--;
if (this.inventorySelection < 0) this.inventorySelection = inventory.length - 1;
break;
case 'ArrowDown':
case 'NumPad2':
this.inventorySelection++;
if (this.inventorySelection >= inventory.length) this.inventorySelection = 0;
break;
case 'Enter':
case ' ':
if (inventory[this.inventorySelection]) {
this.useItem(this.inventorySelection);
}
break;
case 'Escape':
case 'i':
case 'I':
this.gameState = 'PLAY';
break;
}
this.render();
}
buyItem(entry) { buyItem(entry) {
if (!entry) return;
if (this.player.gold >= entry.price) { if (this.player.gold >= entry.price) {
this.player.gold -= entry.price; this.player.gold -= entry.price;
// Clone item to avoid reference issues if buying multiple // Clone item to avoid reference issues if buying multiple
@ -376,6 +571,44 @@ export class Game {
} }
} }
sellItem(index) {
const item = this.player.inventory[index];
if (!item) return;
// Cannot sell quest items or the ancient map
if (item.type === 'quest' || item.type === 'map') {
this.ui.log("This item is too precious to sell!");
return;
}
// Base price calculation (1/4 of buy price)
// We'll estimate price based on item level/type if it doesn't have a value
let baseValue = 40; // Default
if (item.type === 'weapon') baseValue = 100 * item.level;
if (item.type === 'shield') baseValue = 40 * item.level;
if (item.type === 'potion') baseValue = 20;
if (item.type === 'spellbook') baseValue = 400;
const sellPrice = Math.floor(baseValue / 4);
// Unequip if selling equipped item
if (this.player.equipment.weapon === item) this.player.equipment.weapon = null;
if (this.player.equipment.shield === item) this.player.equipment.shield = null;
this.player.gold += sellPrice;
this.player.inventory.splice(index, 1);
this.ui.log(`You sold ${item.name} for ${sellPrice} gold.`);
// Reset selection if last item was sold
if (this.shopSelection >= this.player.inventory.length) {
this.shopSelection = Math.max(0, this.player.inventory.length - 1);
}
this.ui.updateInventory(this.player);
this.ui.updateStats(this.player, this.depth);
}
movePlayer(dx, dy) { movePlayer(dx, dy) {
const newX = this.player.x + dx; const newX = this.player.x + dx;
const newY = this.player.y + dy; const newY = this.player.y + dy;
@ -385,6 +618,43 @@ export class Game {
if (targetMonster) { if (targetMonster) {
if (targetMonster instanceof Shopkeeper) { if (targetMonster instanceof Shopkeeper) {
this.openShop(targetMonster); this.openShop(targetMonster);
} else if (targetMonster instanceof QuestGiver) {
// ... quest item logic ...
const itemName = targetMonster.message.match(/my (.*)!/)[1];
const questData = this.questItems.find(qi => qi.name === itemName);
// Activate quest if not already
if (questData) questData.activated = true;
const itemIndex = this.player.inventory.findIndex(i => i.name === itemName);
if (itemIndex !== -1) {
this.player.inventory.splice(itemIndex, 1);
this.player.gold += 10;
this.ui.showPopup("Thank you! Have some gold!", 2000);
this.ui.log(`${targetMonster.name} says: "Thank you! Have some gold!"`);
this.ui.log(`You received 10 gold for returning the ${itemName}.`);
this.ui.updateInventory(this.player);
this.ui.updateStats(this.player, this.depth);
} else {
this.ui.showPopup(targetMonster.message, 2000);
this.ui.log(`${targetMonster.name} says: "${targetMonster.message}"`);
}
} else if (targetMonster instanceof WanderingQuestGiver) {
const q = targetMonster.quest;
if (q.rewarded) {
this.ui.showPopup("Good luck on your travels!", 2000);
} else if (q.completed) {
this.player.gold += 10;
q.rewarded = true;
this.ui.showPopup("Great work! Here is 10 gold.", 2000);
this.ui.log(`${targetMonster.name} says: "Great work! Here is 10 gold."`);
this.ui.updateStats(this.player, this.depth);
} else {
const msg = `Please kill ${q.required} ${q.target}s. Progress: ${q.current}/${q.required}`;
this.ui.showPopup(msg, 2000);
this.ui.log(`${targetMonster.name} says: "${msg}"`);
}
} else { } else {
this.attack(this.player, targetMonster); this.attack(this.player, targetMonster);
} }
@ -457,7 +727,7 @@ export class Game {
let symbol = '*'; let symbol = '*';
let color = '#ffffff'; let color = '#ffffff';
if (spellName === 'Magic Arrow') { if (spellName === 'zappy laser') {
manaCost = 2; manaCost = 2;
damage = Math.floor(this.player.stats.int / 2) + Math.floor(Math.random() * 4) + 1; damage = Math.floor(this.player.stats.int / 2) + Math.floor(Math.random() * 4) + 1;
symbol = '*'; symbol = '*';
@ -486,13 +756,91 @@ export class Game {
symbol: symbol, symbol: symbol,
color: color, color: color,
damage: damage, damage: damage,
name: spellName name: spellName,
owner: this.player
}; };
this.projectiles.push(projectile); this.projectiles.push(projectile);
this.animateProjectile(projectile); this.animateProjectile(projectile);
} }
castLightSpell() {
const manaCost = 3;
if (this.player.mana < manaCost) {
this.ui.log("Not enough mana!");
return;
}
this.player.mana -= manaCost;
this.ui.updateStats(this.player, this.depth);
this.ui.log("You cast Light!");
// Permanently light a 5-square radius
const radius = 5;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const lx = this.player.x + dx;
const ly = this.player.y + dy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= radius + 0.5) {
if (lx >= 0 && lx < this.map.width && ly >= 0 && ly < this.map.height) {
this.map.permanentlyLit[ly][lx] = true;
}
}
}
}
this.render();
}
castHealSpell() {
const manaCost = 4;
if (this.player.mana < manaCost) {
this.ui.log("Not enough mana!");
return;
}
const healAmount = 5 + Math.floor(this.player.stats.int / 2);
this.player.mana -= manaCost;
this.player.hp = Math.min(this.player.hp + healAmount, this.player.maxHp);
this.ui.updateStats(this.player, this.depth);
this.ui.log(`You cast Heal and recover ${healAmount} HP!`);
this.render();
}
castReturnSpell() {
const manaCost = 6;
if (this.player.mana < manaCost) {
this.ui.log("Not enough mana!");
return;
}
this.player.mana -= manaCost;
this.ui.updateStats(this.player, this.depth);
if (this.depth > 0) {
// Save current level
this.dungeonLevels[this.depth] = {
map: this.map,
items: this.items
};
this.depth = 0;
this.ui.log("You cast Return and teleport to town!");
this.generateLevel('down'); // Center of town
} else {
if (this.deepestDepthReached === 0) {
this.ui.log("You haven't entered the dungeon yet!");
this.player.mana += manaCost; // Refund
return;
}
this.depth = this.deepestDepthReached;
this.ui.log(`You cast Return and teleport to level ${this.depth}!`);
this.generateLevel('down'); // Start of level
}
this.render();
}
animateProjectile(projectile) { animateProjectile(projectile) {
const interval = setInterval(() => { const interval = setInterval(() => {
const newX = projectile.x + projectile.dx; const newX = projectile.x + projectile.dx;
@ -508,12 +856,12 @@ export class Game {
} }
const monster = this.monsters.find(m => m.x === newX && m.y === newY); const monster = this.monsters.find(m => m.x === newX && m.y === newY);
if (monster) { if (monster && projectile.owner === this.player) {
clearInterval(interval); clearInterval(interval);
this.removeProjectile(projectile); this.removeProjectile(projectile);
// Shopkeepers are immune // Shopkeepers and QuestGivers are immune
if (monster instanceof Shopkeeper) { if (monster instanceof Shopkeeper || monster instanceof QuestGiver || monster instanceof WanderingQuestGiver) {
this.ui.log(`${projectile.name} hits ${monster.name} but does nothing.`); this.ui.log(`${projectile.name} hits ${monster.name} but does nothing.`);
} else { } else {
// Deal damage // Deal damage
@ -527,6 +875,15 @@ export class Game {
return; return;
} }
// Hit player?
if (newX === this.player.x && newY === this.player.y && projectile.owner !== this.player) {
clearInterval(interval);
this.removeProjectile(projectile);
this.attack(projectile.owner, this.player, projectile.damage);
this.render();
return;
}
projectile.x = newX; projectile.x = newX;
projectile.y = newY; projectile.y = newY;
this.render(); this.render();
@ -551,6 +908,36 @@ export class Game {
killMonster(monster) { killMonster(monster) {
this.ui.log(`${monster.name} dies!`); this.ui.log(`${monster.name} dies!`);
this.monsters = this.monsters.filter(m => m !== monster); this.monsters = this.monsters.filter(m => m !== monster);
// Track wandering quests
this.wanderingQuests.forEach(q => {
if (q.target === monster.name && !q.completed) {
q.current++;
if (q.current >= q.required) {
q.completed = true;
this.ui.log(`Quest Objective Complete: ${q.required} ${q.target}s killed!`);
}
}
});
// Quest Item Drop logic
if (monster.name === "Orc") {
const potentialDrops = this.questItems.filter(qi => qi.activated && !qi.dropped);
if (potentialDrops.length > 0) {
// Determine if we should drop one.
if (Math.random() < 0.3) {
const itemData = potentialDrops[0];
itemData.dropped = true;
const questItem = new Item(itemData.name, "quest", 1, {});
questItem.x = monster.x;
questItem.y = monster.y;
this.items.push(questItem);
this.ui.log(`The Orc was carrying a ${itemData.name}!`);
}
}
}
const gold = monster.level * (Math.floor(Math.random() * 5) + 1); const gold = monster.level * (Math.floor(Math.random() * 5) + 1);
this.player.gold += gold; this.player.gold += gold;
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`); this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
@ -566,6 +953,32 @@ export class Game {
} }
} }
monsterShoot(monster) {
const dx = Math.sign(this.player.x - monster.x);
const dy = Math.sign(this.player.y - monster.y);
const damage = monster.level + Math.floor(Math.random() * 3);
const projectile = {
x: monster.x,
y: monster.y,
dx: dx,
dy: dy,
symbol: '-',
color: '#ffffff',
damage: damage,
name: 'Arrow',
owner: monster
};
// Adjust symbol based on direction
if (dy !== 0 && dx === 0) projectile.symbol = '|';
if (dy !== 0 && dx !== 0) projectile.symbol = (dx * dy > 0) ? '\\' : '/';
this.projectiles.push(projectile);
this.animateProjectile(projectile);
this.ui.log(`${monster.name} fires an arrow!`);
}
pickupItem() { pickupItem() {
const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y); const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y);
if (itemIndex !== -1) { if (itemIndex !== -1) {
@ -597,6 +1010,16 @@ export class Game {
// Remove potion // Remove potion
this.player.inventory.splice(index, 1); this.player.inventory.splice(index, 1);
} }
} else if (item.type === 'spellbook') {
const spell = item.stats.spell;
if (this.player.spells.includes(spell)) {
this.ui.log(`You already know the ${spell} spell.`);
} else {
this.player.spells.push(spell);
this.ui.log(`You learned the ${spell} spell!`);
this.player.inventory.splice(index, 1);
this.ui.updateSpells(this.player);
}
} }
this.ui.updateInventory(this.player); this.ui.updateInventory(this.player);
@ -604,9 +1027,9 @@ export class Game {
this.render(); // Re-render to show equipped status if we add that this.render(); // Re-render to show equipped status if we add that
} }
attack(attacker, defender) { attack(attacker, defender, presetDamage = null) {
// Shopkeepers are invincible/pacifist // Shopkeepers and QuestGivers are invincible/pacifist
if (defender instanceof Shopkeeper) return; if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver) return;
// Peasants are invincible // Peasants are invincible
if (defender.name === "Peasant") { if (defender.name === "Peasant") {
@ -623,11 +1046,13 @@ export class Game {
return; return;
} }
let damage = 0; let damage = presetDamage;
if (attacker === this.player) { if (damage === null) {
damage = this.player.getDamage() + Math.floor(Math.random() * 2); if (attacker === this.player) {
} else { damage = this.player.getDamage() + Math.floor(Math.random() * 2);
damage = 1 + Math.floor(Math.random() * 2); // Monster damage } else {
damage = 1 + Math.floor(Math.random() * 2); // Monster damage
}
} }
// Apply defense // Apply defense
@ -638,42 +1063,24 @@ export class Game {
this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`); this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`);
if (defender.hp <= 0) { if (defender.hp <= 0) {
this.ui.log(`${defender.name} dies!`);
if (defender === this.player) { if (defender === this.player) {
this.ui.log("GAME OVER"); this.ui.log("GAME OVER");
this.gameOver = true; this.gameOver = true;
setTimeout(() => alert("Game Over! Refresh to restart."), 100); setTimeout(() => alert("Game Over! Refresh to restart."), 100);
} else { } else {
// Remove monster this.killMonster(defender);
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() { updateMonsters() {
for (const monster of this.monsters) { for (const monster of this.monsters) {
if (monster instanceof Shopkeeper) continue; // Shopkeepers don't move if (monster instanceof Shopkeeper || monster instanceof QuestGiver) continue; // Shopkeepers and QuestGivers don't move
if (monster.name === "Peasant") { // Speed check: Does the monster move this turn?
if (monster.speed !== undefined && Math.random() > monster.speed) continue;
if (monster instanceof WanderingQuestGiver || monster.name === "Peasant") {
// Random walk (Slow: 10% chance to move) // Random walk (Slow: 10% chance to move)
if (Math.random() < 0.1) { if (Math.random() < 0.1) {
const dx = Math.floor(Math.random() * 3) - 1; const dx = Math.floor(Math.random() * 3) - 1;
@ -688,12 +1095,22 @@ export class Game {
continue; continue;
} }
// Simple chase logic // Simple chase or ranged logic
const dx = this.player.x - monster.x; const dx = this.player.x - monster.x;
const dy = this.player.y - monster.y; const dy = this.player.y - monster.y;
const dist = Math.sqrt(dx * dx + dy * dy); const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 8) { // Aggro range if (dist < 8) { // Aggro range
// Ranged behavior for Archers
if (monster instanceof Archer && dist > 1.5 && dist <= monster.range) {
const isOrthogonal = this.player.x === monster.x || this.player.y === monster.y;
const isDiagonal = Math.abs(dx) === Math.abs(dy);
if (isOrthogonal || isDiagonal) {
this.monsterShoot(monster);
continue;
}
}
let moveX = 0; let moveX = 0;
let moveY = 0; let moveY = 0;
@ -724,7 +1141,11 @@ export class Game {
this.ui.renderMap(this.map, this.player, this.monsters, this.items, this.projectiles, this.tileSize); this.ui.renderMap(this.map, this.player, this.monsters, this.items, this.projectiles, this.tileSize);
if (this.gameState === 'SHOP') { if (this.gameState === 'SHOP') {
this.ui.drawShop(this.currentShopkeeper, this.player, this.shopSelection); this.ui.drawShop(this.currentShopkeeper, this.player, this.shopSelection, this.shopMode);
}
if (this.gameState === 'INVENTORY') {
this.ui.drawInventory(this.player, this.inventorySelection);
} }
this.ui.updateStats(this.player, this.depth); this.ui.updateStats(this.player, this.depth);

View File

@ -14,5 +14,7 @@ export class Item {
if (type === 'shield') this.symbol = ']'; if (type === 'shield') this.symbol = ']';
if (type === 'potion') this.symbol = '!'; if (type === 'potion') this.symbol = '!';
if (type === 'map') { this.symbol = '?'; this.color = '#ffd700'; } if (type === 'map') { this.symbol = '?'; this.color = '#ffd700'; }
if (type === 'quest') { this.symbol = '*'; this.color = '#ff00ff'; }
if (type === 'spellbook') { this.symbol = 'B'; this.color = '#00ffff'; }
} }
} }

View File

@ -4,15 +4,20 @@ export class Map {
this.height = height; this.height = height;
this.tiles = []; this.tiles = [];
this.rooms = []; this.rooms = [];
this.permanentlyLit = []; // New grid for spell effects
} }
generate() { generate() {
this.isTown = false;
this.rooms = []; this.rooms = [];
// Initialize with walls this.permanentlyLit = [];
// Initialize with walls and dark
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
this.tiles[y] = []; this.tiles[y] = [];
this.permanentlyLit[y] = [];
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
this.tiles[y][x] = '#'; this.tiles[y][x] = '#';
this.permanentlyLit[y][x] = false;
} }
} }
@ -61,12 +66,16 @@ export class Map {
} }
generateTown() { generateTown() {
this.isTown = true;
this.rooms = []; this.rooms = [];
this.permanentlyLit = [];
// Fill with grass/floor // Fill with grass/floor
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
this.tiles[y] = []; this.tiles[y] = [];
this.permanentlyLit[y] = [];
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
this.tiles[y][x] = '.'; this.tiles[y][x] = '.';
this.permanentlyLit[y][x] = true; // Town is all lit
} }
} }
@ -141,4 +150,20 @@ export class Map {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false; if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false;
return this.tiles[y][x] === '.' || this.tiles[y][x] === '>' || this.tiles[y][x] === '<'; return this.tiles[y][x] === '.' || this.tiles[y][x] === '>' || this.tiles[y][x] === '<';
} }
isLit(x, y) {
// Town is always fully lit
if (this.isTown) return true;
// Check permanently lit tiles (from spell)
if (this.permanentlyLit[y] && this.permanentlyLit[y][x]) return true;
// Check if tile is inside any room
for (const room of this.rooms) {
if (x >= room.x && x < room.x + room.w && y >= room.y && y < room.y + room.h) {
return true;
}
}
return false;
}
} }

182
src/UI.js
View File

@ -1,11 +1,19 @@
export class UI { export class UI {
constructor(ctx, game) { constructor(ctx, game) {
console.log("UI Version 1.1 Loaded");
this.ctx = ctx; this.ctx = ctx;
this.game = game; this.game = game;
this.messageLog = document.getElementById('message-log'); 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.spellsList = document.getElementById('spells-list');
this.inventoryLink = document.getElementById('inventory-link');
if (this.inventoryLink) {
this.inventoryLink.onclick = (e) => {
e.preventDefault();
this.game.toggleInventory();
};
}
this.popup = null; this.popup = null;
this.popupTimeout = null; this.popupTimeout = null;
} }
@ -40,7 +48,17 @@ export class UI {
if (mapX >= 0 && mapX < map.width && mapY >= 0 && mapY < map.height) { if (mapX >= 0 && mapX < map.width && mapY >= 0 && mapY < map.height) {
const tile = map.tiles[mapY][mapX]; const tile = map.tiles[mapY][mapX];
this.drawTile(x * tileSize, y * tileSize, tile, tileSize); const isLit = map.isLit(mapX, mapY);
const dist = Math.sqrt(Math.pow(player.x - mapX, 2) + Math.pow(player.y - mapY, 2));
const inRadius = dist <= 3.5; // Radius of 3 (using 3.5 for better circle approximation)
if (isLit || inRadius) {
this.drawTile(x * tileSize, y * tileSize, tile, tileSize);
} else {
// Draw nothing or a very dark tile for "unseen" areas
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
} }
} }
} }
@ -48,6 +66,10 @@ export class UI {
// Draw Items // Draw Items
if (items) { if (items) {
for (const item of items) { for (const item of items) {
const isLit = map.isLit(item.x, item.y);
const dist = Math.sqrt(Math.pow(player.x - item.x, 2) + Math.pow(player.y - item.y, 2));
if (!(isLit || dist <= 3.5)) continue;
const screenX = (item.x - startX) * tileSize; const screenX = (item.x - startX) * tileSize;
const screenY = (item.y - startY) * tileSize; const screenY = (item.y - startY) * tileSize;
if (screenX >= -tileSize && screenX < this.ctx.canvas.width && if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
@ -59,6 +81,10 @@ export class UI {
// Draw Monsters // Draw Monsters
for (const monster of monsters) { for (const monster of monsters) {
const isLit = map.isLit(monster.x, monster.y);
const dist = Math.sqrt(Math.pow(player.x - monster.x, 2) + Math.pow(player.y - monster.y, 2));
if (!(isLit || dist <= 3.5)) continue;
const screenX = (monster.x - startX) * tileSize; const screenX = (monster.x - startX) * tileSize;
const screenY = (monster.y - startY) * tileSize; const screenY = (monster.y - startY) * tileSize;
// Only draw if visible // Only draw if visible
@ -71,6 +97,10 @@ export class UI {
// Draw Projectiles // Draw Projectiles
if (projectiles) { if (projectiles) {
for (const proj of projectiles) { for (const proj of projectiles) {
const isLit = map.isLit(proj.x, proj.y);
const dist = Math.sqrt(Math.pow(player.x - proj.x, 2) + Math.pow(player.y - proj.y, 2));
if (!(isLit || dist <= 3.5)) continue;
const screenX = (proj.x - startX) * tileSize; const screenX = (proj.x - startX) * tileSize;
const screenY = (proj.y - startY) * tileSize; const screenY = (proj.y - startY) * tileSize;
if (screenX >= -tileSize && screenX < this.ctx.canvas.width && if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
@ -99,25 +129,76 @@ export class UI {
document.getElementById('stat-ac').textContent = player.getDefense(); document.getElementById('stat-ac').textContent = player.getDefense();
document.getElementById('stat-floor').textContent = depth; document.getElementById('stat-floor').textContent = depth;
document.getElementById('stat-gold').textContent = player.gold; 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-mana').textContent = `${player.mana}/${player.maxMana}`;
document.getElementById('stat-int').textContent = player.stats.int; document.getElementById('stat-int').textContent = player.stats.int;
} }
updateInventory(player) { updateInventory(player) {
this.inventoryList.innerHTML = ''; // Now handled by canvas-based inventory box
player.inventory.forEach((item, index) => { if (this.inventoryLink) {
const li = document.createElement('li'); this.inventoryLink.textContent = `[ View Inventory (${player.inventory.length}) ]`;
li.textContent = item.name; }
if (player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item) { }
li.textContent += " (E)";
} drawInventory(player, selectedIndex) {
li.style.cursor = 'pointer'; const ctx = this.ctx;
li.onclick = () => { const width = 500;
this.game.useItem(index); const height = 400;
}; const x = (ctx.canvas.width - width) / 2;
this.inventoryList.appendChild(li); 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('Inventory', x + width / 2, y + 30);
// Items
ctx.font = '16px monospace';
ctx.textAlign = 'left';
let itemY = y + 70;
if (player.inventory.length === 0) {
ctx.fillStyle = '#888888';
ctx.textAlign = 'center';
ctx.fillText('Empty', x + width / 2, itemY + 20);
} else {
player.inventory.forEach((item, index) => {
const isSelected = index === selectedIndex;
const isEquipped = player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item;
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';
}
let displayName = item.name;
if (isEquipped) displayName += " (E)";
ctx.fillText(displayName, x + 50, itemY + 10);
itemY += 30;
});
}
// Instructions
ctx.fillStyle = '#888888';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText('1: Laser | 2: Fireball | 3: Light | 4: Heal | 5: Return | Up/Down: Select | Enter: Use/Equip | Esc/i: Close', x + width / 2, y + height - 20);
// Reset
ctx.textAlign = 'start';
} }
updateSpells(player) { updateSpells(player) {
@ -220,10 +301,10 @@ export class UI {
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY); this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
} }
drawShop(shopkeeper, player, selectedIndex) { drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') {
const ctx = this.ctx; const ctx = this.ctx;
const width = 400; const width = 500;
const height = 300; const height = 400;
const x = (ctx.canvas.width - width) / 2; const x = (ctx.canvas.width - width) / 2;
const y = (ctx.canvas.height - height) / 2; const y = (ctx.canvas.height - height) / 2;
@ -238,44 +319,63 @@ export class UI {
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.font = '20px monospace'; ctx.font = '20px monospace';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(shopkeeper.shopName, x + width / 2, y + 30); ctx.fillText(`${shopkeeper.shopName} - ${mode}`, x + width / 2, y + 30);
// Items // Items
ctx.font = '16px monospace'; ctx.font = '16px monospace';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
let itemY = y + 70; let itemY = y + 70;
shopkeeper.inventory.forEach((entry, index) => { const list = mode === 'BUY' ? shopkeeper.inventory : player.inventory;
const item = entry.item;
const price = entry.price;
const isSelected = index === selectedIndex;
if (isSelected) { if (list.length === 0) {
ctx.fillStyle = '#333333'; ctx.fillStyle = '#888888';
ctx.fillRect(x + 20, itemY - 5, width - 40, 25); ctx.textAlign = 'center';
ctx.fillStyle = '#ffff00'; ctx.fillText(mode === 'BUY' ? 'Sold Out' : 'Nothing to sell', x + width / 2, itemY + 20);
ctx.fillText('>', x + 25, itemY + 10); } else {
} else { list.forEach((entry, index) => {
ctx.fillStyle = '#aaaaaa'; const isSelected = index === selectedIndex;
} const item = mode === 'BUY' ? entry.item : entry;
let price = 0;
if (mode === 'BUY') {
price = entry.price;
} else {
// Calc sell price
let baseValue = 40;
if (item.type === 'weapon') baseValue = 100 * item.level;
if (item.type === 'shield') baseValue = 40 * item.level;
if (item.type === 'potion') baseValue = 20;
price = Math.floor(baseValue / 4);
}
ctx.fillText(`${item.name}`, x + 50, itemY + 10); if (isSelected) {
ctx.textAlign = 'right'; ctx.fillStyle = '#333333';
ctx.fillText(`${price}g`, x + width - 50, itemY + 10); ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
ctx.textAlign = 'left'; ctx.fillStyle = '#ffff00';
ctx.fillText('>', x + 25, itemY + 10);
} else {
ctx.fillStyle = '#aaaaaa';
}
itemY += 30; 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 // Player Gold
ctx.fillStyle = '#ffd700'; ctx.fillStyle = '#ffd700';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 30); ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 50);
// Instructions // Instructions
ctx.fillStyle = '#888888'; ctx.fillStyle = '#888888';
ctx.font = '12px monospace'; ctx.font = '12px monospace';
ctx.fillText('Up/Down: Select | Enter: Buy | Esc: Exit', x + width / 2, y + height - 10); ctx.fillText('Tab: Toggle Buy/Sell | Up/Down: Select | Enter: Action | Esc: Exit', x + width / 2, y + height - 20);
// Reset // Reset
ctx.textAlign = 'start'; ctx.textAlign = 'start';