-
+
+
diff --git a/src/Entity.js b/src/Entity.js
index e034f5b..f65be10 100644
--- a/src/Entity.js
+++ b/src/Entity.js
@@ -32,6 +32,7 @@ export class Player extends Entity {
this.xp = 0;
this.maxXp = 10;
this.gold = 0;
+ this.poisoned = false;
}
gainXp(amount) {
@@ -55,7 +56,7 @@ export class Player extends Entity {
let dmg = Math.floor(this.stats.str / 5); // Base damage
if (dmg < 1) dmg = 1;
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;
}
@@ -63,7 +64,7 @@ export class Player extends Entity {
getDefense() {
let def = 0;
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
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 {
constructor(x, y, name, shopName, inventory) {
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 }
}
}
+
+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)];
+ }
+}
diff --git a/src/Game.js b/src/Game.js
index 0680a5b..ac59b42 100644
--- a/src/Game.js
+++ b/src/Game.js
@@ -2,14 +2,24 @@ import { Map } from './Map.js';
import { UI } from './UI.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';
export class Game {
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.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.player = new Player(10, 10);
this.monsters = [];
@@ -21,8 +31,33 @@ export class Game {
this.dungeonLevels = {}; // Persistent storage for each depth
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.shopSelection = 0;
this.shopMode = 'BUY'; // 'BUY' or 'SELL'
@@ -45,6 +80,90 @@ export class Game {
// Bind input
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() {
@@ -65,8 +184,8 @@ export class Game {
// Reposition player
if (this.depth === 0) {
if (direction === 'up') {
- this.player.x = 43;
- this.player.y = 8;
+ this.player.x = 63;
+ this.player.y = 13;
} else {
this.player.x = Math.floor(this.map.width / 2);
this.player.y = Math.floor(this.map.height / 2);
@@ -87,7 +206,7 @@ export class Game {
} else {
// Generate NEW level
this.items = [];
- this.map = new Map(50, 50);
+ this.map = new Map(this.mapWidth, this.mapHeight);
if (this.depth === 0) {
this.map.generateTown();
@@ -119,6 +238,7 @@ export class Game {
}
this.spawnMonsters(); // Always respawn monsters
+ this.updateExplored();
this.render();
this.ui.updateStats(this.player, this.depth);
@@ -169,8 +289,8 @@ export class Game {
spawnStairs() {
if (this.depth === 0) {
// Town: Stairs Down in the Dungeon Entrance building
- const sx = 43; // Center X
- const sy = 8; // Center Y
+ const sx = 63; // Center X
+ const sy = 13; // Center Y
this.map.tiles[sy][sx] = '>';
return;
}
@@ -214,24 +334,29 @@ export class Game {
{ item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 },
{ item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 }
];
- const weaponSmith = new Shopkeeper(41, 18, "Smith", "Weapon Shop", weaponShopInventory);
+ weaponShopInventory.forEach(entry => entry.item.identified = true);
+ const weaponSmith = new Shopkeeper(59, 33, "Smith", "Weapon Shop", weaponShopInventory);
this.monsters.push(weaponSmith);
// General Store (West)
const generalStoreInventory = [
{ 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: Light", "spellbook", 1, { spell: "Light" }), price: 200 },
{ item: new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" }), price: 400 },
- { item: new Item("Spellbook: Return", "spellbook", 1, { spell: "Return" }), price: 500 }
+ { 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);
// Spawn Quest Givers in Houses
- const q1 = new QuestGiver(13, 32, "Villager", "Please bring back my Golden Locket! It was stolen by an orc!");
- const q2 = new QuestGiver(25, 32, "Old Man", "Please bring back my Silver Chalice! It was stolen by an orc!");
- const q3 = new QuestGiver(37, 32, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!");
+ const q1 = new QuestGiver(23, 57, "Villager", "Please bring back my Golden Locket! 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(57, 57, "Woman", "Please bring back my Ruby Ring! It was stolen by an orc!");
this.monsters.push(q1, q2, q3);
// Spawn Wandering Quest Givers
@@ -247,6 +372,16 @@ export class Game {
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;
}
@@ -273,7 +408,8 @@ export class Game {
else monster = new Archer(mx, my, "Skeleton Archer", "s", "#eeeeee", 3);
} else if (this.depth <= 7) {
if (type < 0.2) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2);
- else if (type < 0.5) monster = new Monster(mx, my, "Orc", "o", "#008000", 3);
+ else if (type < 0.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 monster = new Archer(mx, my, "Elite Archer", "S", "#ffffff", 5);
} else {
@@ -287,6 +423,7 @@ export class Game {
// Set Monster Speeds
if (monster.name === "Rat") 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 instanceof Archer) monster.speed = 0.7;
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 (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) {
if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 });
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) {
// Spellbooks
const spellRoll = Math.random();
- if (spellRoll < 0.25) {
+ if (spellRoll < 0.16) {
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" });
- } else if (spellRoll < 0.75) {
+ } else if (spellRoll < 0.48) {
item = new Item("Spellbook: Heal", "spellbook", 1, { spell: "Heal" });
- } else {
+ } else if (spellRoll < 0.64) {
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 {
item = new Item("Potion", "potion", 1, { heal: 5 + this.depth * 2 });
@@ -363,6 +505,20 @@ export class Game {
item.x = ix;
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);
}
}
@@ -371,6 +527,12 @@ export class Game {
handleInput(e) {
if (this.gameOver) return;
+ if (this.gameState === 'INTRO') {
+ this.gameState = 'PLAY';
+ this.render();
+ return;
+ }
+
if (this.gameState === 'SHOP') {
this.handleShopInput(e);
return;
@@ -381,6 +543,14 @@ export class Game {
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') {
this.handleTargetingInput(e);
return;
@@ -405,6 +575,11 @@ export class Game {
case 'NumPad3': dx = 1; dy = 1; handled = true; break;
case '.': case 'NumPad5': handled = true; break; // Wait
case 'i': case 'I': this.toggleInventory(); handled = true; break;
+ case 'm': case 'M':
+ this.gameState = 'OVERVIEW';
+ this.render();
+ handled = true;
+ break;
case 'Enter':
case '>':
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!");
handled = true;
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) {
@@ -465,18 +650,18 @@ export class Game {
handleShopInput(e) {
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) {
case 'ArrowUp':
case 'NumPad8':
this.shopSelection--;
- if (this.shopSelection < 0) this.shopSelection = inventory.length - 1;
+ if (this.shopSelection < 0) this.shopSelection = list.length - 1;
break;
case 'ArrowDown':
case 'NumPad2':
this.shopSelection++;
- if (this.shopSelection >= inventory.length) this.shopSelection = 0;
+ if (this.shopSelection >= list.length) this.shopSelection = 0;
break;
case 'Tab':
this.shopMode = this.shopMode === 'BUY' ? 'SELL' : 'BUY';
@@ -485,9 +670,12 @@ export class Game {
case 'Enter':
case ' ':
if (this.shopMode === 'BUY') {
- this.buyItem(inventory[this.shopSelection]);
+ this.buyItem(list[this.shopSelection]);
} else {
- this.sellItem(this.shopSelection);
+ if (list[this.shopSelection]) {
+ const actualIndex = list[this.shopSelection].indices[0];
+ this.sellItem(actualIndex);
+ }
}
break;
case 'Escape':
@@ -522,25 +710,141 @@ export class Game {
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) {
e.preventDefault();
- const inventory = this.player.inventory;
+ const groupedItems = this.getGroupedInventory();
switch (e.key) {
case 'ArrowUp':
case 'NumPad8':
this.inventorySelection--;
- if (this.inventorySelection < 0) this.inventorySelection = inventory.length - 1;
+ if (this.inventorySelection < 0) this.inventorySelection = groupedItems.length - 1;
break;
case 'ArrowDown':
case 'NumPad2':
this.inventorySelection++;
- if (this.inventorySelection >= inventory.length) this.inventorySelection = 0;
+ if (this.inventorySelection >= groupedItems.length) this.inventorySelection = 0;
break;
case 'Enter':
case ' ':
- if (inventory[this.inventorySelection]) {
- this.useItem(this.inventorySelection);
+ if (groupedItems[this.inventorySelection]) {
+ const actualIndex = groupedItems[this.inventorySelection].indices[0];
+ this.useItem(actualIndex);
}
break;
case 'Escape':
@@ -561,9 +865,10 @@ export class Game {
// Copy visual props
newItem.symbol = entry.item.symbol;
newItem.color = entry.item.color;
+ newItem.identified = true; // Shop items are identified
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.updateStats(this.player, this.depth);
} else {
@@ -581,8 +886,15 @@ export class Game {
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)
- // We'll estimate price based on item level/type if it doesn't have a value
let baseValue = 40; // Default
if (item.type === 'weapon') baseValue = 100 * item.level;
if (item.type === 'shield') baseValue = 40 * item.level;
@@ -598,7 +910,7 @@ export class Game {
this.player.gold += sellPrice;
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
if (this.shopSelection >= this.player.inventory.length) {
@@ -655,6 +967,49 @@ export class Game {
this.ui.showPopup(msg, 2000);
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 {
this.attack(this.player, targetMonster);
}
@@ -662,21 +1017,56 @@ export class Game {
this.player.x = newX;
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
const item = this.items.find(i => i.x === newX && i.y === newY);
if (item) {
this.ui.log(`You see a ${item.name}. (Press 'g' to get)`);
}
} else {
- console.log(`Blocked at ${newX},${newY}. Tile: '${this.map.tiles[newY][newX]}'`);
- if (dx !== 0 || dy !== 0) this.ui.log("Blocked!");
+ // Quietly blocked
}
+ this.updateExplored();
+
// Temple Healing (Town only)
- if (this.depth === 0 && this.player.x === 25 && this.player.y === 12) {
- if (this.player.hp < this.player.maxHp) {
+ if (this.depth === 0 && this.player.x === 40 && this.player.y === 14) {
+ if (this.player.hp < this.player.maxHp || this.player.poisoned) {
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);
} else {
this.ui.log("You enter the temple. It is peaceful here.");
@@ -790,6 +1180,7 @@ export class Game {
}
}
}
+ this.updateExplored();
this.render();
}
@@ -841,6 +1232,55 @@ export class Game {
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) {
const interval = setInterval(() => {
const newX = projectile.x + projectile.dx;
@@ -930,6 +1370,7 @@ export class Game {
itemData.dropped = true;
const questItem = new Item(itemData.name, "quest", 1, {});
+ questItem.identified = true;
questItem.x = monster.x;
questItem.y = monster.y;
this.items.push(questItem);
@@ -943,6 +1384,7 @@ export class Game {
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
if (monster.name === "The Dungeon Lord") {
const map = new Item("Ancient Map", "map", 1, {});
+ map.identified = true;
map.x = monster.x;
map.y = monster.y;
this.items.push(map);
@@ -998,18 +1440,55 @@ export class Game {
if (!item) return;
if (item.type === 'weapon') {
- this.player.equipment.weapon = item;
- this.ui.log(`You equipped ${item.name}.`);
+ 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.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') {
- this.player.equipment.shield = item;
- this.ui.log(`You equipped ${item.name}.`);
+ 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.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') {
if (item.stats.heal) {
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.`);
- // Remove potion
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') {
const spell = item.stats.spell;
if (this.player.spells.includes(spell)) {
@@ -1029,7 +1508,7 @@ export class Game {
attack(attacker, defender, presetDamage = null) {
// 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
if (defender.name === "Peasant") {
@@ -1062,6 +1541,15 @@ export class Game {
defender.hp -= 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 === this.player) {
this.ui.log("GAME OVER");
@@ -1075,7 +1563,7 @@ export class Game {
updateMonsters() {
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?
if (monster.speed !== undefined && Math.random() > monster.speed) continue;
@@ -1100,12 +1588,12 @@ export class Game {
const dy = this.player.y - monster.y;
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
if (monster instanceof Archer && dist > 1.5 && dist <= monster.range) {
const isOrthogonal = this.player.x === monster.x || this.player.y === monster.y;
const isDiagonal = Math.abs(dx) === Math.abs(dy);
- if (isOrthogonal || isDiagonal) {
+ if ((isOrthogonal || isDiagonal) && this.map.hasLineOfSight(monster.x, monster.y, this.player.x, this.player.y)) {
this.monsterShoot(monster);
continue;
}
@@ -1148,6 +1636,14 @@ export class Game {
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);
}
}
diff --git a/src/Item.js b/src/Item.js
index 92201f6..507c44c 100644
--- a/src/Item.js
+++ b/src/Item.js
@@ -1,7 +1,7 @@
export class Item {
constructor(name, type, level, stats) {
this.name = name;
- this.type = type; // 'weapon', 'armor', 'potion'
+ this.type = type; // 'weapon', 'armor', 'shield', 'potion', 'spellbook', 'antidote', 'map', 'quest'
this.level = level;
this.stats = stats || {}; // { damage: 5 } or { defense: 2 }
this.x = 0;
@@ -9,12 +9,38 @@ export class Item {
this.symbol = '?';
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 === 'armor') this.symbol = '[';
if (type === 'shield') 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 === 'quest') { this.symbol = '*'; this.color = '#ff00ff'; }
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;
+ }
}
diff --git a/src/Map.js b/src/Map.js
index d164fae..b1f7eeb 100644
--- a/src/Map.js
+++ b/src/Map.js
@@ -5,23 +5,33 @@ export class Map {
this.tiles = [];
this.rooms = [];
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() {
this.isTown = false;
this.rooms = [];
this.permanentlyLit = [];
+ this.explored = [];
+ this.traps = [];
+ this.fountains = [];
+ this.signs = [];
// Initialize with walls and dark
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
this.permanentlyLit[y] = [];
+ this.explored[y] = [];
for (let x = 0; x < this.width; x++) {
this.tiles[y][x] = '#';
this.permanentlyLit[y][x] = false;
+ this.explored[y][x] = false;
}
}
- const MAX_ROOMS = 10;
+ const MAX_ROOMS = 30;
const MIN_SIZE = 6;
const MAX_SIZE = 12;
@@ -63,19 +73,51 @@ export class Map {
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() {
this.isTown = true;
this.rooms = [];
this.permanentlyLit = [];
+ this.explored = [];
+ this.traps = [];
+ this.fountains = [];
+ this.signs = [];
// Fill with grass/floor
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
this.permanentlyLit[y] = [];
+ this.explored[y] = [];
for (let x = 0; x < this.width; x++) {
this.tiles[y][x] = '.';
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)
- 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)
- 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)
- 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)
- drawBuilding(40, 5, 6, 6, 'bottom');
+ drawBuilding(60, 10, 6, 6, 'bottom');
// 5. Houses (South)
- drawBuilding(10, 30, 6, 5, 'top');
- drawBuilding(22, 30, 6, 5, 'top');
- drawBuilding(34, 30, 6, 5, 'top');
+ drawBuilding(20, 55, 6, 5, 'top');
+ drawBuilding(37, 55, 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] === '<';
}
+ 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) {
// Town is always fully lit
if (this.isTown) return true;
diff --git a/src/UI.js b/src/UI.js
index dacf267..4393110 100644
--- a/src/UI.js
+++ b/src/UI.js
@@ -1,3 +1,6 @@
+import { Farmer, Gravekeeper } from './Entity.js';
+
+
export class UI {
constructor(ctx, game) {
console.log("UI Version 1.1 Loaded");
@@ -5,7 +8,8 @@ export class UI {
this.game = game;
this.messageLog = document.getElementById('message-log');
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) {
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.popupTimeout = null;
}
@@ -50,12 +59,18 @@ export class UI {
const tile = map.tiles[mapY][mapX];
const isLit = map.isLit(mapX, mapY);
const dist = Math.sqrt(Math.pow(player.x - mapX, 2) + Math.pow(player.y - mapY, 2));
- const inRadius = dist <= 3.5; // Radius of 3 (using 3.5 for better circle approximation)
+ const inRadius = dist <= 3.5;
+ const isExplored = map.explored[mapY][mapX];
- if (isLit || inRadius) {
- this.drawTile(x * tileSize, y * tileSize, tile, tileSize);
+ // Tiles are visible if lit, in radius, or remembered
+ 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 {
- // Draw nothing or a very dark tile for "unseen" areas
+ // Draw nothing for unseen areas
this.ctx.fillStyle = '#000000';
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
if (items) {
for (const item of items) {
const isLit = map.isLit(item.x, item.y);
const dist = Math.sqrt(Math.pow(player.x - item.x, 2) + Math.pow(player.y - item.y, 2));
- if (!(isLit || dist <= 3.5)) continue;
+ const 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 screenY = (item.y - startY) * tileSize;
@@ -83,7 +177,10 @@ export class UI {
for (const monster of monsters) {
const isLit = map.isLit(monster.x, monster.y);
const dist = Math.sqrt(Math.pow(player.x - monster.x, 2) + Math.pow(player.y - monster.y, 2));
- if (!(isLit || dist <= 3.5)) continue;
+ const 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 screenY = (monster.y - startY) * tileSize;
@@ -120,7 +217,10 @@ export class UI {
}
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-str').textContent = player.stats.str;
document.getElementById('stat-level').textContent = player.level;
@@ -131,6 +231,11 @@ export class UI {
document.getElementById('stat-gold').textContent = player.gold;
document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`;
document.getElementById('stat-int').textContent = player.stats.int;
+
+ const statusElem = document.getElementById('stat-status');
+ if (statusElem) {
+ statusElem.textContent = player.poisoned ? "POISONED" : "";
+ }
}
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) {
const ctx = this.ctx;
const width = 500;
@@ -165,14 +370,19 @@ export class UI {
ctx.textAlign = 'left';
let itemY = y + 70;
- if (player.inventory.length === 0) {
+ const groupedItems = this.game.getGroupedInventory();
+
+ if (groupedItems.length === 0) {
ctx.fillStyle = '#888888';
ctx.textAlign = 'center';
ctx.fillText('Empty', x + width / 2, itemY + 20);
} else {
- player.inventory.forEach((item, index) => {
+ groupedItems.forEach((group, index) => {
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) {
ctx.fillStyle = '#333333';
@@ -180,13 +390,32 @@ export class UI {
ctx.fillStyle = '#ffff00';
ctx.fillText('>', x + 25, itemY + 10);
} else {
- ctx.fillStyle = '#aaaaaa';
+ // 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 {
+ ctx.fillStyle = '#aaaaaa';
+ }
}
- let displayName = item.name;
+ let displayName = group.item.getDisplayName();
+ if (group.count > 1) displayName += ` (${group.count})`;
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;
});
}
@@ -195,7 +424,7 @@ export class UI {
ctx.fillStyle = '#888888';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
- ctx.fillText('1: Laser | 2: Fireball | 3: Light | 4: Heal | 5: Return | Up/Down: Select | Enter: Use/Equip | Esc/i: Close', x + width / 2, y + height - 20);
+ 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
ctx.textAlign = 'start';
@@ -207,48 +436,63 @@ export class UI {
player.spells.forEach((spell, index) => {
const li = document.createElement('li');
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);
});
}
- drawTile(screenX, screenY, tile, size) {
+ drawTile(screenX, screenY, tile, size, isMemorized = false) {
+ const ctx = this.ctx;
if (tile === '#') {
- this.ctx.fillStyle = '#808080'; // Gray wall
- this.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';
- this.ctx.beginPath();
- this.ctx.moveTo(screenX + size, screenY);
- this.ctx.lineTo(screenX + size, screenY + size);
- this.ctx.lineTo(screenX, screenY + size);
- this.ctx.stroke();
+ ctx.fillStyle = isMemorized ? '#404040' : '#808080'; // Darker gray for memorized walls
+ ctx.fillRect(screenX, screenY, size, size);
+
+ // Wall highlights
+ ctx.strokeStyle = isMemorized ? '#606060' : '#ffffff';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(screenX + 1, screenY + 1, size - 2, size - 2);
} else if (tile === '>') {
// Stairs Down
- this.ctx.fillStyle = '#202020';
- this.ctx.fillRect(screenX, screenY, size, size);
- this.ctx.fillStyle = '#ffffff';
- this.ctx.fillText('>', screenX + size / 4, screenY);
+ ctx.fillStyle = '#202020';
+ ctx.fillRect(screenX, screenY, size, size);
+ ctx.fillStyle = isMemorized ? '#888888' : '#ffffff';
+ ctx.fillText('>', screenX + size / 4, screenY);
} else if (tile === '<') {
// Stairs Up
- this.ctx.fillStyle = '#202020';
- this.ctx.fillRect(screenX, screenY, size, size);
- this.ctx.fillStyle = '#ffffff';
- this.ctx.fillText('<', screenX + size / 4, screenY);
+ ctx.fillStyle = '#202020';
+ ctx.fillRect(screenX, screenY, size, size);
+ ctx.fillStyle = isMemorized ? '#888888' : '#ffffff';
+ 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 {
// Floor
- this.ctx.fillStyle = '#202020';
- this.ctx.fillRect(screenX, screenY, size, size);
- this.ctx.fillStyle = '#404040';
- this.ctx.fillText('.', screenX + size / 4, screenY);
+ ctx.fillStyle = '#202020';
+ ctx.fillRect(screenX, screenY, size, size);
+ ctx.fillStyle = isMemorized ? '#222222' : '#404040'; // Very dark for memorized floor
+ ctx.fillText('.', screenX + size / 4, screenY);
}
}
@@ -297,8 +541,53 @@ export class UI {
}
drawEntity(screenX, screenY, entity, size) {
- this.ctx.fillStyle = entity.color;
- this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
+ 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.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') {
@@ -326,7 +615,7 @@ export class UI {
ctx.textAlign = 'left';
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) {
ctx.fillStyle = '#888888';
@@ -335,7 +624,8 @@ export class UI {
} else {
list.forEach((entry, index) => {
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;
if (mode === 'BUY') {
@@ -355,10 +645,29 @@ export class UI {
ctx.fillStyle = '#ffff00';
ctx.fillText('>', x + 25, itemY + 10);
} else {
- ctx.fillStyle = '#aaaaaa';
+ 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 {
+ 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.fillText(`${price}g`, x + width - 50, itemY + 10);
ctx.textAlign = 'left';
@@ -375,7 +684,7 @@ export class UI {
// Instructions
ctx.fillStyle = '#888888';
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
ctx.textAlign = 'start';
diff --git a/style.css b/style.css
index 44cdf08..585582a 100644
--- a/style.css
+++ b/style.css
@@ -8,8 +8,8 @@
}
body {
- background-color: #008080; /* Classic teal desktop */
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Fallback to modern sans */
+ background-color: var(--win-bg); /* Match window background */
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
@@ -21,15 +21,13 @@ body {
/* Windows 3.1 / 95 Style Window */
.window {
background-color: var(--win-bg);
- border: 2px solid var(--win-white);
- border-right-color: var(--win-gray-dark);
- border-bottom-color: var(--win-gray-dark);
- box-shadow: 2px 2px 0 #000;
- padding: 2px;
+ border: none; /* Remove outer borders for full screen */
+ box-shadow: none;
+ padding: 0;
display: flex;
flex-direction: column;
- width: 900px;
- height: 700px;
+ width: 100vw;
+ height: 100vh;
}
.title-bar {
@@ -79,10 +77,13 @@ body {
display: flex;
justify-content: center;
align-items: center;
+ border: none; /* Remove border for cleaner look */
}
#game-canvas {
image-rendering: pixelated;
+ width: 100%;
+ height: 100%;
}
#sidebar {
@@ -113,11 +114,14 @@ body {
}
#message-log {
- height: 100px;
- padding: 5px;
+ height: 120px;
+ padding: 10px;
overflow-y: auto;
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 {