Added more features and art assets
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 848 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 856 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -20,8 +20,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="window-body">
|
<div class="window-body">
|
||||||
<div id="main-layout">
|
<div id="main-layout">
|
||||||
<div id="viewport-container" class="inset-border">
|
<div id="viewport-container">
|
||||||
<canvas id="game-canvas" width="640" height="480"></canvas>
|
<canvas id="game-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<div id="stats-panel" class="group-box">
|
<div id="stats-panel" class="group-box">
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
<br>
|
<br>
|
||||||
<div>HP: <span id="stat-hp">10</span>/<span id="stat-max-hp">10</span></div>
|
<div>HP: <span id="stat-hp">10</span>/<span id="stat-max-hp">10</span></div>
|
||||||
<div>Mana: <span id="stat-mana">10</span>/<span id="stat-max-mana">10</span></div>
|
<div>Mana: <span id="stat-mana">10</span>/<span id="stat-max-mana">10</span></div>
|
||||||
|
<div id="stat-status" style="color: #00ff00; font-weight: bold; height: 1.2em;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="inventory-panel" class="group-box">
|
<div id="inventory-panel" class="group-box">
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export class Player extends Entity {
|
||||||
this.xp = 0;
|
this.xp = 0;
|
||||||
this.maxXp = 10;
|
this.maxXp = 10;
|
||||||
this.gold = 0;
|
this.gold = 0;
|
||||||
|
this.poisoned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
gainXp(amount) {
|
gainXp(amount) {
|
||||||
|
|
@ -55,7 +56,7 @@ export class Player extends Entity {
|
||||||
let dmg = Math.floor(this.stats.str / 5); // Base damage
|
let dmg = Math.floor(this.stats.str / 5); // Base damage
|
||||||
if (dmg < 1) dmg = 1;
|
if (dmg < 1) dmg = 1;
|
||||||
if (this.equipment.weapon) {
|
if (this.equipment.weapon) {
|
||||||
dmg += this.equipment.weapon.stats.damage || 0;
|
dmg += (this.equipment.weapon.stats.damage || 0) + (this.equipment.weapon.modifier || 0);
|
||||||
}
|
}
|
||||||
return dmg;
|
return dmg;
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +64,7 @@ export class Player extends Entity {
|
||||||
getDefense() {
|
getDefense() {
|
||||||
let def = 0;
|
let def = 0;
|
||||||
if (this.equipment.shield) {
|
if (this.equipment.shield) {
|
||||||
def += this.equipment.shield.stats.defense || 0;
|
def += (this.equipment.shield.stats.defense || 0) + (this.equipment.shield.modifier || 0);
|
||||||
}
|
}
|
||||||
// Could add armor here later
|
// Could add armor here later
|
||||||
return def;
|
return def;
|
||||||
|
|
@ -91,6 +92,13 @@ export class Archer extends Monster {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Cobra extends Monster {
|
||||||
|
constructor(x, y, name, symbol, color, level) {
|
||||||
|
super(x, y, name, symbol, color, level);
|
||||||
|
this.poisonChance = 0.3; // 30% chance to poison on hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -112,3 +120,53 @@ export class WanderingQuestGiver extends Entity {
|
||||||
this.quest = null; // { target: string, required: number, current: number, completed: false }
|
this.quest = null; // { target: string, required: number, current: number, completed: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Farmer extends Entity {
|
||||||
|
constructor(x, y, name) {
|
||||||
|
super(x, y, name, 'f', '#ffa500'); // Orange 'f'
|
||||||
|
this.quotes = [
|
||||||
|
"You're growing like a weed!",
|
||||||
|
"Potatoes! Boil 'em, mash 'em, stick 'em in a stew!",
|
||||||
|
"The Dungeon Lord steals our crops to feed his monsters!",
|
||||||
|
"That Dungeon Lord is nothing but trouble!",
|
||||||
|
"You look undernourished, have a carrot!"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomQuote() {
|
||||||
|
return this.quotes[Math.floor(Math.random() * this.quotes.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Priest extends Entity {
|
||||||
|
constructor(x, y, name) {
|
||||||
|
super(x, y, name, 'P', '#ffffff'); // White 'P'
|
||||||
|
this.quotes = [
|
||||||
|
"The sun's light blesses our crops.",
|
||||||
|
"I remember when you were just a baby, now you're a hero!",
|
||||||
|
"Come back if you get injured.",
|
||||||
|
"Blessings upon you.",
|
||||||
|
"Don't forget to say your prayers!"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomQuote() {
|
||||||
|
return this.quotes[Math.floor(Math.random() * this.quotes.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Gravekeeper extends Entity {
|
||||||
|
constructor(x, y, name) {
|
||||||
|
super(x, y, name, 'G', '#808080'); // Gray 'G'
|
||||||
|
this.quotes = [
|
||||||
|
"Guarding this place is a grave responsibility.",
|
||||||
|
"Hope you don't end up here!",
|
||||||
|
"Got this shovel from a lass named Rosella.",
|
||||||
|
"I won't let the Dungeon Lord's monsters disturb the dead!"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomQuote() {
|
||||||
|
return this.quotes[Math.floor(Math.random() * this.quotes.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
584
src/Game.js
|
|
@ -2,14 +2,24 @@ 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, QuestGiver, WanderingQuestGiver, Archer } from './Entity.js';
|
import { Monster, Shopkeeper, QuestGiver, WanderingQuestGiver, Archer, Cobra, Farmer, Priest, Gravekeeper } 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");
|
console.log("Game Version 1.4 Loaded - Asset Support");
|
||||||
this.canvas = document.getElementById('game-canvas');
|
this.canvas = document.getElementById('game-canvas');
|
||||||
this.ctx = this.canvas.getContext('2d');
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Handle resizing
|
||||||
|
this.resizeCanvas();
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
this.resizeCanvas();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.assets = {};
|
||||||
|
this.loadAssets();
|
||||||
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.player = new Player(10, 10);
|
this.player = new Player(10, 10);
|
||||||
this.monsters = [];
|
this.monsters = [];
|
||||||
|
|
@ -21,8 +31,33 @@ export class Game {
|
||||||
this.dungeonLevels = {}; // Persistent storage for each depth
|
this.dungeonLevels = {}; // Persistent storage for each depth
|
||||||
|
|
||||||
this.tileSize = 32;
|
this.tileSize = 32;
|
||||||
|
this.mapWidth = 80;
|
||||||
|
this.mapHeight = 80;
|
||||||
|
|
||||||
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY'
|
this.gameState = 'INTRO'; // 'INTRO', 'PLAY', 'SHOP', 'TARGETING', 'INVENTORY', 'OVERVIEW'
|
||||||
|
this.storyText = [
|
||||||
|
"Once upon a time the people of Oakhaven found a baby left",
|
||||||
|
"on the temple steps--you! The villagers raised you as",
|
||||||
|
"their own, always wondering what mysterious traveler had",
|
||||||
|
"blessed them with a child. You grew up strong and brave,",
|
||||||
|
"working hard in the fields and protecting the town from",
|
||||||
|
"wolf packs and bandits. After one particularly tough",
|
||||||
|
"fight, your friends even started calling you Hero!",
|
||||||
|
"",
|
||||||
|
"Recently a solar eclipse darkened the sky. As the",
|
||||||
|
"villagers stood watching this cosmic event, a forgotten",
|
||||||
|
"dry well in the corner of town burst open and monsters",
|
||||||
|
"swarmed out! They declared that Oakhaven was now ruled",
|
||||||
|
"by the Dungeon Lord, who would be taking whatever he",
|
||||||
|
"wanted from the fields and the shops! Under this",
|
||||||
|
"oppression food is becoming scarce and the situation",
|
||||||
|
"will soon be desperate. Of all villagers, you have the",
|
||||||
|
"best chance to defeat the Dungeon Lord and free your home!",
|
||||||
|
"",
|
||||||
|
"Take up arms, and become the Hero you were destined to be!",
|
||||||
|
"",
|
||||||
|
"[ Click anywhere to start your adventure ]"
|
||||||
|
];
|
||||||
this.currentShopkeeper = null;
|
this.currentShopkeeper = null;
|
||||||
this.shopSelection = 0;
|
this.shopSelection = 0;
|
||||||
this.shopMode = 'BUY'; // 'BUY' or 'SELL'
|
this.shopMode = 'BUY'; // 'BUY' or 'SELL'
|
||||||
|
|
@ -45,6 +80,90 @@ export class Game {
|
||||||
|
|
||||||
// Bind input
|
// Bind input
|
||||||
window.addEventListener('keydown', (e) => this.handleInput(e));
|
window.addEventListener('keydown', (e) => this.handleInput(e));
|
||||||
|
this.canvas.addEventListener('mousedown', (e) => this.handleCanvasClick(e));
|
||||||
|
|
||||||
|
// Start real-time timers
|
||||||
|
this.startPoisonTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAssets() {
|
||||||
|
const assetNames = [
|
||||||
|
'archer', 'axe', 'buckler', 'cobra', 'dagger', 'dragon',
|
||||||
|
'dungeon lord', 'fountain', 'hero', 'house questgiver',
|
||||||
|
'kite shield', 'kobold', 'mysterious figure', 'ogre',
|
||||||
|
'orc', 'peasant1', 'peasant2', 'questgiver outside',
|
||||||
|
'rat', 'skeleton archer', 'spellbook', 'sword',
|
||||||
|
'wooden shield', 'zappy laser'
|
||||||
|
];
|
||||||
|
|
||||||
|
assetNames.forEach(name => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = `assets/${name}.png`;
|
||||||
|
img.onload = () => {
|
||||||
|
this.assets[name] = img;
|
||||||
|
this.render(); // Re-render when an asset loads
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas() {
|
||||||
|
const container = document.getElementById('viewport-container');
|
||||||
|
if (container) {
|
||||||
|
this.canvas.width = container.clientWidth;
|
||||||
|
this.canvas.height = container.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startPoisonTimer() {
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.player && this.player.poisoned && !this.gameOver) {
|
||||||
|
const damage = Math.max(1, Math.floor(this.player.maxHp * 0.05)); // 5% of max HP
|
||||||
|
this.player.hp -= damage;
|
||||||
|
this.ui.log(`The poison burns... You lose ${damage} HP.`);
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
|
||||||
|
if (this.player.hp <= 0) {
|
||||||
|
this.ui.log("You have succumbed to the poison!");
|
||||||
|
this.ui.log("GAME OVER");
|
||||||
|
this.gameOver = true;
|
||||||
|
setTimeout(() => alert("Game Over! Refresh to restart."), 100);
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}, 30000); // Every 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
updateExplored() {
|
||||||
|
if (!this.map || !this.map.explored) return;
|
||||||
|
|
||||||
|
const radius = 3.5;
|
||||||
|
const startX = Math.max(0, Math.floor(this.player.x - radius));
|
||||||
|
const endX = Math.min(this.map.width - 1, Math.ceil(this.player.x + radius));
|
||||||
|
const startY = Math.max(0, Math.floor(this.player.y - radius));
|
||||||
|
const endY = Math.min(this.map.height - 1, Math.ceil(this.player.y + radius));
|
||||||
|
|
||||||
|
for (let y = startY; y <= endY; y++) {
|
||||||
|
for (let x = startX; x <= endX; x++) {
|
||||||
|
const dist = Math.sqrt(Math.pow(this.player.x - x, 2) + Math.pow(this.player.y - y, 2));
|
||||||
|
if (dist <= radius) {
|
||||||
|
// Check LOS for exploration too, so we don't explore through walls
|
||||||
|
if (this.map.hasLineOfSight(this.player.x, this.player.y, x, y)) {
|
||||||
|
this.map.explored[y][x] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also explore anything permanently lit
|
||||||
|
if (this.map.permanentlyLit) {
|
||||||
|
for (let y = 0; y < this.map.height; y++) {
|
||||||
|
for (let x = 0; x < this.map.width; x++) {
|
||||||
|
if (this.map.permanentlyLit[y][x]) {
|
||||||
|
this.map.explored[y][x] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
@ -65,8 +184,8 @@ export class Game {
|
||||||
// Reposition player
|
// Reposition player
|
||||||
if (this.depth === 0) {
|
if (this.depth === 0) {
|
||||||
if (direction === 'up') {
|
if (direction === 'up') {
|
||||||
this.player.x = 43;
|
this.player.x = 63;
|
||||||
this.player.y = 8;
|
this.player.y = 13;
|
||||||
} else {
|
} else {
|
||||||
this.player.x = Math.floor(this.map.width / 2);
|
this.player.x = Math.floor(this.map.width / 2);
|
||||||
this.player.y = Math.floor(this.map.height / 2);
|
this.player.y = Math.floor(this.map.height / 2);
|
||||||
|
|
@ -87,7 +206,7 @@ export class Game {
|
||||||
} else {
|
} else {
|
||||||
// Generate NEW level
|
// Generate NEW level
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.map = new Map(50, 50);
|
this.map = new Map(this.mapWidth, this.mapHeight);
|
||||||
|
|
||||||
if (this.depth === 0) {
|
if (this.depth === 0) {
|
||||||
this.map.generateTown();
|
this.map.generateTown();
|
||||||
|
|
@ -119,6 +238,7 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spawnMonsters(); // Always respawn monsters
|
this.spawnMonsters(); // Always respawn monsters
|
||||||
|
this.updateExplored();
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.ui.updateStats(this.player, this.depth);
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
|
@ -169,8 +289,8 @@ export class Game {
|
||||||
spawnStairs() {
|
spawnStairs() {
|
||||||
if (this.depth === 0) {
|
if (this.depth === 0) {
|
||||||
// Town: Stairs Down in the Dungeon Entrance building
|
// Town: Stairs Down in the Dungeon Entrance building
|
||||||
const sx = 43; // Center X
|
const sx = 63; // Center X
|
||||||
const sy = 8; // Center Y
|
const sy = 13; // Center Y
|
||||||
this.map.tiles[sy][sx] = '>';
|
this.map.tiles[sy][sx] = '>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -214,24 +334,29 @@ export class Game {
|
||||||
{ item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 },
|
{ item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 },
|
||||||
{ item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 }
|
{ item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 }
|
||||||
];
|
];
|
||||||
const weaponSmith = new Shopkeeper(41, 18, "Smith", "Weapon Shop", weaponShopInventory);
|
weaponShopInventory.forEach(entry => entry.item.identified = true);
|
||||||
|
const weaponSmith = new Shopkeeper(59, 33, "Smith", "Weapon Shop", weaponShopInventory);
|
||||||
this.monsters.push(weaponSmith);
|
this.monsters.push(weaponSmith);
|
||||||
|
|
||||||
// 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("Antidote", "antidote", 1, {}), price: 30 },
|
||||||
{ item: new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" }), price: 300 },
|
{ 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: Light", "spellbook", 1, { spell: "Light" }), price: 200 },
|
||||||
{ item: new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }), price: 400 },
|
{ item: new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }), price: 400 },
|
||||||
{ item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 }
|
{ item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 },
|
||||||
|
{ item: new Item("Spellbook: Detect Traps", "spellbook", 1, { spell: "Detect Traps" }), price: 150 },
|
||||||
|
{ item: new Item("Spellbook: Cure Poison", "spellbook", 1, { spell: "Cure Poison" }), price: 250 }
|
||||||
];
|
];
|
||||||
const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory);
|
generalStoreInventory.forEach(entry => entry.item.identified = true);
|
||||||
|
const merchant = new Shopkeeper(19, 33, "Merchant", "General Store", generalStoreInventory);
|
||||||
this.monsters.push(merchant);
|
this.monsters.push(merchant);
|
||||||
|
|
||||||
// Spawn Quest Givers in Houses
|
// 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 q1 = new QuestGiver(23, 57, "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 q2 = new QuestGiver(40, 57, "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!");
|
const q3 = new QuestGiver(57, 57, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!");
|
||||||
this.monsters.push(q1, q2, q3);
|
this.monsters.push(q1, q2, q3);
|
||||||
|
|
||||||
// Spawn Wandering Quest Givers
|
// Spawn Wandering Quest Givers
|
||||||
|
|
@ -247,6 +372,16 @@ export class Game {
|
||||||
this.monsters.push(wqg);
|
this.monsters.push(wqg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn Farmers near fields
|
||||||
|
this.monsters.push(new Farmer(21, 19, "Farmer Giles"));
|
||||||
|
this.monsters.push(new Farmer(54, 48, "Farmer Maggot"));
|
||||||
|
|
||||||
|
// Spawn Priest in Temple
|
||||||
|
this.monsters.push(new Priest(40, 11, "Father Sun"));
|
||||||
|
|
||||||
|
// Spawn Gravekeeper in Graveyard
|
||||||
|
this.monsters.push(new Gravekeeper(11, 9, "Mort"));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,7 +408,8 @@ export class Game {
|
||||||
else monster = new Archer(mx, my, "Skeleton Archer", "s", "#eeeeee", 3);
|
else monster = new Archer(mx, my, "Skeleton Archer", "s", "#eeeeee", 3);
|
||||||
} else if (this.depth <= 7) {
|
} else if (this.depth <= 7) {
|
||||||
if (type < 0.2) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
|
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.4) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
|
||||||
|
else if (type < 0.6) monster = new Cobra(mx, my, "Cobra", "c", "#ffff00", 4);
|
||||||
else if (type < 0.8) monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 5);
|
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 monster = new Archer(mx, my, "Elite Archer", "S", "#ffffff", 5);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -287,6 +423,7 @@ export class Game {
|
||||||
// Set Monster Speeds
|
// Set Monster Speeds
|
||||||
if (monster.name === "Rat") monster.speed = 1.0;
|
if (monster.name === "Rat") monster.speed = 1.0;
|
||||||
else if (monster.name === "Kobold") monster.speed = 1.0;
|
else if (monster.name === "Kobold") monster.speed = 1.0;
|
||||||
|
else if (monster.name === "Cobra") monster.speed = 0.9;
|
||||||
else if (monster.name === "Orc") monster.speed = 0.8;
|
else if (monster.name === "Orc") monster.speed = 0.8;
|
||||||
else if (monster instanceof Archer) monster.speed = 0.7;
|
else if (monster instanceof Archer) monster.speed = 0.7;
|
||||||
else if (monster.name === "Ogre") monster.speed = 0.5;
|
else if (monster.name === "Ogre") monster.speed = 0.5;
|
||||||
|
|
@ -330,6 +467,7 @@ export class Game {
|
||||||
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 + this.depth * 2 });
|
if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
|
||||||
|
else if (type < 0.35) item = new Item("Antidote", "antidote", 1, {});
|
||||||
else if (type < 0.45) {
|
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 });
|
||||||
|
|
@ -348,14 +486,18 @@ export class Game {
|
||||||
} else if (type < 0.8) {
|
} else if (type < 0.8) {
|
||||||
// Spellbooks
|
// Spellbooks
|
||||||
const spellRoll = Math.random();
|
const spellRoll = Math.random();
|
||||||
if (spellRoll < 0.25) {
|
if (spellRoll < 0.16) {
|
||||||
item = new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" });
|
item = new Item("Spellbook: Fireball", "spellbook", 1, { spell: "Fireball" });
|
||||||
} else if (spellRoll < 0.5) {
|
} else if (spellRoll < 0.32) {
|
||||||
item = new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" });
|
item = new Item("Spellbook: Light", "spellbook", 1, { spell: "Light" });
|
||||||
} else if (spellRoll < 0.75) {
|
} else if (spellRoll < 0.48) {
|
||||||
item = new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" });
|
item = new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" });
|
||||||
} else {
|
} else if (spellRoll < 0.64) {
|
||||||
item = new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" });
|
item = new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" });
|
||||||
|
} else if (spellRoll < 0.8) {
|
||||||
|
item = new Item("Spellbook: Detect Traps", "spellbook", 1, { spell: "Detect Traps" });
|
||||||
|
} else {
|
||||||
|
item = new Item("Spellbook: Cure Poison", "spellbook", 1, { spell: "Cure Poison" });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
|
item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
|
||||||
|
|
@ -363,6 +505,20 @@ export class Game {
|
||||||
|
|
||||||
item.x = ix;
|
item.x = ix;
|
||||||
item.y = iy;
|
item.y = iy;
|
||||||
|
|
||||||
|
// Roll for Enchantment / Curse (20% enchanted, 20% cursed)
|
||||||
|
if (item.type === 'weapon' || item.type === 'shield' || item.type === 'armor') {
|
||||||
|
const roll = Math.random();
|
||||||
|
if (roll < 0.2) {
|
||||||
|
// Enchanted
|
||||||
|
item.modifier = Math.floor(Math.random() * 3) + 1;
|
||||||
|
} else if (roll < 0.4) {
|
||||||
|
// Cursed
|
||||||
|
item.modifier = -(Math.floor(Math.random() * 2) + 1);
|
||||||
|
item.isCursed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +527,12 @@ export class Game {
|
||||||
handleInput(e) {
|
handleInput(e) {
|
||||||
if (this.gameOver) return;
|
if (this.gameOver) return;
|
||||||
|
|
||||||
|
if (this.gameState === 'INTRO') {
|
||||||
|
this.gameState = 'PLAY';
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.gameState === 'SHOP') {
|
if (this.gameState === 'SHOP') {
|
||||||
this.handleShopInput(e);
|
this.handleShopInput(e);
|
||||||
return;
|
return;
|
||||||
|
|
@ -381,6 +543,14 @@ export class Game {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.gameState === 'OVERVIEW') {
|
||||||
|
if (e.key === 'm' || e.key === 'M' || e.key === 'Escape') {
|
||||||
|
this.gameState = 'PLAY';
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.gameState === 'TARGETING') {
|
if (this.gameState === 'TARGETING') {
|
||||||
this.handleTargetingInput(e);
|
this.handleTargetingInput(e);
|
||||||
return;
|
return;
|
||||||
|
|
@ -405,6 +575,11 @@ export class Game {
|
||||||
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 'i': case 'I': this.toggleInventory(); handled = true; break;
|
||||||
|
case 'm': case 'M':
|
||||||
|
this.gameState = 'OVERVIEW';
|
||||||
|
this.render();
|
||||||
|
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] === '>') {
|
||||||
|
|
@ -451,6 +626,16 @@ export class Game {
|
||||||
else this.ui.log("You don't know that spell!");
|
else this.ui.log("You don't know that spell!");
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
|
case '6':
|
||||||
|
if (this.player.spells.includes('Detect Traps')) this.castDetectTrapsSpell();
|
||||||
|
else this.ui.log("You don't know that spell!");
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
case '7':
|
||||||
|
if (this.player.spells.includes('Cure Poison')) this.castCurePoisonSpell();
|
||||||
|
else this.ui.log("You don't know that spell!");
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
|
|
@ -465,18 +650,18 @@ export class Game {
|
||||||
|
|
||||||
handleShopInput(e) {
|
handleShopInput(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const inventory = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.player.inventory;
|
const list = this.shopMode === 'BUY' ? this.currentShopkeeper.inventory : this.getGroupedInventory();
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'NumPad8':
|
case 'NumPad8':
|
||||||
this.shopSelection--;
|
this.shopSelection--;
|
||||||
if (this.shopSelection < 0) this.shopSelection = inventory.length - 1;
|
if (this.shopSelection < 0) this.shopSelection = list.length - 1;
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'NumPad2':
|
case 'NumPad2':
|
||||||
this.shopSelection++;
|
this.shopSelection++;
|
||||||
if (this.shopSelection >= inventory.length) this.shopSelection = 0;
|
if (this.shopSelection >= list.length) this.shopSelection = 0;
|
||||||
break;
|
break;
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY';
|
this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY';
|
||||||
|
|
@ -485,9 +670,12 @@ export class Game {
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
if (this.shopMode === 'BUY') {
|
if (this.shopMode === 'BUY') {
|
||||||
this.buyItem(inventory[this.shopSelection]);
|
this.buyItem(list[this.shopSelection]);
|
||||||
} else {
|
} else {
|
||||||
this.sellItem(this.shopSelection);
|
if (list[this.shopSelection]) {
|
||||||
|
const actualIndex = list[this.shopSelection].indices[0];
|
||||||
|
this.sellItem(actualIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
|
@ -522,25 +710,141 @@ export class Game {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGroupedInventory() {
|
||||||
|
const groups = [];
|
||||||
|
this.player.inventory.forEach((item, index) => {
|
||||||
|
const group = groups.find(g => g.name === item.name && g.type === item.type && JSON.stringify(g.stats) === JSON.stringify(item.stats));
|
||||||
|
if (group) {
|
||||||
|
group.count++;
|
||||||
|
group.indices.push(index);
|
||||||
|
} else {
|
||||||
|
groups.push({
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
stats: item.stats,
|
||||||
|
item: item, // Representative item
|
||||||
|
count: 1,
|
||||||
|
indices: [index]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCanvasClick(e) {
|
||||||
|
if (this.gameOver) return;
|
||||||
|
|
||||||
|
if (this.gameState === 'INTRO') {
|
||||||
|
this.gameState = 'PLAY';
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relative mouse coordinates on canvas
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
const mouseX = e.clientX - rect.left;
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
if (this.gameState === 'TARGETING') {
|
||||||
|
// Calculate which tile was clicked
|
||||||
|
const viewWidth = Math.ceil(this.canvas.width / this.tileSize);
|
||||||
|
const viewHeight = Math.ceil(this.canvas.height / this.tileSize);
|
||||||
|
const startX = this.player.x - Math.floor(viewWidth / 2);
|
||||||
|
const startY = this.player.y - Math.floor(viewHeight / 2);
|
||||||
|
|
||||||
|
const clickedMapX = Math.floor(mouseX / this.tileSize) + startX;
|
||||||
|
const clickedMapY = Math.floor(mouseY / this.tileSize) + startY;
|
||||||
|
|
||||||
|
// Calculate direction from player to clicked tile
|
||||||
|
const dx = Math.sign(clickedMapX - this.player.x);
|
||||||
|
const dy = Math.sign(clickedMapY - this.player.y);
|
||||||
|
|
||||||
|
if (dx !== 0 || dy !== 0) {
|
||||||
|
this.castSpell(this.targetingSpell, dx, dy);
|
||||||
|
this.gameState = 'PLAY';
|
||||||
|
this.targetingSpell = null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
} else if (this.gameState === 'PLAY') {
|
||||||
|
// Click player to toggle inventory
|
||||||
|
const viewWidth = Math.ceil(this.canvas.width / this.tileSize);
|
||||||
|
const viewHeight = Math.ceil(this.canvas.height / this.tileSize);
|
||||||
|
const startX = this.player.x - Math.floor(viewWidth / 2);
|
||||||
|
const startY = this.player.y - Math.floor(viewHeight / 2);
|
||||||
|
|
||||||
|
const clickedMapX = Math.floor(mouseX / this.tileSize) + startX;
|
||||||
|
const clickedMapY = Math.floor(mouseY / this.tileSize) + startY;
|
||||||
|
|
||||||
|
if (clickedMapX === this.player.x && clickedMapY === this.player.y) {
|
||||||
|
this.toggleInventory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click signs
|
||||||
|
if (this.map.signs) {
|
||||||
|
const sign = this.map.signs.find(s => s.x === clickedMapX && s.y === clickedMapY);
|
||||||
|
if (sign) {
|
||||||
|
this.ui.showPopup(sign.text, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.gameState === 'INVENTORY' || this.gameState === 'SHOP') {
|
||||||
|
// Check if clicking inside the box items
|
||||||
|
const width = 500;
|
||||||
|
const height = this.gameState === 'INVENTORY' ? 400 : 400; // Match UI.js dimensions
|
||||||
|
const bx = (this.canvas.width - width) / 2;
|
||||||
|
const by = (this.canvas.height - height) / 2;
|
||||||
|
|
||||||
|
if (mouseX >= bx + 20 && mouseX <= bx + width - 20) {
|
||||||
|
const isSelling = this.gameState === 'SHOP' && this.shopMode === 'SELL';
|
||||||
|
const isInventory = this.gameState === 'INVENTORY';
|
||||||
|
|
||||||
|
let list;
|
||||||
|
if (isInventory || isSelling) {
|
||||||
|
list = this.getGroupedInventory();
|
||||||
|
} else {
|
||||||
|
list = this.currentShopkeeper.inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startY = by + 70;
|
||||||
|
const clickedIndex = Math.floor((mouseY - startY + 5) / 30);
|
||||||
|
|
||||||
|
if (clickedIndex >= 0 && clickedIndex < list.length) {
|
||||||
|
if (isInventory) {
|
||||||
|
const actualIndex = list[clickedIndex].indices[0];
|
||||||
|
this.useItem(actualIndex);
|
||||||
|
} else {
|
||||||
|
if (this.shopMode === 'BUY') {
|
||||||
|
this.buyItem(list[clickedIndex]);
|
||||||
|
} else {
|
||||||
|
const actualIndex = list[clickedIndex].indices[0];
|
||||||
|
this.sellItem(actualIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleInventoryInput(e) {
|
handleInventoryInput(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const inventory = this.player.inventory;
|
const groupedItems = this.getGroupedInventory();
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'NumPad8':
|
case 'NumPad8':
|
||||||
this.inventorySelection--;
|
this.inventorySelection--;
|
||||||
if (this.inventorySelection < 0) this.inventorySelection = inventory.length - 1;
|
if (this.inventorySelection < 0) this.inventorySelection = groupedItems.length - 1;
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'NumPad2':
|
case 'NumPad2':
|
||||||
this.inventorySelection++;
|
this.inventorySelection++;
|
||||||
if (this.inventorySelection >= inventory.length) this.inventorySelection = 0;
|
if (this.inventorySelection >= groupedItems.length) this.inventorySelection = 0;
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
if (inventory[this.inventorySelection]) {
|
if (groupedItems[this.inventorySelection]) {
|
||||||
this.useItem(this.inventorySelection);
|
const actualIndex = groupedItems[this.inventorySelection].indices[0];
|
||||||
|
this.useItem(actualIndex);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
|
@ -561,9 +865,10 @@ export class Game {
|
||||||
// Copy visual props
|
// Copy visual props
|
||||||
newItem.symbol = entry.item.symbol;
|
newItem.symbol = entry.item.symbol;
|
||||||
newItem.color = entry.item.color;
|
newItem.color = entry.item.color;
|
||||||
|
newItem.identified = true; // Shop items are identified
|
||||||
|
|
||||||
this.player.inventory.push(newItem);
|
this.player.inventory.push(newItem);
|
||||||
this.ui.log(`You bought ${entry.item.name} for ${entry.price} gold.`);
|
this.ui.log(`You bought ${newItem.getDisplayName()} for ${entry.price} gold.`);
|
||||||
this.ui.updateInventory(this.player);
|
this.ui.updateInventory(this.player);
|
||||||
this.ui.updateStats(this.player, this.depth);
|
this.ui.updateStats(this.player, this.depth);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -581,8 +886,15 @@ export class Game {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cannot sell cursed items while equipped
|
||||||
|
if (item.isCursed && item.identified) {
|
||||||
|
if (this.player.equipment.weapon === item || this.player.equipment.shield === item || this.player.equipment.armor === item) {
|
||||||
|
this.ui.log("You cannot sell a cursed item you are currently wearing!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Base price calculation (1/4 of buy price)
|
// 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
|
let baseValue = 40; // Default
|
||||||
if (item.type === 'weapon') baseValue = 100 * item.level;
|
if (item.type === 'weapon') baseValue = 100 * item.level;
|
||||||
if (item.type === 'shield') baseValue = 40 * item.level;
|
if (item.type === 'shield') baseValue = 40 * item.level;
|
||||||
|
|
@ -598,7 +910,7 @@ export class Game {
|
||||||
this.player.gold += sellPrice;
|
this.player.gold += sellPrice;
|
||||||
this.player.inventory.splice(index, 1);
|
this.player.inventory.splice(index, 1);
|
||||||
|
|
||||||
this.ui.log(`You sold ${item.name} for ${sellPrice} gold.`);
|
this.ui.log(`You sold ${item.getDisplayName()} for ${sellPrice} gold.`);
|
||||||
|
|
||||||
// Reset selection if last item was sold
|
// Reset selection if last item was sold
|
||||||
if (this.shopSelection >= this.player.inventory.length) {
|
if (this.shopSelection >= this.player.inventory.length) {
|
||||||
|
|
@ -655,6 +967,49 @@ export class Game {
|
||||||
this.ui.showPopup(msg, 2000);
|
this.ui.showPopup(msg, 2000);
|
||||||
this.ui.log(`${targetMonster.name} says: "${msg}"`);
|
this.ui.log(`${targetMonster.name} says: "${msg}"`);
|
||||||
}
|
}
|
||||||
|
} else if (targetMonster instanceof Farmer) {
|
||||||
|
const quote = targetMonster.getRandomQuote();
|
||||||
|
this.ui.showPopup(quote, 2000);
|
||||||
|
this.ui.log(`${targetMonster.name} says: "${quote}"`);
|
||||||
|
} else if (targetMonster instanceof Priest) {
|
||||||
|
// Father Sun's Identify & Uncurse services
|
||||||
|
const unidentifiedItems = this.player.inventory.filter(i => !i.identified);
|
||||||
|
const cursedEquipped = Object.values(this.player.equipment).filter(i => i && i.isCursed && i.identified);
|
||||||
|
|
||||||
|
if (unidentifiedItems.length > 0) {
|
||||||
|
if (this.player.gold >= 50) {
|
||||||
|
this.player.gold -= 50;
|
||||||
|
unidentifiedItems.forEach(i => i.identified = true);
|
||||||
|
this.ui.showPopup("Father Sun identifies your mysterious items!", 2000);
|
||||||
|
this.ui.log(`Father Sun says: "The sun reveals the truth of your gear." (50g paid)`);
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
this.ui.updateInventory(this.player);
|
||||||
|
} else {
|
||||||
|
this.ui.log(`Father Sun says: "I would identify your gear, but you lack the 50 gold donation."`);
|
||||||
|
}
|
||||||
|
} else if (cursedEquipped.length > 0) {
|
||||||
|
if (this.player.gold >= 100) {
|
||||||
|
this.player.gold -= 100;
|
||||||
|
cursedEquipped.forEach(i => {
|
||||||
|
i.isCursed = false;
|
||||||
|
this.ui.log(`The curse is lifted from your ${i.name}!`);
|
||||||
|
});
|
||||||
|
this.ui.showPopup("The curses are lifted!", 2000);
|
||||||
|
this.ui.log(`Father Sun says: "May you be free from these dark shackles." (100g paid)`);
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
this.ui.updateInventory(this.player);
|
||||||
|
} else {
|
||||||
|
this.ui.log(`Father Sun says: "I would lift your curses, but you lack the 100 gold donation."`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const quote = targetMonster.getRandomQuote();
|
||||||
|
this.ui.showPopup(quote, 2000);
|
||||||
|
this.ui.log(`${targetMonster.name} says: "${quote}"`);
|
||||||
|
}
|
||||||
|
} else if (targetMonster instanceof Gravekeeper) {
|
||||||
|
const quote = targetMonster.getRandomQuote();
|
||||||
|
this.ui.showPopup(quote, 2000);
|
||||||
|
this.ui.log(`${targetMonster.name} says: "${quote}"`);
|
||||||
} else {
|
} else {
|
||||||
this.attack(this.player, targetMonster);
|
this.attack(this.player, targetMonster);
|
||||||
}
|
}
|
||||||
|
|
@ -662,21 +1017,56 @@ export class Game {
|
||||||
this.player.x = newX;
|
this.player.x = newX;
|
||||||
this.player.y = newY;
|
this.player.y = newY;
|
||||||
|
|
||||||
|
// Check for trap
|
||||||
|
const trap = this.map.traps.find(t => t.x === newX && t.y === newY);
|
||||||
|
if (trap) {
|
||||||
|
trap.revealed = true;
|
||||||
|
const damage = Math.floor(this.depth * 1.5) + Math.floor(Math.random() * 3) + 2;
|
||||||
|
this.player.hp -= damage;
|
||||||
|
this.ui.log(`*SNAP* You triggered a trap! You take ${damage} damage.`);
|
||||||
|
this.ui.showPopup("TRAP!", 1000);
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
|
||||||
|
if (this.player.hp <= 0) {
|
||||||
|
this.ui.log("The trap was lethal!");
|
||||||
|
this.ui.log("GAME OVER");
|
||||||
|
this.gameOver = true;
|
||||||
|
setTimeout(() => alert("Game Over! Refresh to restart."), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for fountain
|
||||||
|
const fountain = this.map.fountains.find(f => f.x === newX && f.y === newY);
|
||||||
|
if (fountain && !fountain.used) {
|
||||||
|
if (this.player.hp < this.player.maxHp || this.player.poisoned) {
|
||||||
|
this.player.hp = this.player.maxHp;
|
||||||
|
this.player.poisoned = false;
|
||||||
|
fountain.used = true;
|
||||||
|
this.ui.log("You drink from the glowing fountain. You feel completely restored!");
|
||||||
|
this.ui.showPopup("RESTORED!", 1000);
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
} else {
|
||||||
|
this.ui.log("You drink from the fountain. It's refreshing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for item
|
// Check for item
|
||||||
const item = this.items.find(i => i.x === newX && i.y === newY);
|
const item = this.items.find(i => i.x === newX && i.y === newY);
|
||||||
if (item) {
|
if (item) {
|
||||||
this.ui.log(`You see a ${item.name}. (Press 'g' to get)`);
|
this.ui.log(`You see a ${item.name}. (Press 'g' to get)`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`Blocked at ${newX},${newY}. Tile: '${this.map.tiles[newY][newX]}'`);
|
// Quietly blocked
|
||||||
if (dx !== 0 || dy !== 0) this.ui.log("Blocked!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateExplored();
|
||||||
|
|
||||||
// Temple Healing (Town only)
|
// Temple Healing (Town only)
|
||||||
if (this.depth === 0 && this.player.x === 25 && this.player.y === 12) {
|
if (this.depth === 0 && this.player.x === 40 && this.player.y === 14) {
|
||||||
if (this.player.hp < this.player.maxHp) {
|
if (this.player.hp < this.player.maxHp || this.player.poisoned) {
|
||||||
this.player.hp = this.player.maxHp;
|
this.player.hp = this.player.maxHp;
|
||||||
this.ui.log("You enter the temple and feel refreshed. HP fully restored!");
|
this.player.poisoned = false;
|
||||||
|
this.ui.log("You enter the temple and feel refreshed. HP fully restored and poison cured!");
|
||||||
this.ui.updateStats(this.player, this.depth);
|
this.ui.updateStats(this.player, this.depth);
|
||||||
} else {
|
} else {
|
||||||
this.ui.log("You enter the temple. It is peaceful here.");
|
this.ui.log("You enter the temple. It is peaceful here.");
|
||||||
|
|
@ -790,6 +1180,7 @@ export class Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.updateExplored();
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -841,6 +1232,55 @@ export class Game {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
castDetectTrapsSpell() {
|
||||||
|
const manaCost = 2;
|
||||||
|
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 Detect Traps!");
|
||||||
|
|
||||||
|
let foundSomething = false;
|
||||||
|
this.map.traps.forEach(trap => {
|
||||||
|
if (!trap.revealed) {
|
||||||
|
// 50% chance to detect each unrevealed trap on the current level
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
trap.revealed = true;
|
||||||
|
foundSomething = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundSomething) {
|
||||||
|
this.ui.log("You sense danger nearby...");
|
||||||
|
} else {
|
||||||
|
this.ui.log("You sense nothing unusual.");
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
castCurePoisonSpell() {
|
||||||
|
const manaCost = 3;
|
||||||
|
if (this.player.mana < manaCost) {
|
||||||
|
this.ui.log("Not enough mana!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.player.poisoned) {
|
||||||
|
this.ui.log("You are not poisoned.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player.mana -= manaCost;
|
||||||
|
this.player.poisoned = false;
|
||||||
|
this.ui.updateStats(this.player, this.depth);
|
||||||
|
this.ui.log("You cast Cure Poison! The toxins leave your body.");
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
animateProjectile(projectile) {
|
animateProjectile(projectile) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const newX = projectile.x + projectile.dx;
|
const newX = projectile.x + projectile.dx;
|
||||||
|
|
@ -930,6 +1370,7 @@ export class Game {
|
||||||
itemData.dropped = true;
|
itemData.dropped = true;
|
||||||
|
|
||||||
const questItem = new Item(itemData.name, "quest", 1, {});
|
const questItem = new Item(itemData.name, "quest", 1, {});
|
||||||
|
questItem.identified = true;
|
||||||
questItem.x = monster.x;
|
questItem.x = monster.x;
|
||||||
questItem.y = monster.y;
|
questItem.y = monster.y;
|
||||||
this.items.push(questItem);
|
this.items.push(questItem);
|
||||||
|
|
@ -943,6 +1384,7 @@ export class Game {
|
||||||
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
|
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
|
||||||
if (monster.name === "The Dungeon Lord") {
|
if (monster.name === "The Dungeon Lord") {
|
||||||
const map = new Item("Ancient Map", "map", 1, {});
|
const map = new Item("Ancient Map", "map", 1, {});
|
||||||
|
map.identified = true;
|
||||||
map.x = monster.x;
|
map.x = monster.x;
|
||||||
map.y = monster.y;
|
map.y = monster.y;
|
||||||
this.items.push(map);
|
this.items.push(map);
|
||||||
|
|
@ -998,18 +1440,55 @@ export class Game {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
if (item.type === 'weapon') {
|
if (item.type === 'weapon') {
|
||||||
|
if (this.player.equipment.weapon === item) {
|
||||||
|
if (item.isCursed && item.identified) {
|
||||||
|
this.ui.log(`You cannot unequip the cursed ${item.name}!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.player.equipment.weapon = null;
|
||||||
|
this.ui.log(`You unequipped ${item.getDisplayName()}.`);
|
||||||
|
} else {
|
||||||
this.player.equipment.weapon = item;
|
this.player.equipment.weapon = item;
|
||||||
this.ui.log(`You equipped ${item.name}.`);
|
this.ui.log(`You equipped ${item.getDisplayName()}.`);
|
||||||
|
if (!item.identified) {
|
||||||
|
item.identified = true;
|
||||||
|
if (item.modifier !== 0 || item.isCursed) {
|
||||||
|
this.ui.log(`It's a ${item.getDisplayName()}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (item.type === 'shield') {
|
} else if (item.type === 'shield') {
|
||||||
|
if (this.player.equipment.shield === item) {
|
||||||
|
if (item.isCursed && item.identified) {
|
||||||
|
this.ui.log(`You cannot unequip the cursed ${item.name}!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.player.equipment.shield = null;
|
||||||
|
this.ui.log(`You unequipped ${item.getDisplayName()}.`);
|
||||||
|
} else {
|
||||||
this.player.equipment.shield = item;
|
this.player.equipment.shield = item;
|
||||||
this.ui.log(`You equipped ${item.name}.`);
|
this.ui.log(`You equipped ${item.getDisplayName()}.`);
|
||||||
|
if (!item.identified) {
|
||||||
|
item.identified = true;
|
||||||
|
if (item.modifier !== 0 || item.isCursed) {
|
||||||
|
this.ui.log(`It's a ${item.getDisplayName()}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (item.type === 'potion') {
|
} else if (item.type === 'potion') {
|
||||||
if (item.stats.heal) {
|
if (item.stats.heal) {
|
||||||
this.player.hp = Math.min(this.player.hp + item.stats.heal, this.player.maxHp);
|
this.player.hp = Math.min(this.player.hp + item.stats.heal, this.player.maxHp);
|
||||||
this.ui.log(`You drank ${item.name} and recovered ${item.stats.heal} HP.`);
|
this.ui.log(`You drank ${item.name} and recovered ${item.stats.heal} HP.`);
|
||||||
// Remove potion
|
|
||||||
this.player.inventory.splice(index, 1);
|
this.player.inventory.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
} else if (item.type === 'antidote') {
|
||||||
|
if (this.player.poisoned) {
|
||||||
|
this.player.poisoned = false;
|
||||||
|
this.ui.log("You drink the Antidote. The poison is cured!");
|
||||||
|
this.player.inventory.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.ui.log("You aren't poisoned.");
|
||||||
|
}
|
||||||
} else if (item.type === 'spellbook') {
|
} else if (item.type === 'spellbook') {
|
||||||
const spell = item.stats.spell;
|
const spell = item.stats.spell;
|
||||||
if (this.player.spells.includes(spell)) {
|
if (this.player.spells.includes(spell)) {
|
||||||
|
|
@ -1029,7 +1508,7 @@ export class Game {
|
||||||
|
|
||||||
attack(attacker, defender, presetDamage = null) {
|
attack(attacker, defender, presetDamage = null) {
|
||||||
// Shopkeepers and QuestGivers are invincible/pacifist
|
// Shopkeepers and QuestGivers are invincible/pacifist
|
||||||
if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver) return;
|
if (defender instanceof Shopkeeper || defender instanceof QuestGiver || defender instanceof WanderingQuestGiver || defender instanceof Farmer || defender instanceof Priest || defender instanceof Gravekeeper) return;
|
||||||
|
|
||||||
// Peasants are invincible
|
// Peasants are invincible
|
||||||
if (defender.name === "Peasant") {
|
if (defender.name === "Peasant") {
|
||||||
|
|
@ -1062,6 +1541,15 @@ export class Game {
|
||||||
defender.hp -= damage;
|
defender.hp -= damage;
|
||||||
this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`);
|
this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`);
|
||||||
|
|
||||||
|
// Poison logic
|
||||||
|
if (attacker instanceof Cobra && defender === this.player && !this.player.poisoned) {
|
||||||
|
if (Math.random() < attacker.poisonChance) {
|
||||||
|
this.player.poisoned = true;
|
||||||
|
this.ui.log("You have been poisoned by the Cobra!");
|
||||||
|
this.ui.showPopup("POISONED!", 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (defender.hp <= 0) {
|
if (defender.hp <= 0) {
|
||||||
if (defender === this.player) {
|
if (defender === this.player) {
|
||||||
this.ui.log("GAME OVER");
|
this.ui.log("GAME OVER");
|
||||||
|
|
@ -1075,7 +1563,7 @@ export class Game {
|
||||||
|
|
||||||
updateMonsters() {
|
updateMonsters() {
|
||||||
for (const monster of this.monsters) {
|
for (const monster of this.monsters) {
|
||||||
if (monster instanceof Shopkeeper || monster instanceof QuestGiver) continue; // Shopkeepers and QuestGivers don't move
|
if (monster instanceof Shopkeeper || monster instanceof QuestGiver || monster instanceof Farmer || monster instanceof Priest || monster instanceof Gravekeeper) continue; // Stationary NPCs don't move
|
||||||
|
|
||||||
// Speed check: Does the monster move this turn?
|
// Speed check: Does the monster move this turn?
|
||||||
if (monster.speed !== undefined && Math.random() > monster.speed) continue;
|
if (monster.speed !== undefined && Math.random() > monster.speed) continue;
|
||||||
|
|
@ -1100,12 +1588,12 @@ export class Game {
|
||||||
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 && this.map.hasLineOfSight(monster.x, monster.y, this.player.x, this.player.y)) { // Aggro range + LOS
|
||||||
// Ranged behavior for Archers
|
// Ranged behavior for Archers
|
||||||
if (monster instanceof Archer && dist > 1.5 && dist <= monster.range) {
|
if (monster instanceof Archer && dist > 1.5 && dist <= monster.range) {
|
||||||
const isOrthogonal = this.player.x === monster.x || this.player.y === monster.y;
|
const isOrthogonal = this.player.x === monster.x || this.player.y === monster.y;
|
||||||
const isDiagonal = Math.abs(dx) === Math.abs(dy);
|
const isDiagonal = Math.abs(dx) === Math.abs(dy);
|
||||||
if (isOrthogonal || isDiagonal) {
|
if ((isOrthogonal || isDiagonal) && this.map.hasLineOfSight(monster.x, monster.y, this.player.x, this.player.y)) {
|
||||||
this.monsterShoot(monster);
|
this.monsterShoot(monster);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1148,6 +1636,14 @@ export class Game {
|
||||||
this.ui.drawInventory(this.player, this.inventorySelection);
|
this.ui.drawInventory(this.player, this.inventorySelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.gameState === 'OVERVIEW') {
|
||||||
|
this.ui.drawOverviewMap(this.map, this.player);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.gameState === 'INTRO') {
|
||||||
|
this.ui.drawIntro(this.storyText);
|
||||||
|
}
|
||||||
|
|
||||||
this.ui.updateStats(this.player, this.depth);
|
this.ui.updateStats(this.player, this.depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/Item.js
|
|
@ -1,7 +1,7 @@
|
||||||
export class Item {
|
export class Item {
|
||||||
constructor(name, type, level, stats) {
|
constructor(name, type, level, stats) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.type = type; // 'weapon', 'armor', 'potion'
|
this.type = type; // 'weapon', 'armor', 'shield', 'potion', 'spellbook', 'antidote', 'map', 'quest'
|
||||||
this.level = level;
|
this.level = level;
|
||||||
this.stats = stats || {}; // { damage: 5 } or { defense: 2 }
|
this.stats = stats || {}; // { damage: 5 } or { defense: 2 }
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
|
|
@ -9,12 +9,38 @@ export class Item {
|
||||||
this.symbol = '?';
|
this.symbol = '?';
|
||||||
this.color = '#ffff00';
|
this.color = '#ffff00';
|
||||||
|
|
||||||
|
// Enchantment / Curse properties
|
||||||
|
this.modifier = 0;
|
||||||
|
this.isCursed = false;
|
||||||
|
this.identified = false;
|
||||||
|
|
||||||
|
// Auto-identify non-equippables
|
||||||
|
if (['potion', 'antidote', 'spellbook', 'map', 'quest'].includes(type)) {
|
||||||
|
this.identified = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'weapon') this.symbol = ')';
|
if (type === 'weapon') this.symbol = ')';
|
||||||
if (type === 'armor') this.symbol = '[';
|
if (type === 'armor') this.symbol = '[';
|
||||||
if (type === 'shield') this.symbol = ']';
|
if (type === 'shield') this.symbol = ']';
|
||||||
if (type === 'potion') this.symbol = '!';
|
if (type === 'potion') this.symbol = '!';
|
||||||
|
if (type === 'antidote') { this.symbol = '!'; this.color = '#00ff00'; }
|
||||||
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 === 'quest') { this.symbol = '*'; this.color = '#ff00ff'; }
|
||||||
if (type === 'spellbook') { this.symbol = 'B'; this.color = '#00ffff'; }
|
if (type === 'spellbook') { this.symbol = 'B'; this.color = '#00ffff'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisplayName() {
|
||||||
|
if (!this.identified) {
|
||||||
|
if (this.modifier !== 0 || this.isCursed) {
|
||||||
|
return `A Mysterious ${this.name}`;
|
||||||
|
}
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = this.name;
|
||||||
|
if (this.isCursed) name = "Cursed " + name;
|
||||||
|
if (this.modifier > 0) name += ` (+${this.modifier})`;
|
||||||
|
if (this.modifier < 0) name += ` (${this.modifier})`;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
127
src/Map.js
|
|
@ -5,23 +5,33 @@ export class Map {
|
||||||
this.tiles = [];
|
this.tiles = [];
|
||||||
this.rooms = [];
|
this.rooms = [];
|
||||||
this.permanentlyLit = []; // New grid for spell effects
|
this.permanentlyLit = []; // New grid for spell effects
|
||||||
|
this.explored = []; // Track which tiles the player has seen
|
||||||
|
this.traps = []; // New array for level traps
|
||||||
|
this.fountains = []; // New array for healing fountains
|
||||||
|
this.signs = []; // New array for interactive signs
|
||||||
}
|
}
|
||||||
|
|
||||||
generate() {
|
generate() {
|
||||||
this.isTown = false;
|
this.isTown = false;
|
||||||
this.rooms = [];
|
this.rooms = [];
|
||||||
this.permanentlyLit = [];
|
this.permanentlyLit = [];
|
||||||
|
this.explored = [];
|
||||||
|
this.traps = [];
|
||||||
|
this.fountains = [];
|
||||||
|
this.signs = [];
|
||||||
// Initialize with walls and dark
|
// 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] = [];
|
this.permanentlyLit[y] = [];
|
||||||
|
this.explored[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;
|
this.permanentlyLit[y][x] = false;
|
||||||
|
this.explored[y][x] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_ROOMS = 10;
|
const MAX_ROOMS = 30;
|
||||||
const MIN_SIZE = 6;
|
const MIN_SIZE = 6;
|
||||||
const MAX_SIZE = 12;
|
const MAX_SIZE = 12;
|
||||||
|
|
||||||
|
|
@ -63,19 +73,51 @@ export class Map {
|
||||||
this.rooms.push(newRoom);
|
this.rooms.push(newRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.generateTraps();
|
||||||
|
this.generateFountain();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTraps() {
|
||||||
|
const numTraps = Math.floor(Math.random() * 6); // 0-5
|
||||||
|
for (let i = 0; i < numTraps; i++) {
|
||||||
|
if (this.rooms.length === 0) break;
|
||||||
|
const room = this.rooms[Math.floor(Math.random() * this.rooms.length)];
|
||||||
|
const tx = Math.floor(Math.random() * room.w) + room.x;
|
||||||
|
const ty = Math.floor(Math.random() * room.h) + room.y;
|
||||||
|
|
||||||
|
// Avoid placing trap exactly on player starting stairs if possible
|
||||||
|
this.traps.push({ x: tx, y: ty, revealed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFountain() {
|
||||||
|
if (Math.random() < 0.25) { // 25% chance
|
||||||
|
if (this.rooms.length === 0) return;
|
||||||
|
const room = this.rooms[Math.floor(Math.random() * this.rooms.length)];
|
||||||
|
const fx = Math.floor(Math.random() * room.w) + room.x;
|
||||||
|
const fy = Math.floor(Math.random() * room.h) + room.y;
|
||||||
|
this.fountains.push({ x: fx, y: fy, used: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateTown() {
|
generateTown() {
|
||||||
this.isTown = true;
|
this.isTown = true;
|
||||||
this.rooms = [];
|
this.rooms = [];
|
||||||
this.permanentlyLit = [];
|
this.permanentlyLit = [];
|
||||||
|
this.explored = [];
|
||||||
|
this.traps = [];
|
||||||
|
this.fountains = [];
|
||||||
|
this.signs = [];
|
||||||
// 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] = [];
|
this.permanentlyLit[y] = [];
|
||||||
|
this.explored[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
|
this.permanentlyLit[y][x] = true; // Town is all lit
|
||||||
|
this.explored[y][x] = true; // Town is fully explored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,21 +150,55 @@ export class Map {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Temple (North Center)
|
// 1. Temple (North Center)
|
||||||
drawBuilding(20, 5, 10, 8, 'bottom');
|
drawBuilding(35, 10, 10, 8, 'bottom');
|
||||||
|
this.signs.push({ x: 41, y: 18, text: "Temple of the Sun" });
|
||||||
|
|
||||||
// 2. General Store (West)
|
// 2. General Store (West)
|
||||||
drawBuilding(5, 15, 8, 6, 'right');
|
drawBuilding(15, 30, 8, 6, 'right');
|
||||||
|
this.signs.push({ x: 23, y: 31, text: "Oakhaven General Goods" });
|
||||||
|
|
||||||
// 3. Weapon Shop (East)
|
// 3. Weapon Shop (East)
|
||||||
drawBuilding(37, 15, 8, 6, 'left');
|
drawBuilding(55, 30, 8, 6, 'left');
|
||||||
|
this.signs.push({ x: 54, y: 31, text: "Oakhaven Blacksmith" });
|
||||||
|
|
||||||
// 4. Dungeon Entrance (North East)
|
// 4. Dungeon Entrance (North East)
|
||||||
drawBuilding(40, 5, 6, 6, 'bottom');
|
drawBuilding(60, 10, 6, 6, 'bottom');
|
||||||
|
|
||||||
// 5. Houses (South)
|
// 5. Houses (South)
|
||||||
drawBuilding(10, 30, 6, 5, 'top');
|
drawBuilding(20, 55, 6, 5, 'top');
|
||||||
drawBuilding(22, 30, 6, 5, 'top');
|
drawBuilding(37, 55, 6, 5, 'top');
|
||||||
drawBuilding(34, 30, 6, 5, 'top');
|
drawBuilding(54, 55, 6, 5, 'top');
|
||||||
|
|
||||||
|
// 6. Cosmetic Fields (Farmed land)
|
||||||
|
const drawField = (x, y, w, h) => {
|
||||||
|
for (let fy = y; fy < y + h; fy++) {
|
||||||
|
for (let fx = x; fx < x + w; fx++) {
|
||||||
|
// Alternating brown and green for a "plowed" look
|
||||||
|
this.tiles[fy][fx] = (fx + fy) % 2 === 0 ? '"' : ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drawField(10, 15, 10, 8); // West field
|
||||||
|
drawField(55, 45, 12, 6); // East field
|
||||||
|
|
||||||
|
// 7. Graveyard (North West)
|
||||||
|
const drawGraveyard = (x, y, w, h) => {
|
||||||
|
for (let gy = y; gy < y + h; gy++) {
|
||||||
|
for (let gx = x; gx < x + w; gx++) {
|
||||||
|
if (gy === y || gy === y + h - 1 || gx === x || gx === x + w - 1) {
|
||||||
|
this.tiles[gy][gx] = '#'; // Fence/Wall
|
||||||
|
} else {
|
||||||
|
// Randomly place tombstones
|
||||||
|
this.tiles[gy][gx] = Math.random() < 0.2 ? 't' : '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Entrance
|
||||||
|
this.tiles[y + h - 1][Math.floor(x + w / 2)] = '.';
|
||||||
|
};
|
||||||
|
|
||||||
|
drawGraveyard(5, 5, 12, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -151,6 +227,41 @@ export class Map {
|
||||||
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] === '<';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasLineOfSight(x1, y1, x2, y2) {
|
||||||
|
let dx = Math.abs(x2 - x1);
|
||||||
|
let dy = Math.abs(y2 - y1);
|
||||||
|
let x = x1;
|
||||||
|
let y = y1;
|
||||||
|
let n = 1 + dx + dy;
|
||||||
|
let x_inc = (x2 > x1) ? 1 : -1;
|
||||||
|
let y_inc = (y2 > y1) ? 1 : -1;
|
||||||
|
let error = dx - dy;
|
||||||
|
dx *= 2;
|
||||||
|
dy *= 2;
|
||||||
|
|
||||||
|
for (; n > 0; --n) {
|
||||||
|
// Check if current tile blocks light (only wall '#' blocks light)
|
||||||
|
if (this.tiles[y][x] === '#' && (x !== x1 || y !== y1) && (x !== x2 || y !== y2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error > 0) {
|
||||||
|
x += x_inc;
|
||||||
|
error -= dy;
|
||||||
|
} else if (error < 0) {
|
||||||
|
y += y_inc;
|
||||||
|
error += dx;
|
||||||
|
} else {
|
||||||
|
// Diagonal move
|
||||||
|
x += x_inc;
|
||||||
|
y += y_inc;
|
||||||
|
error += dx - dy;
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
isLit(x, y) {
|
isLit(x, y) {
|
||||||
// Town is always fully lit
|
// Town is always fully lit
|
||||||
if (this.isTown) return true;
|
if (this.isTown) return true;
|
||||||
|
|
|
||||||
403
src/UI.js
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { Farmer, Gravekeeper } from './Entity.js';
|
||||||
|
|
||||||
|
|
||||||
export class UI {
|
export class UI {
|
||||||
constructor(ctx, game) {
|
constructor(ctx, game) {
|
||||||
console.log("UI Version 1.1 Loaded");
|
console.log("UI Version 1.1 Loaded");
|
||||||
|
|
@ -5,7 +8,8 @@ export class UI {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
this.messageLog = document.getElementById('message-log');
|
this.messageLog = document.getElementById('message-log');
|
||||||
this.spellsList = document.getElementById('spells-list');
|
this.spellsList = document.getElementById('spells-list');
|
||||||
this.inventoryLink = document.getElementById('inventory-link');
|
this.inventoryLink = docugment.getElementById('inventory-link');
|
||||||
|
this.inventoryLabel = document.querySelector('#inventory-panel .group-box-label');
|
||||||
|
|
||||||
if (this.inventoryLink) {
|
if (this.inventoryLink) {
|
||||||
this.inventoryLink.onclick = (e) => {
|
this.inventoryLink.onclick = (e) => {
|
||||||
|
|
@ -14,6 +18,11 @@ export class UI {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.inventoryLabel) {
|
||||||
|
this.inventoryLabel.style.cursor = 'pointer';
|
||||||
|
this.inventoryLabel.onclick = () => this.game.toggleInventory();
|
||||||
|
}
|
||||||
|
|
||||||
this.popup = null;
|
this.popup = null;
|
||||||
this.popupTimeout = null;
|
this.popupTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
@ -50,12 +59,18 @@ export class UI {
|
||||||
const tile = map.tiles[mapY][mapX];
|
const tile = map.tiles[mapY][mapX];
|
||||||
const isLit = map.isLit(mapX, mapY);
|
const isLit = map.isLit(mapX, mapY);
|
||||||
const dist = Math.sqrt(Math.pow(player.x - mapX, 2) + Math.pow(player.y - mapY, 2));
|
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)
|
const inRadius = dist <= 3.5;
|
||||||
|
const isExplored = map.explored[mapY][mapX];
|
||||||
|
|
||||||
if (isLit || inRadius) {
|
// Tiles are visible if lit, in radius, or remembered
|
||||||
this.drawTile(x * tileSize, y * tileSize, tile, tileSize);
|
if (isLit || inRadius || isExplored) {
|
||||||
|
// Check if it's currently in Line of Sight to draw it "bright"
|
||||||
|
const hasLOS = map.hasLineOfSight(player.x, player.y, mapX, mapY);
|
||||||
|
const isCurrentlyVisible = (isLit || inRadius) && hasLOS;
|
||||||
|
|
||||||
|
this.drawTile(x * tileSize, y * tileSize, tile, tileSize, !isCurrentlyVisible);
|
||||||
} else {
|
} else {
|
||||||
// Draw nothing or a very dark tile for "unseen" areas
|
// Draw nothing for unseen areas
|
||||||
this.ctx.fillStyle = '#000000';
|
this.ctx.fillStyle = '#000000';
|
||||||
this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
||||||
}
|
}
|
||||||
|
|
@ -63,12 +78,91 @@ export class UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw Traps
|
||||||
|
if (map.traps) {
|
||||||
|
for (const trap of map.traps) {
|
||||||
|
if (!trap.revealed) continue;
|
||||||
|
|
||||||
|
const isLit = map.isLit(trap.x, trap.y);
|
||||||
|
const dist = Math.sqrt(Math.pow(player.x - trap.x, 2) + Math.pow(player.y - trap.y, 2));
|
||||||
|
const inRadius = dist <= 3.5;
|
||||||
|
|
||||||
|
if (!(isLit || inRadius)) continue;
|
||||||
|
if (!map.hasLineOfSight(player.x, player.y, trap.x, trap.y)) continue;
|
||||||
|
|
||||||
|
const screenX = (trap.x - startX) * tileSize;
|
||||||
|
const screenY = (trap.y - startY) * tileSize;
|
||||||
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||||
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
||||||
|
this.ctx.fillStyle = '#ff0000';
|
||||||
|
this.ctx.fillText('^', screenX + tileSize / 4, screenY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Fountains
|
||||||
|
if (map.fountains) {
|
||||||
|
for (const fountain of map.fountains) {
|
||||||
|
const isLit = map.isLit(fountain.x, fountain.y);
|
||||||
|
const dist = Math.sqrt(Math.pow(player.x - fountain.x, 2) + Math.pow(player.y - fountain.y, 2));
|
||||||
|
const inRadius = dist <= 3.5;
|
||||||
|
|
||||||
|
if (!(isLit || inRadius)) continue;
|
||||||
|
if (!map.hasLineOfSight(player.x, player.y, fountain.x, fountain.y)) continue;
|
||||||
|
|
||||||
|
const screenX = (fountain.x - startX) * tileSize;
|
||||||
|
const screenY = (fountain.y - startY) * tileSize;
|
||||||
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||||
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
||||||
|
|
||||||
|
const asset = this.game.assets['fountain'];
|
||||||
|
if (asset && !fountain.used) {
|
||||||
|
this.ctx.drawImage(asset, screenX, screenY, tileSize, tileSize);
|
||||||
|
} else {
|
||||||
|
this.ctx.fillStyle = fountain.used ? '#808080' : '#00ffff';
|
||||||
|
this.ctx.fillText('&', screenX + tileSize / 4, screenY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Signs
|
||||||
|
if (map.signs) {
|
||||||
|
for (const sign of map.signs) {
|
||||||
|
const isLit = map.isLit(sign.x, sign.y);
|
||||||
|
const dist = Math.sqrt(Math.pow(player.x - sign.x, 2) + Math.pow(player.y - sign.y, 2));
|
||||||
|
const inRadius = dist <= 3.5;
|
||||||
|
const isExplored = map.explored[sign.y][sign.x];
|
||||||
|
|
||||||
|
if (!(isLit || inRadius || isExplored)) continue;
|
||||||
|
|
||||||
|
// Only show bright if in LOS
|
||||||
|
const hasLOS = map.hasLineOfSight(player.x, player.y, sign.x, sign.y);
|
||||||
|
const isCurrentlyVisible = (isLit || inRadius) && hasLOS;
|
||||||
|
|
||||||
|
const screenX = (sign.x - startX) * tileSize;
|
||||||
|
const screenY = (sign.y - startY) * tileSize;
|
||||||
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||||
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
||||||
|
this.ctx.fillStyle = isCurrentlyVisible ? '#8b4513' : '#4a250a'; // Darker brown if not visible
|
||||||
|
this.ctx.fillRect(screenX + 4, screenY + 4, tileSize - 8, tileSize - 8);
|
||||||
|
this.ctx.fillStyle = isCurrentlyVisible ? '#ffffff' : '#888888';
|
||||||
|
this.ctx.font = `bold ${tileSize/2}px monospace`;
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.fillText('?', screenX + tileSize / 2, screenY + tileSize / 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 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));
|
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 inRadius = dist <= 3.5;
|
||||||
|
|
||||||
|
if (!(isLit || inRadius)) continue;
|
||||||
|
if (!map.hasLineOfSight(player.x, player.y, item.x, item.y)) 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;
|
||||||
|
|
@ -83,7 +177,10 @@ export class UI {
|
||||||
for (const monster of monsters) {
|
for (const monster of monsters) {
|
||||||
const isLit = map.isLit(monster.x, monster.y);
|
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));
|
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 inRadius = dist <= 3.5;
|
||||||
|
|
||||||
|
if (!(isLit || inRadius)) continue;
|
||||||
|
if (!map.hasLineOfSight(player.x, player.y, monster.x, monster.y)) 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;
|
||||||
|
|
@ -120,7 +217,10 @@ export class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStats(player, depth) {
|
updateStats(player, depth) {
|
||||||
document.getElementById('stat-hp').textContent = player.hp;
|
const hpElem = document.getElementById('stat-hp');
|
||||||
|
hpElem.textContent = player.hp;
|
||||||
|
hpElem.style.color = player.poisoned ? '#00ff00' : '#ffffff';
|
||||||
|
|
||||||
document.getElementById('stat-max-hp').textContent = player.maxHp;
|
document.getElementById('stat-max-hp').textContent = player.maxHp;
|
||||||
document.getElementById('stat-str').textContent = player.stats.str;
|
document.getElementById('stat-str').textContent = player.stats.str;
|
||||||
document.getElementById('stat-level').textContent = player.level;
|
document.getElementById('stat-level').textContent = player.level;
|
||||||
|
|
@ -131,6 +231,11 @@ export class UI {
|
||||||
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;
|
||||||
|
|
||||||
|
const statusElem = document.getElementById('stat-status');
|
||||||
|
if (statusElem) {
|
||||||
|
statusElem.textContent = player.poisoned ? "POISONED" : "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInventory(player) {
|
updateInventory(player) {
|
||||||
|
|
@ -140,6 +245,106 @@ export class UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawOverviewMap(map, player) {
|
||||||
|
const width = 500;
|
||||||
|
const height = 500;
|
||||||
|
const x = (this.ctx.canvas.width - width) / 2;
|
||||||
|
const y = (this.ctx.canvas.height - height) / 2;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
this.ctx.strokeStyle = '#ffffff';
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
this.ctx.fillStyle = '#ffffff';
|
||||||
|
this.ctx.font = 'bold 20px monospace';
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.fillText('MAP OVERVIEW', x + width / 2, y + 30);
|
||||||
|
|
||||||
|
// Map Scale
|
||||||
|
const padding = 50;
|
||||||
|
const mapAreaWidth = width - padding * 2;
|
||||||
|
const mapAreaHeight = height - padding * 2;
|
||||||
|
const scaleX = mapAreaWidth / map.width;
|
||||||
|
const scaleY = mapAreaHeight / map.height;
|
||||||
|
const scale = Math.min(scaleX, scaleY);
|
||||||
|
|
||||||
|
const offsetX = x + (width - (map.width * scale)) / 2;
|
||||||
|
const offsetY = y + (height - (map.height * scale)) / 2;
|
||||||
|
|
||||||
|
for (let my = 0; my < map.height; my++) {
|
||||||
|
for (let mx = 0; mx < map.width; mx++) {
|
||||||
|
if (map.explored[my][mx]) {
|
||||||
|
const tile = map.tiles[my][mx];
|
||||||
|
if (tile === '#') {
|
||||||
|
this.ctx.fillStyle = '#555555';
|
||||||
|
} else if (tile === '>') {
|
||||||
|
this.ctx.fillStyle = '#ffffff'; // Highlight stairs down
|
||||||
|
} else if (tile === '<') {
|
||||||
|
this.ctx.fillStyle = '#ffffff'; // Highlight stairs up
|
||||||
|
} else if (tile === 't') {
|
||||||
|
// Tombstone
|
||||||
|
this.ctx.fillStyle = '#222222';
|
||||||
|
} else {
|
||||||
|
this.ctx.fillStyle = '#222222';
|
||||||
|
}
|
||||||
|
this.ctx.fillRect(offsetX + mx * scale, offsetY + my * scale, scale, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Player Position
|
||||||
|
this.ctx.fillStyle = '#ffff00';
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(offsetX + player.x * scale + scale / 2, offsetY + player.y * scale + scale / 2, scale * 1.5, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// Legend/Instructions
|
||||||
|
this.ctx.fillStyle = '#888888';
|
||||||
|
this.ctx.font = '12px monospace';
|
||||||
|
this.ctx.fillText('Yellow: You | White: Stairs | [M] to Close', x + width / 2, y + height - 15);
|
||||||
|
|
||||||
|
this.ctx.textAlign = 'start';
|
||||||
|
}
|
||||||
|
|
||||||
|
drawIntro(textLines) {
|
||||||
|
const width = 750;
|
||||||
|
const height = 550;
|
||||||
|
const x = (this.ctx.canvas.width - width) / 2;
|
||||||
|
const y = (this.ctx.canvas.height - height) / 2;
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||||
|
this.ctx.fillRect(x + 10, y + 10, width, height);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
this.ctx.fillStyle = '#000000';
|
||||||
|
this.ctx.fillRect(x, y, width, height);
|
||||||
|
this.ctx.strokeStyle = '#ffffff';
|
||||||
|
this.ctx.lineWidth = 3;
|
||||||
|
this.ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
this.ctx.fillStyle = '#ffff00';
|
||||||
|
this.ctx.font = 'bold 24px "Courier New", monospace';
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.fillText('THE LEGEND OF OAKHAVEN', x + width / 2, y + 40);
|
||||||
|
|
||||||
|
// Text
|
||||||
|
this.ctx.fillStyle = '#ffffff';
|
||||||
|
this.ctx.font = '16px "Courier New", monospace';
|
||||||
|
let lineY = y + 80;
|
||||||
|
textLines.forEach(line => {
|
||||||
|
this.ctx.fillText(line, x + width / 2, lineY);
|
||||||
|
lineY += 20;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ctx.textAlign = 'start';
|
||||||
|
}
|
||||||
|
|
||||||
drawInventory(player, selectedIndex) {
|
drawInventory(player, selectedIndex) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const width = 500;
|
const width = 500;
|
||||||
|
|
@ -165,28 +370,52 @@ export class UI {
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
let itemY = y + 70;
|
let itemY = y + 70;
|
||||||
|
|
||||||
if (player.inventory.length === 0) {
|
const groupedItems = this.game.getGroupedInventory();
|
||||||
|
|
||||||
|
if (groupedItems.length === 0) {
|
||||||
ctx.fillStyle = '#888888';
|
ctx.fillStyle = '#888888';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText('Empty', x + width / 2, itemY + 20);
|
ctx.fillText('Empty', x + width / 2, itemY + 20);
|
||||||
} else {
|
} else {
|
||||||
player.inventory.forEach((item, index) => {
|
groupedItems.forEach((group, index) => {
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
const isEquipped = player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item;
|
const isEquipped = group.indices.some(idx => {
|
||||||
|
const item = player.inventory[idx];
|
||||||
|
return player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item;
|
||||||
|
});
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
ctx.fillStyle = '#333333';
|
ctx.fillStyle = '#333333';
|
||||||
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
||||||
ctx.fillStyle = '#ffff00';
|
ctx.fillStyle = '#ffff00';
|
||||||
ctx.fillText('>', x + 25, itemY + 10);
|
ctx.fillText('>', x + 25, itemY + 10);
|
||||||
|
} else {
|
||||||
|
// Color based on quality if identified
|
||||||
|
if (!group.item.identified) {
|
||||||
|
ctx.fillStyle = '#aaaaaa'; // Gray for unidentified
|
||||||
|
} else if (group.item.isCursed) {
|
||||||
|
ctx.fillStyle = '#ff4444'; // Red for cursed
|
||||||
|
} else if (group.item.modifier > 0) {
|
||||||
|
ctx.fillStyle = '#00ffff'; // Cyan for enchanted
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#aaaaaa';
|
ctx.fillStyle = '#aaaaaa';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let displayName = item.name;
|
let displayName = group.item.getDisplayName();
|
||||||
|
if (group.count > 1) displayName += ` (${group.count})`;
|
||||||
if (isEquipped) displayName += " (E)";
|
if (isEquipped) displayName += " (E)";
|
||||||
|
|
||||||
ctx.fillText(displayName, x + 50, itemY + 10);
|
// Draw Icon if available
|
||||||
|
const assetName = this.getAssetName(group.item);
|
||||||
|
const asset = this.game.assets[assetName];
|
||||||
|
if (asset) {
|
||||||
|
this.ctx.drawImage(asset, x + 50, itemY, 20, 20);
|
||||||
|
this.ctx.fillText(displayName, x + 80, itemY + 10);
|
||||||
|
} else {
|
||||||
|
this.ctx.fillText(displayName, x + 50, itemY + 10);
|
||||||
|
}
|
||||||
|
|
||||||
itemY += 30;
|
itemY += 30;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +424,7 @@ export class UI {
|
||||||
ctx.fillStyle = '#888888';
|
ctx.fillStyle = '#888888';
|
||||||
ctx.font = '12px monospace';
|
ctx.font = '12px monospace';
|
||||||
ctx.textAlign = 'center';
|
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);
|
ctx.fillText('1:Laser|2:Fire|3:Light|4:Heal|5:Ret|6:Det|7:Cure|Up/Down:Sel|Enter:Use|Esc/i:Close', x + width / 2, y + height - 20);
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
ctx.textAlign = 'start';
|
ctx.textAlign = 'start';
|
||||||
|
|
@ -207,48 +436,63 @@ export class UI {
|
||||||
player.spells.forEach((spell, index) => {
|
player.spells.forEach((spell, index) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.textContent = `${index + 1}. ${spell}`;
|
li.textContent = `${index + 1}. ${spell}`;
|
||||||
|
li.style.cursor = 'pointer';
|
||||||
|
li.style.padding = '2px';
|
||||||
|
li.onmouseover = () => li.style.backgroundColor = 'rgba(255,255,255,0.1)';
|
||||||
|
li.onmouseout = () => li.style.backgroundColor = 'transparent';
|
||||||
|
li.onclick = () => {
|
||||||
|
if (spell === 'zappy laser') this.game.startTargeting('zappy laser');
|
||||||
|
else if (spell === 'Fireball') this.game.startTargeting('Fireball');
|
||||||
|
else if (spell === 'Light') this.game.castLightSpell();
|
||||||
|
else if (spell === 'Heal') this.game.castHealSpell();
|
||||||
|
else if (spell === 'Return') this.game.castReturnSpell();
|
||||||
|
else if (spell === 'Detect Traps') this.game.castDetectTrapsSpell();
|
||||||
|
else if (spell === 'Cure Poison') this.game.castCurePoisonSpell();
|
||||||
|
};
|
||||||
this.spellsList.appendChild(li);
|
this.spellsList.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTile(screenX, screenY, tile, size) {
|
drawTile(screenX, screenY, tile, size, isMemorized = false) {
|
||||||
|
const ctx = this.ctx;
|
||||||
if (tile === '#') {
|
if (tile === '#') {
|
||||||
this.ctx.fillStyle = '#808080'; // Gray wall
|
ctx.fillStyle = isMemorized ? '#404040' : '#808080'; // Darker gray for memorized walls
|
||||||
this.ctx.fillRect(screenX, screenY, size, size);
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
// Add a bevel effect for walls
|
|
||||||
this.ctx.strokeStyle = '#ffffff';
|
|
||||||
this.ctx.lineWidth = 2;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(screenX, screenY + size);
|
|
||||||
this.ctx.lineTo(screenX, screenY);
|
|
||||||
this.ctx.lineTo(screenX + size, screenY);
|
|
||||||
this.ctx.stroke();
|
|
||||||
|
|
||||||
this.ctx.strokeStyle = '#404040';
|
// Wall highlights
|
||||||
this.ctx.beginPath();
|
ctx.strokeStyle = isMemorized ? '#606060' : '#ffffff';
|
||||||
this.ctx.moveTo(screenX + size, screenY);
|
ctx.lineWidth = 1;
|
||||||
this.ctx.lineTo(screenX + size, screenY + size);
|
ctx.strokeRect(screenX + 1, screenY + 1, size - 2, size - 2);
|
||||||
this.ctx.lineTo(screenX, screenY + size);
|
|
||||||
this.ctx.stroke();
|
|
||||||
|
|
||||||
} else if (tile === '>') {
|
} else if (tile === '>') {
|
||||||
// Stairs Down
|
// Stairs Down
|
||||||
this.ctx.fillStyle = '#202020';
|
ctx.fillStyle = '#202020';
|
||||||
this.ctx.fillRect(screenX, screenY, size, size);
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
this.ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = isMemorized ? '#888888' : '#ffffff';
|
||||||
this.ctx.fillText('>', screenX + size / 4, screenY);
|
ctx.fillText('>', screenX + size / 4, screenY);
|
||||||
} else if (tile === '<') {
|
} else if (tile === '<') {
|
||||||
// Stairs Up
|
// Stairs Up
|
||||||
this.ctx.fillStyle = '#202020';
|
ctx.fillStyle = '#202020';
|
||||||
this.ctx.fillRect(screenX, screenY, size, size);
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
this.ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = isMemorized ? '#888888' : '#ffffff';
|
||||||
this.ctx.fillText('<', screenX + size / 4, screenY);
|
ctx.fillText('<', screenX + size / 4, screenY);
|
||||||
|
} else if (tile === '"' || tile === ',') {
|
||||||
|
// Field
|
||||||
|
if (tile === '"') ctx.fillStyle = isMemorized ? '#3d2b25' : '#5d4037';
|
||||||
|
else ctx.fillStyle = isMemorized ? '#1b4a1e' : '#2e7d32';
|
||||||
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
|
} else if (tile === 't') {
|
||||||
|
// Tombstone
|
||||||
|
ctx.fillStyle = '#202020';
|
||||||
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
|
ctx.fillStyle = isMemorized ? '#404040' : '#888888';
|
||||||
|
ctx.fillText('†', screenX + size / 4, screenY);
|
||||||
} else {
|
} else {
|
||||||
// Floor
|
// Floor
|
||||||
this.ctx.fillStyle = '#202020';
|
ctx.fillStyle = '#202020';
|
||||||
this.ctx.fillRect(screenX, screenY, size, size);
|
ctx.fillRect(screenX, screenY, size, size);
|
||||||
this.ctx.fillStyle = '#404040';
|
ctx.fillStyle = isMemorized ? '#222222' : '#404040'; // Very dark for memorized floor
|
||||||
this.ctx.fillText('.', screenX + size / 4, screenY);
|
ctx.fillText('.', screenX + size / 4, screenY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,9 +541,54 @@ export class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawEntity(screenX, screenY, entity, size) {
|
drawEntity(screenX, screenY, entity, size) {
|
||||||
|
const assetName = this.getAssetName(entity);
|
||||||
|
const asset = this.game.assets[assetName];
|
||||||
|
|
||||||
|
if (asset) {
|
||||||
|
this.ctx.drawImage(asset, screenX, screenY, size, size);
|
||||||
|
} else {
|
||||||
this.ctx.fillStyle = entity.color;
|
this.ctx.fillStyle = entity.color;
|
||||||
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
|
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetName(entity) {
|
||||||
|
if (!entity) return null;
|
||||||
|
const name = entity.name || "";
|
||||||
|
|
||||||
|
if (name === 'Player') return 'hero';
|
||||||
|
if (name === 'Rat') return 'rat';
|
||||||
|
if (name === 'Kobold') return 'kobold';
|
||||||
|
if (name === 'Orc') return 'orc';
|
||||||
|
if (name === 'Skeleton Archer') return 'skeleton archer';
|
||||||
|
if (name === 'Elite Archer' || name === 'Sniper' || name === 'Archer') return 'archer';
|
||||||
|
if (name === 'Ogre' || name === 'Troll') return 'ogre';
|
||||||
|
if (name === 'Cobra') return 'cobra';
|
||||||
|
if (name === 'Dragon') return 'dragon';
|
||||||
|
if (name === 'The Dungeon Lord') return 'dungeon lord';
|
||||||
|
|
||||||
|
// Town NPCs
|
||||||
|
if (name === 'Peasant' || name.startsWith('Traveler') || name.startsWith('Farmer') || entity instanceof Farmer) {
|
||||||
|
// Consistent but "random" assignment based on coordinates
|
||||||
|
return (entity.x + entity.y) % 2 === 0 ? 'peasant1' : 'peasant2';
|
||||||
|
}
|
||||||
|
if (name === 'Villager' || name === 'Old Man' || name === 'Woman') return 'house questgiver';
|
||||||
|
if (name === 'Priest' || name === 'Father Sun' || name === 'Smith' || name === 'Merchant' || name === 'Mort' || entity instanceof Gravekeeper) return 'mysterious figure';
|
||||||
|
|
||||||
|
// Items
|
||||||
|
if (name === 'Dagger') return 'dagger';
|
||||||
|
if (name.includes('Sword') || name.includes('Blade')) return 'sword';
|
||||||
|
if (name.includes('Axe') || name.includes('Hammer')) return 'axe';
|
||||||
|
if (name === 'Buckler') return 'buckler';
|
||||||
|
if (name === 'Wooden Shield') return 'wooden shield';
|
||||||
|
if (name.includes('Shield')) return 'kite shield';
|
||||||
|
if (entity.type === 'spellbook' || name.includes('Spellbook')) return 'spellbook';
|
||||||
|
|
||||||
|
// Projectiles / Special
|
||||||
|
if (name === 'zappy laser') return 'zappy laser';
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') {
|
drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
|
|
@ -326,7 +615,7 @@ export class UI {
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
let itemY = y + 70;
|
let itemY = y + 70;
|
||||||
|
|
||||||
const list = mode === 'BUY' ? shopkeeper.inventory : player.inventory;
|
const list = mode === 'BUY' ? shopkeeper.inventory : this.game.getGroupedInventory();
|
||||||
|
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
ctx.fillStyle = '#888888';
|
ctx.fillStyle = '#888888';
|
||||||
|
|
@ -335,7 +624,8 @@ export class UI {
|
||||||
} else {
|
} else {
|
||||||
list.forEach((entry, index) => {
|
list.forEach((entry, index) => {
|
||||||
const isSelected = index === selectedIndex;
|
const isSelected = index === selectedIndex;
|
||||||
const item = mode === 'BUY' ? entry.item : entry;
|
const item = mode === 'BUY' ? entry.item : entry.item;
|
||||||
|
const count = mode === 'SELL' ? entry.count : 1;
|
||||||
|
|
||||||
let price = 0;
|
let price = 0;
|
||||||
if (mode === 'BUY') {
|
if (mode === 'BUY') {
|
||||||
|
|
@ -354,11 +644,30 @@ export class UI {
|
||||||
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
||||||
ctx.fillStyle = '#ffff00';
|
ctx.fillStyle = '#ffff00';
|
||||||
ctx.fillText('>', x + 25, itemY + 10);
|
ctx.fillText('>', x + 25, itemY + 10);
|
||||||
|
} else {
|
||||||
|
if (mode === 'SELL') {
|
||||||
|
if (!item.identified) ctx.fillStyle = '#aaaaaa';
|
||||||
|
else if (item.isCursed) ctx.fillStyle = '#ff4444';
|
||||||
|
else if (item.modifier > 0) ctx.fillStyle = '#00ffff';
|
||||||
|
else ctx.fillStyle = '#aaaaaa';
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#aaaaaa';
|
ctx.fillStyle = '#aaaaaa';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Icon if available
|
||||||
|
const assetName = this.getAssetName(item);
|
||||||
|
const asset = this.game.assets[assetName];
|
||||||
|
let itemName = item.getDisplayName ? item.getDisplayName() : item.name;
|
||||||
|
if (count > 1) itemName += ` (${count})`;
|
||||||
|
|
||||||
|
if (asset) {
|
||||||
|
this.ctx.drawImage(asset, x + 50, itemY, 20, 20);
|
||||||
|
this.ctx.fillText(`${itemName}`, x + 80, itemY + 10);
|
||||||
|
} else {
|
||||||
|
this.ctx.fillText(`${itemName}`, x + 50, itemY + 10);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillText(`${item.name}`, x + 50, itemY + 10);
|
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
|
ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
|
|
@ -375,7 +684,7 @@ export class UI {
|
||||||
// Instructions
|
// Instructions
|
||||||
ctx.fillStyle = '#888888';
|
ctx.fillStyle = '#888888';
|
||||||
ctx.font = '12px monospace';
|
ctx.font = '12px monospace';
|
||||||
ctx.fillText('Tab: Toggle Buy/Sell | Up/Down: Select | Enter: Action | Esc: Exit', x + width / 2, y + height - 20);
|
ctx.fillText('Tab: Mode | Up/Down: Sel | Enter: Action | Esc: Exit', x + width / 2, y + height - 20);
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
ctx.textAlign = 'start';
|
ctx.textAlign = 'start';
|
||||||
|
|
|
||||||
28
style.css
|
|
@ -8,8 +8,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #008080; /* Classic teal desktop */
|
background-color: var(--win-bg); /* Match window background */
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Fallback to modern sans */
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -21,15 +21,13 @@ body {
|
||||||
/* Windows 3.1 / 95 Style Window */
|
/* Windows 3.1 / 95 Style Window */
|
||||||
.window {
|
.window {
|
||||||
background-color: var(--win-bg);
|
background-color: var(--win-bg);
|
||||||
border: 2px solid var(--win-white);
|
border: none; /* Remove outer borders for full screen */
|
||||||
border-right-color: var(--win-gray-dark);
|
box-shadow: none;
|
||||||
border-bottom-color: var(--win-gray-dark);
|
padding: 0;
|
||||||
box-shadow: 2px 2px 0 #000;
|
|
||||||
padding: 2px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 900px;
|
width: 100vw;
|
||||||
height: 700px;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar {
|
.title-bar {
|
||||||
|
|
@ -79,10 +77,13 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border: none; /* Remove border for cleaner look */
|
||||||
}
|
}
|
||||||
|
|
||||||
#game-canvas {
|
#game-canvas {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
|
|
@ -113,11 +114,14 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-log {
|
#message-log {
|
||||||
height: 100px;
|
height: 120px;
|
||||||
padding: 5px;
|
padding: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
|
background: #000;
|
||||||
|
color: #00ff00; /* Matrix/Classic terminal style */
|
||||||
|
border: 2px solid var(--win-gray-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
|
|
|
||||||