Added more features
parent
e082b12cc7
commit
02edcd9d9e
|
|
@ -44,9 +44,9 @@
|
|||
</div>
|
||||
<div id="inventory-panel" class="group-box">
|
||||
<div class="group-box-label">Inventory</div>
|
||||
<ul id="inventory-list">
|
||||
<!-- Items will go here -->
|
||||
</ul>
|
||||
<div id="inventory-link-container" style="text-align: center; padding: 5px;">
|
||||
<a href="#" id="inventory-link" style="color: var(--win-blue); font-weight: bold;">[ View Inventory ]</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="spells-panel" class="group-box">
|
||||
<div class="group-box-label">Spells</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export class Entity {
|
||||
constructor(x, y, name, symbol, color) {
|
||||
if (name === 'Player') console.log("Entity Version 1.2 Loaded");
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.name = name;
|
||||
|
|
@ -26,7 +27,7 @@ export class Player extends Entity {
|
|||
};
|
||||
this.mana = 10;
|
||||
this.maxMana = 10;
|
||||
this.spells = ['Magic Arrow', 'Fireball'];
|
||||
this.spells = ['zappy laser'];
|
||||
this.level = 1;
|
||||
this.xp = 0;
|
||||
this.maxXp = 10;
|
||||
|
|
@ -75,6 +76,7 @@ export class Monster extends Entity {
|
|||
this.level = level;
|
||||
this.hp = level * 5;
|
||||
this.maxHp = this.hp;
|
||||
this.speed = 1.0; // Default: move every turn
|
||||
}
|
||||
|
||||
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 {
|
||||
constructor(x, y, name, shopName, inventory) {
|
||||
super(x, y, name, '$', '#ffd700');
|
||||
|
|
@ -89,3 +98,17 @@ export class Shopkeeper extends Entity {
|
|||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
565
src/Game.js
565
src/Game.js
|
|
@ -2,33 +2,47 @@ import { Map } from './Map.js';
|
|||
import { UI } from './UI.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';
|
||||
|
||||
export class Game {
|
||||
constructor() {
|
||||
console.log("Game Version 1.3 Loaded - Persistent Levels");
|
||||
this.canvas = document.getElementById('game-canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.ui = new UI(this.ctx, this); // Pass game instance to UI for callbacks
|
||||
this.map = new Map(50, 50);
|
||||
this.player = new Player(10, 10);
|
||||
this.monsters = [];
|
||||
this.items = [];
|
||||
this.gameOver = false;
|
||||
this.monsters = [];
|
||||
this.items = [];
|
||||
this.gameOver = false;
|
||||
this.depth = 0; // Start in Town
|
||||
this.maxDepth = 3;
|
||||
this.maxDepth = 10;
|
||||
this.deepestDepthReached = 0;
|
||||
this.dungeonLevels = {}; // Persistent storage for each depth
|
||||
|
||||
this.tileSize = 32;
|
||||
|
||||
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING'
|
||||
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY'
|
||||
this.currentShopkeeper = null;
|
||||
this.shopSelection = 0;
|
||||
this.shopMode = 'BUY'; // 'BUY' or 'SELL'
|
||||
this.inventorySelection = 0;
|
||||
this.targetingSpell = null;
|
||||
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
|
||||
window.addEventListener('keydown', (e) => this.handleInput(e));
|
||||
}
|
||||
|
|
@ -41,13 +55,16 @@ export class Game {
|
|||
|
||||
generateLevel(direction = 'down') {
|
||||
this.monsters = [];
|
||||
this.items = [];
|
||||
|
||||
if (this.dungeonLevels[this.depth]) {
|
||||
// Load existing level
|
||||
const levelData = this.dungeonLevels[this.depth];
|
||||
this.map = levelData.map;
|
||||
this.items = levelData.items;
|
||||
|
||||
// Reposition player
|
||||
if (this.depth === 0) {
|
||||
this.map.generateTown();
|
||||
// Spawn player in center (default) or at stairs if coming up
|
||||
if (direction === 'up') {
|
||||
// Coming up from dungeon, spawn at dungeon entrance
|
||||
this.player.x = 43;
|
||||
this.player.y = 8;
|
||||
} else {
|
||||
|
|
@ -55,16 +72,35 @@ export class Game {
|
|||
this.player.y = Math.floor(this.map.height / 2);
|
||||
}
|
||||
} else {
|
||||
this.map.generate();
|
||||
// Spawn Player
|
||||
if (this.map.rooms.length > 0) {
|
||||
if (direction === 'down') {
|
||||
// Start at first room (Stairs Up location)
|
||||
const startRoom = this.map.rooms[0];
|
||||
this.player.x = Math.floor(startRoom.x + startRoom.w / 2);
|
||||
this.player.y = Math.floor(startRoom.y + startRoom.h / 2);
|
||||
} else {
|
||||
// Start at last room (Stairs Down location)
|
||||
const endRoom = this.map.rooms[this.map.rooms.length - 1];
|
||||
this.player.x = Math.floor(endRoom.x + endRoom.w / 2);
|
||||
this.player.y = Math.floor(endRoom.y + endRoom.h / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
|
|
@ -72,10 +108,18 @@ export class Game {
|
|||
}
|
||||
}
|
||||
|
||||
this.spawnMonsters();
|
||||
this.spawnItems();
|
||||
this.spawnStairs();
|
||||
|
||||
// Save new level state
|
||||
this.dungeonLevels[this.depth] = {
|
||||
map: this.map,
|
||||
items: this.items
|
||||
};
|
||||
}
|
||||
|
||||
this.spawnMonsters(); // Always respawn monsters
|
||||
|
||||
this.render();
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
this.ui.updateInventory(this.player);
|
||||
|
|
@ -89,7 +133,14 @@ export class Game {
|
|||
return;
|
||||
}
|
||||
|
||||
// Save current level state
|
||||
this.dungeonLevels[this.depth] = {
|
||||
map: this.map,
|
||||
items: this.items
|
||||
};
|
||||
|
||||
this.depth++;
|
||||
this.deepestDepthReached = Math.max(this.deepestDepthReached, this.depth);
|
||||
this.ui.log(`You descend to level ${this.depth}...`);
|
||||
this.generateLevel('down');
|
||||
}
|
||||
|
|
@ -100,6 +151,12 @@ export class Game {
|
|||
return;
|
||||
}
|
||||
|
||||
// Save current level state
|
||||
this.dungeonLevels[this.depth] = {
|
||||
map: this.map,
|
||||
items: this.items
|
||||
};
|
||||
|
||||
this.depth--;
|
||||
if (this.depth === 0) {
|
||||
this.ui.log("You return to the Town of Oakhaven.");
|
||||
|
|
@ -162,11 +219,34 @@ export class Game {
|
|||
|
||||
// General Store (West)
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -182,20 +262,37 @@ export class Game {
|
|||
const type = Math.random();
|
||||
let monster;
|
||||
|
||||
// Difficulty scaling
|
||||
if (this.depth === 1) {
|
||||
// Difficulty scaling for 10 levels
|
||||
if (this.depth <= 2) {
|
||||
if (type < 0.7) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1);
|
||||
else monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
|
||||
} else if (this.depth === 2) {
|
||||
if (type < 0.4) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1);
|
||||
else if (type < 0.8) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
|
||||
else monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
|
||||
} else if (this.depth <= 4) {
|
||||
if (type < 0.3) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1);
|
||||
else if (type < 0.6) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
|
||||
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 {
|
||||
if (type < 0.3) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
|
||||
else if (type < 0.7) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
|
||||
else monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 4);
|
||||
if (type < 0.2) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
|
||||
else if (type < 0.4) monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 5);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -209,6 +306,7 @@ export class Game {
|
|||
const boss = new Monster(mx, my, "The Dungeon Lord", "D", "#ff00ff", 10);
|
||||
boss.hp = 100;
|
||||
boss.maxHp = 100;
|
||||
boss.speed = 0.6;
|
||||
this.monsters.push(boss);
|
||||
}
|
||||
}
|
||||
|
|
@ -228,21 +326,39 @@ export class Game {
|
|||
let item;
|
||||
|
||||
// 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 (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 });
|
||||
else if (type < 0.5) {
|
||||
if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
|
||||
else if (type < 0.45) {
|
||||
if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 });
|
||||
else if (tier === 2) item = new Item("Short Sword", "weapon", 2, { damage: 6 });
|
||||
else item = new Item("Long Sword", "weapon", 3, { damage: 8 });
|
||||
} else if (type < 0.7) {
|
||||
else if (tier === 3) item = new Item("Long Sword", "weapon", 3, { damage: 8 });
|
||||
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 });
|
||||
else if (tier === 2) item = new Item("Wooden Shield", "shield", 2, { defense: 2 });
|
||||
else item = new Item("Kite Shield", "shield", 3, { defense: 3 });
|
||||
} else {
|
||||
if (tier >= 3) item = new Item("Great Axe", "weapon", 4, { damage: 12 });
|
||||
else if (tier === 3) item = new Item("Kite Shield", "shield", 3, { defense: 4 });
|
||||
else item = new Item("Tower Shield", "shield", 4, { defense: 6 });
|
||||
} 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 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;
|
||||
|
|
@ -260,6 +376,11 @@ export class Game {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.gameState === 'INVENTORY') {
|
||||
this.handleInventoryInput(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gameState === 'TARGETING') {
|
||||
this.handleTargetingInput(e);
|
||||
return;
|
||||
|
|
@ -283,6 +404,7 @@ export class Game {
|
|||
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 'i': case 'I': this.toggleInventory(); handled = true; break;
|
||||
case 'Enter':
|
||||
case '>':
|
||||
if (this.map.tiles[this.player.y][this.player.x] === '>') {
|
||||
|
|
@ -304,8 +426,31 @@ export class Game {
|
|||
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;
|
||||
case '1':
|
||||
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) {
|
||||
|
|
@ -320,7 +465,7 @@ export class Game {
|
|||
|
||||
handleShopInput(e) {
|
||||
e.preventDefault();
|
||||
const inventory = this.currentShopkeeper.inventory;
|
||||
const inventory = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.player.inventory;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
|
|
@ -333,9 +478,17 @@ export class Game {
|
|||
this.shopSelection++;
|
||||
if (this.shopSelection >= inventory.length) this.shopSelection = 0;
|
||||
break;
|
||||
case 'Tab':
|
||||
this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY';
|
||||
this.shopSelection = 0;
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (this.shopMode === 'BUY') {
|
||||
this.buyItem(inventory[this.shopSelection]);
|
||||
} else {
|
||||
this.sellItem(this.shopSelection);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.closeShop();
|
||||
|
|
@ -348,6 +501,7 @@ export class Game {
|
|||
this.gameState = 'SHOP';
|
||||
this.currentShopkeeper = shopkeeper;
|
||||
this.shopSelection = 0;
|
||||
this.shopMode = 'BUY';
|
||||
this.ui.log(`You talk to ${shopkeeper.name}.`);
|
||||
this.render();
|
||||
}
|
||||
|
|
@ -358,7 +512,48 @@ export class Game {
|
|||
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) {
|
||||
if (!entry) return;
|
||||
if (this.player.gold >= entry.price) {
|
||||
this.player.gold -= entry.price;
|
||||
// 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) {
|
||||
const newX = this.player.x + dx;
|
||||
const newY = this.player.y + dy;
|
||||
|
|
@ -385,6 +618,43 @@ export class Game {
|
|||
if (targetMonster) {
|
||||
if (targetMonster instanceof Shopkeeper) {
|
||||
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 {
|
||||
this.attack(this.player, targetMonster);
|
||||
}
|
||||
|
|
@ -457,7 +727,7 @@ export class Game {
|
|||
let symbol = '*';
|
||||
let color = '#ffffff';
|
||||
|
||||
if (spellName === 'Magic Arrow') {
|
||||
if (spellName === 'zappy laser') {
|
||||
manaCost = 2;
|
||||
damage = Math.floor(this.player.stats.int / 2) + Math.floor(Math.random() * 4) + 1;
|
||||
symbol = '*';
|
||||
|
|
@ -486,13 +756,91 @@ export class Game {
|
|||
symbol: symbol,
|
||||
color: color,
|
||||
damage: damage,
|
||||
name: spellName
|
||||
name: spellName,
|
||||
owner: this.player
|
||||
};
|
||||
|
||||
this.projectiles.push(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) {
|
||||
const interval = setInterval(() => {
|
||||
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);
|
||||
if (monster) {
|
||||
if (monster && projectile.owner === this.player) {
|
||||
clearInterval(interval);
|
||||
this.removeProjectile(projectile);
|
||||
|
||||
// Shopkeepers are immune
|
||||
if (monster instanceof Shopkeeper) {
|
||||
// Shopkeepers and QuestGivers are immune
|
||||
if (monster instanceof Shopkeeper || monster instanceof QuestGiver || monster instanceof WanderingQuestGiver) {
|
||||
this.ui.log(`${projectile.name} hits ${monster.name} but does nothing.`);
|
||||
} else {
|
||||
// Deal damage
|
||||
|
|
@ -527,6 +875,15 @@ export class Game {
|
|||
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.y = newY;
|
||||
this.render();
|
||||
|
|
@ -551,6 +908,36 @@ export class Game {
|
|||
killMonster(monster) {
|
||||
this.ui.log(`${monster.name} dies!`);
|
||||
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);
|
||||
this.player.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() {
|
||||
const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y);
|
||||
if (itemIndex !== -1) {
|
||||
|
|
@ -597,6 +1010,16 @@ export class Game {
|
|||
// Remove potion
|
||||
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);
|
||||
|
|
@ -604,9 +1027,9 @@ export class Game {
|
|||
this.render(); // Re-render to show equipped status if we add that
|
||||
}
|
||||
|
||||
attack(attacker, defender) {
|
||||
// Shopkeepers are invincible/pacifist
|
||||
if (defender instanceof Shopkeeper) return;
|
||||
attack(attacker, defender, presetDamage = null) {
|
||||
// Shopkeepers and QuestGivers are invincible/pacifist
|
||||
if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver) return;
|
||||
|
||||
// Peasants are invincible
|
||||
if (defender.name === "Peasant") {
|
||||
|
|
@ -623,12 +1046,14 @@ export class Game {
|
|||
return;
|
||||
}
|
||||
|
||||
let damage = 0;
|
||||
let damage = presetDamage;
|
||||
if (damage === null) {
|
||||
if (attacker === this.player) {
|
||||
damage = this.player.getDamage() + Math.floor(Math.random() * 2);
|
||||
} else {
|
||||
damage = 1 + Math.floor(Math.random() * 2); // Monster damage
|
||||
}
|
||||
}
|
||||
|
||||
// Apply defense
|
||||
const defense = defender.getDefense ? defender.getDefense() : 0;
|
||||
|
|
@ -638,42 +1063,24 @@ export class Game {
|
|||
this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`);
|
||||
|
||||
if (defender.hp <= 0) {
|
||||
this.ui.log(`${defender.name} dies!`);
|
||||
if (defender === this.player) {
|
||||
this.ui.log("GAME OVER");
|
||||
this.gameOver = true;
|
||||
setTimeout(() => alert("Game Over! Refresh to restart."), 100);
|
||||
} else {
|
||||
// Remove monster
|
||||
this.monsters = this.monsters.filter(m => m !== defender);
|
||||
|
||||
// Gold Drop
|
||||
const gold = defender.level * (Math.floor(Math.random() * 5) + 1);
|
||||
this.player.gold += gold;
|
||||
this.ui.log(`You kill the ${defender.name}! It drops ${gold} gold.`);
|
||||
|
||||
// Boss Drop
|
||||
if (defender.name === "The Dungeon Lord") {
|
||||
const map = new Item("Ancient Map", "map", 1, {});
|
||||
map.x = defender.x;
|
||||
map.y = defender.y;
|
||||
this.items.push(map);
|
||||
this.ui.log("The Dungeon Lord drops an Ancient Map!");
|
||||
}
|
||||
|
||||
// Award XP
|
||||
if (this.player.gainXp(defender.level * 5)) {
|
||||
this.ui.log(`You reached level ${this.player.level}!`);
|
||||
}
|
||||
this.killMonster(defender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateMonsters() {
|
||||
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)
|
||||
if (Math.random() < 0.1) {
|
||||
const dx = Math.floor(Math.random() * 3) - 1;
|
||||
|
|
@ -688,12 +1095,22 @@ export class Game {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Simple chase logic
|
||||
// Simple chase or ranged logic
|
||||
const dx = this.player.x - monster.x;
|
||||
const dy = this.player.y - monster.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 8) { // Aggro range
|
||||
// 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 moveY = 0;
|
||||
|
||||
|
|
@ -724,7 +1141,11 @@ export class Game {
|
|||
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.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);
|
||||
|
|
|
|||
|
|
@ -14,5 +14,7 @@ export class Item {
|
|||
if (type === 'shield') this.symbol = ']';
|
||||
if (type === 'potion') this.symbol = '!';
|
||||
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'; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/Map.js
27
src/Map.js
|
|
@ -4,15 +4,20 @@ export class Map {
|
|||
this.height = height;
|
||||
this.tiles = [];
|
||||
this.rooms = [];
|
||||
this.permanentlyLit = []; // New grid for spell effects
|
||||
}
|
||||
|
||||
generate() {
|
||||
this.isTown = false;
|
||||
this.rooms = [];
|
||||
// Initialize with walls
|
||||
this.permanentlyLit = [];
|
||||
// Initialize with walls and dark
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
this.tiles[y] = [];
|
||||
this.permanentlyLit[y] = [];
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
this.tiles[y][x] = '#';
|
||||
this.permanentlyLit[y][x] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,12 +66,16 @@ export class Map {
|
|||
}
|
||||
|
||||
generateTown() {
|
||||
this.isTown = true;
|
||||
this.rooms = [];
|
||||
this.permanentlyLit = [];
|
||||
// Fill with grass/floor
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
this.tiles[y] = [];
|
||||
this.permanentlyLit[y] = [];
|
||||
for (let x = 0; x < this.width; 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
146
src/UI.js
146
src/UI.js
|
|
@ -1,11 +1,19 @@
|
|||
export class UI {
|
||||
constructor(ctx, game) {
|
||||
console.log("UI Version 1.1 Loaded");
|
||||
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.inventoryLink = document.getElementById('inventory-link');
|
||||
|
||||
if (this.inventoryLink) {
|
||||
this.inventoryLink.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.game.toggleInventory();
|
||||
};
|
||||
}
|
||||
|
||||
this.popup = null;
|
||||
this.popupTimeout = null;
|
||||
}
|
||||
|
|
@ -40,7 +48,17 @@ export class UI {
|
|||
|
||||
if (mapX >= 0 && mapX < map.width && mapY >= 0 && mapY < map.height) {
|
||||
const tile = map.tiles[mapY][mapX];
|
||||
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
|
||||
if (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 screenY = (item.y - startY) * tileSize;
|
||||
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||
|
|
@ -59,6 +81,10 @@ export class UI {
|
|||
|
||||
// Draw 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 screenY = (monster.y - startY) * tileSize;
|
||||
// Only draw if visible
|
||||
|
|
@ -71,6 +97,10 @@ export class UI {
|
|||
// Draw Projectiles
|
||||
if (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 screenY = (proj.y - startY) * tileSize;
|
||||
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||
|
|
@ -99,27 +129,78 @@ 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) {
|
||||
this.inventoryList.innerHTML = '';
|
||||
player.inventory.forEach((item, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = item.name;
|
||||
if (player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item) {
|
||||
li.textContent += " (E)";
|
||||
// Now handled by canvas-based inventory box
|
||||
if (this.inventoryLink) {
|
||||
this.inventoryLink.textContent = `[ View Inventory (${player.inventory.length}) ]`;
|
||||
}
|
||||
li.style.cursor = 'pointer';
|
||||
li.onclick = () => {
|
||||
this.game.useItem(index);
|
||||
};
|
||||
this.inventoryList.appendChild(li);
|
||||
}
|
||||
|
||||
drawInventory(player, selectedIndex) {
|
||||
const ctx = this.ctx;
|
||||
const width = 500;
|
||||
const height = 400;
|
||||
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('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) {
|
||||
if (!this.spellsList) return;
|
||||
this.spellsList.innerHTML = '';
|
||||
|
|
@ -220,10 +301,10 @@ export class UI {
|
|||
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
|
||||
}
|
||||
|
||||
drawShop(shopkeeper, player, selectedIndex) {
|
||||
drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') {
|
||||
const ctx = this.ctx;
|
||||
const width = 400;
|
||||
const height = 300;
|
||||
const width = 500;
|
||||
const height = 400;
|
||||
const x = (ctx.canvas.width - width) / 2;
|
||||
const y = (ctx.canvas.height - height) / 2;
|
||||
|
||||
|
|
@ -238,17 +319,35 @@ export class UI {
|
|||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '20px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(shopkeeper.shopName, x + width / 2, y + 30);
|
||||
ctx.fillText(`${shopkeeper.shopName} - ${mode}`, 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 list = mode === 'BUY' ? shopkeeper.inventory : player.inventory;
|
||||
|
||||
if (list.length === 0) {
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(mode === 'BUY' ? 'Sold Out' : 'Nothing to sell', x + width / 2, itemY + 20);
|
||||
} else {
|
||||
list.forEach((entry, index) => {
|
||||
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);
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
ctx.fillStyle = '#333333';
|
||||
|
|
@ -266,16 +365,17 @@ export class UI {
|
|||
|
||||
itemY += 30;
|
||||
});
|
||||
}
|
||||
|
||||
// Player Gold
|
||||
ctx.fillStyle = '#ffd700';
|
||||
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
|
||||
ctx.fillStyle = '#888888';
|
||||
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
|
||||
ctx.textAlign = 'start';
|
||||
|
|
|
|||
Loading…
Reference in New Issue