2026-01-01 03:42:46 +00:00
|
|
|
import { Farmer, Gravekeeper } from './Entity.js';
|
|
|
|
|
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
export class UI {
|
|
|
|
|
constructor(ctx, game) {
|
2025-12-27 00:00:28 +00:00
|
|
|
console.log("UI Version 1.1 Loaded");
|
2025-11-30 01:33:30 +00:00
|
|
|
this.ctx = ctx;
|
|
|
|
|
this.game = game;
|
|
|
|
|
this.messageLog = document.getElementById('message-log');
|
2025-12-25 01:29:57 +00:00
|
|
|
this.spellsList = document.getElementById('spells-list');
|
2026-01-01 03:42:46 +00:00
|
|
|
this.inventoryLink = docugment.getElementById('inventory-link');
|
|
|
|
|
this.inventoryLabel = document.querySelector('#inventory-panel .group-box-label');
|
2025-12-27 00:00:28 +00:00
|
|
|
|
|
|
|
|
if (this.inventoryLink) {
|
|
|
|
|
this.inventoryLink.onclick = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.game.toggleInventory();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
if (this.inventoryLabel) {
|
|
|
|
|
this.inventoryLabel.style.cursor = 'pointer';
|
|
|
|
|
this.inventoryLabel.onclick = () => this.game.toggleInventory();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
this.popup = null;
|
|
|
|
|
this.popupTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(message) {
|
|
|
|
|
if (!this.messageLog) return;
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = message;
|
|
|
|
|
this.messageLog.appendChild(div);
|
|
|
|
|
this.messageLog.scrollTop = this.messageLog.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 01:29:57 +00:00
|
|
|
renderMap(map, player, monsters, items, projectiles, tileSize) {
|
2025-11-30 01:33:30 +00:00
|
|
|
// Fill background
|
|
|
|
|
this.ctx.fillStyle = '#000000';
|
|
|
|
|
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
|
|
|
|
|
|
|
|
this.ctx.font = `${tileSize}px monospace`;
|
|
|
|
|
this.ctx.textBaseline = 'top';
|
|
|
|
|
|
|
|
|
|
// Calculate viewport (center on player)
|
|
|
|
|
const viewWidth = Math.ceil(this.ctx.canvas.width / tileSize);
|
|
|
|
|
const viewHeight = Math.ceil(this.ctx.canvas.height / tileSize);
|
|
|
|
|
|
|
|
|
|
const startX = player.x - Math.floor(viewWidth / 2);
|
|
|
|
|
const startY = player.y - Math.floor(viewHeight / 2);
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < viewHeight; y++) {
|
|
|
|
|
for (let x = 0; x < viewWidth; x++) {
|
|
|
|
|
const mapX = startX + x;
|
|
|
|
|
const mapY = startY + y;
|
|
|
|
|
|
|
|
|
|
if (mapX >= 0 && mapX < map.width && mapY >= 0 && mapY < map.height) {
|
|
|
|
|
const tile = map.tiles[mapY][mapX];
|
2025-12-27 00:00:28 +00:00
|
|
|
const isLit = map.isLit(mapX, mapY);
|
|
|
|
|
const dist = Math.sqrt(Math.pow(player.x - mapX, 2) + Math.pow(player.y - mapY, 2));
|
2026-01-01 03:42:46 +00:00
|
|
|
const inRadius = dist <= 3.5;
|
|
|
|
|
const isExplored = map.explored[mapY][mapX];
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-12-27 00:00:28 +00:00
|
|
|
} else {
|
2026-01-01 03:42:46 +00:00
|
|
|
// Draw nothing for unseen areas
|
2025-12-27 00:00:28 +00:00
|
|
|
this.ctx.fillStyle = '#000000';
|
|
|
|
|
this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
|
|
|
|
}
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
// Draw Items
|
|
|
|
|
if (items) {
|
|
|
|
|
for (const item of items) {
|
2025-12-27 00:00:28 +00:00
|
|
|
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));
|
2026-01-01 03:42:46 +00:00
|
|
|
const inRadius = dist <= 3.5;
|
|
|
|
|
|
|
|
|
|
if (!(isLit || inRadius)) continue;
|
|
|
|
|
if (!map.hasLineOfSight(player.x, player.y, item.x, item.y)) continue;
|
2025-12-27 00:00:28 +00:00
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
const screenX = (item.x - startX) * tileSize;
|
|
|
|
|
const screenY = (item.y - startY) * tileSize;
|
|
|
|
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
|
|
|
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
|
|
|
|
this.drawEntity(screenX, screenY, item, tileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw Monsters
|
|
|
|
|
for (const monster of monsters) {
|
2025-12-27 00:00:28 +00:00
|
|
|
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));
|
2026-01-01 03:42:46 +00:00
|
|
|
const inRadius = dist <= 3.5;
|
|
|
|
|
|
|
|
|
|
if (!(isLit || inRadius)) continue;
|
|
|
|
|
if (!map.hasLineOfSight(player.x, player.y, monster.x, monster.y)) continue;
|
2025-12-27 00:00:28 +00:00
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
const screenX = (monster.x - startX) * tileSize;
|
|
|
|
|
const screenY = (monster.y - startY) * tileSize;
|
|
|
|
|
// Only draw if visible
|
|
|
|
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
|
|
|
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
|
|
|
|
this.drawEntity(screenX, screenY, monster, tileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 01:29:57 +00:00
|
|
|
// Draw Projectiles
|
|
|
|
|
if (projectiles) {
|
|
|
|
|
for (const proj of projectiles) {
|
2025-12-27 00:00:28 +00:00
|
|
|
const isLit = map.isLit(proj.x, proj.y);
|
|
|
|
|
const dist = Math.sqrt(Math.pow(player.x - proj.x, 2) + Math.pow(player.y - proj.y, 2));
|
|
|
|
|
if (!(isLit || dist <= 3.5)) continue;
|
|
|
|
|
|
2025-12-25 01:29:57 +00:00
|
|
|
const screenX = (proj.x - startX) * tileSize;
|
|
|
|
|
const screenY = (proj.y - startY) * tileSize;
|
|
|
|
|
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
|
|
|
|
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
|
|
|
|
this.drawEntity(screenX, screenY, proj, tileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
// Draw Player
|
|
|
|
|
const playerScreenX = (player.x - startX) * tileSize;
|
|
|
|
|
const playerScreenY = (player.y - startY) * tileSize;
|
|
|
|
|
|
|
|
|
|
this.drawEntity(playerScreenX, playerScreenY, player, tileSize);
|
|
|
|
|
|
|
|
|
|
this.drawPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateStats(player, depth) {
|
2026-01-01 03:42:46 +00:00
|
|
|
const hpElem = document.getElementById('stat-hp');
|
|
|
|
|
hpElem.textContent = player.hp;
|
|
|
|
|
hpElem.style.color = player.poisoned ? '#00ff00' : '#ffffff';
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
document.getElementById('stat-max-hp').textContent = player.maxHp;
|
|
|
|
|
document.getElementById('stat-str').textContent = player.stats.str;
|
|
|
|
|
document.getElementById('stat-level').textContent = player.level;
|
|
|
|
|
document.getElementById('stat-xp').textContent = player.xp;
|
|
|
|
|
document.getElementById('stat-max-xp').textContent = player.maxXp;
|
|
|
|
|
document.getElementById('stat-ac').textContent = player.getDefense();
|
|
|
|
|
document.getElementById('stat-floor').textContent = depth;
|
|
|
|
|
document.getElementById('stat-gold').textContent = player.gold;
|
2025-12-25 01:29:57 +00:00
|
|
|
document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`;
|
|
|
|
|
document.getElementById('stat-int').textContent = player.stats.int;
|
2026-01-01 03:42:46 +00:00
|
|
|
|
|
|
|
|
const statusElem = document.getElementById('stat-status');
|
|
|
|
|
if (statusElem) {
|
|
|
|
|
statusElem.textContent = player.poisoned ? "POISONED" : "";
|
|
|
|
|
}
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateInventory(player) {
|
2025-12-27 00:00:28 +00:00
|
|
|
// Now handled by canvas-based inventory box
|
|
|
|
|
if (this.inventoryLink) {
|
|
|
|
|
this.inventoryLink.textContent = `[ View Inventory (${player.inventory.length}) ]`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-27 00:00:28 +00:00
|
|
|
drawInventory(player, selectedIndex) {
|
|
|
|
|
const ctx = this.ctx;
|
|
|
|
|
const width = 500;
|
|
|
|
|
const height = 400;
|
|
|
|
|
const x = (ctx.canvas.width - width) / 2;
|
|
|
|
|
const y = (ctx.canvas.height - height) / 2;
|
|
|
|
|
|
|
|
|
|
// Background
|
|
|
|
|
ctx.fillStyle = '#000000';
|
|
|
|
|
ctx.fillRect(x, y, width, height);
|
|
|
|
|
ctx.strokeStyle = '#ffffff';
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.strokeRect(x, y, width, height);
|
|
|
|
|
|
|
|
|
|
// Title
|
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
|
|
|
ctx.font = '20px monospace';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.fillText('Inventory', x + width / 2, y + 30);
|
|
|
|
|
|
|
|
|
|
// Items
|
|
|
|
|
ctx.font = '16px monospace';
|
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
|
let itemY = y + 70;
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
const groupedItems = this.game.getGroupedInventory();
|
|
|
|
|
|
|
|
|
|
if (groupedItems.length === 0) {
|
2025-12-27 00:00:28 +00:00
|
|
|
ctx.fillStyle = '#888888';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.fillText('Empty', x + width / 2, itemY + 20);
|
|
|
|
|
} else {
|
2026-01-01 03:42:46 +00:00
|
|
|
groupedItems.forEach((group, index) => {
|
2025-12-27 00:00:28 +00:00
|
|
|
const isSelected = index === selectedIndex;
|
2026-01-01 03:42:46 +00:00
|
|
|
const isEquipped = group.indices.some(idx => {
|
|
|
|
|
const item = player.inventory[idx];
|
|
|
|
|
return player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item;
|
|
|
|
|
});
|
2025-12-27 00:00:28 +00:00
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
ctx.fillStyle = '#333333';
|
|
|
|
|
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
|
|
|
|
ctx.fillStyle = '#ffff00';
|
|
|
|
|
ctx.fillText('>', x + 25, itemY + 10);
|
|
|
|
|
} else {
|
2026-01-01 03:42:46 +00:00
|
|
|
// 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';
|
|
|
|
|
}
|
2025-12-27 00:00:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
let displayName = group.item.getDisplayName();
|
|
|
|
|
if (group.count > 1) displayName += ` (${group.count})`;
|
2025-12-27 00:00:28 +00:00
|
|
|
if (isEquipped) displayName += " (E)";
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-27 00:00:28 +00:00
|
|
|
itemY += 30;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Instructions
|
|
|
|
|
ctx.fillStyle = '#888888';
|
|
|
|
|
ctx.font = '12px monospace';
|
|
|
|
|
ctx.textAlign = 'center';
|
2026-01-01 03:42:46 +00:00
|
|
|
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);
|
2025-12-27 00:00:28 +00:00
|
|
|
|
|
|
|
|
// Reset
|
|
|
|
|
ctx.textAlign = 'start';
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-25 01:29:57 +00:00
|
|
|
updateSpells(player) {
|
|
|
|
|
if (!this.spellsList) return;
|
|
|
|
|
this.spellsList.innerHTML = '';
|
|
|
|
|
player.spells.forEach((spell, index) => {
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
li.textContent = `${index + 1}. ${spell}`;
|
2026-01-01 03:42:46 +00:00
|
|
|
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();
|
|
|
|
|
};
|
2025-12-25 01:29:57 +00:00
|
|
|
this.spellsList.appendChild(li);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
drawTile(screenX, screenY, tile, size, isMemorized = false) {
|
|
|
|
|
const ctx = this.ctx;
|
2025-11-30 01:33:30 +00:00
|
|
|
if (tile === '#') {
|
2026-01-01 03:42:46 +00:00
|
|
|
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);
|
2025-11-30 01:33:30 +00:00
|
|
|
|
|
|
|
|
} else if (tile === '>') {
|
2025-12-25 01:29:57 +00:00
|
|
|
// Stairs Down
|
2026-01-01 03:42:46 +00:00
|
|
|
ctx.fillStyle = '#202020';
|
|
|
|
|
ctx.fillRect(screenX, screenY, size, size);
|
|
|
|
|
ctx.fillStyle = isMemorized ? '#888888' : '#ffffff';
|
|
|
|
|
ctx.fillText('>', screenX + size / 4, screenY);
|
2025-12-25 01:29:57 +00:00
|
|
|
} else if (tile === '<') {
|
|
|
|
|
// Stairs Up
|
2026-01-01 03:42:46 +00:00
|
|
|
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);
|
2025-11-30 01:33:30 +00:00
|
|
|
} else {
|
|
|
|
|
// Floor
|
2026-01-01 03:42:46 +00:00
|
|
|
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);
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showPopup(text, duration) {
|
|
|
|
|
this.popup = text;
|
|
|
|
|
if (this.popupTimeout) clearTimeout(this.popupTimeout);
|
|
|
|
|
this.game.render();
|
|
|
|
|
|
|
|
|
|
this.popupTimeout = setTimeout(() => {
|
|
|
|
|
this.popup = null;
|
|
|
|
|
this.game.render();
|
|
|
|
|
}, duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawPopup() {
|
|
|
|
|
if (!this.popup) return;
|
|
|
|
|
|
|
|
|
|
const ctx = this.ctx;
|
|
|
|
|
ctx.font = '16px "Courier New", monospace';
|
|
|
|
|
const textMetrics = ctx.measureText(this.popup);
|
|
|
|
|
const textWidth = textMetrics.width;
|
|
|
|
|
|
|
|
|
|
const width = textWidth + 40; // Add padding
|
|
|
|
|
const height = 50;
|
|
|
|
|
const x = (this.ctx.canvas.width - width) / 2;
|
|
|
|
|
const y = (this.ctx.canvas.height - height) / 2 - 50; // Slightly above center
|
|
|
|
|
|
|
|
|
|
// Background
|
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
|
|
|
ctx.fillRect(x, y, width, height);
|
|
|
|
|
|
|
|
|
|
// Border
|
|
|
|
|
ctx.strokeStyle = '#000000';
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.strokeRect(x, y, width, height);
|
|
|
|
|
|
|
|
|
|
// Text
|
|
|
|
|
ctx.fillStyle = '#000000';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.textBaseline = 'middle';
|
|
|
|
|
ctx.fillText(this.popup, x + width / 2, y + height / 2);
|
|
|
|
|
|
|
|
|
|
// Reset text align
|
|
|
|
|
ctx.textAlign = 'start';
|
|
|
|
|
ctx.textBaseline = 'alphabetic';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawEntity(screenX, screenY, entity, size) {
|
2026-01-01 03:42:46 +00:00
|
|
|
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;
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
2025-12-25 01:29:57 +00:00
|
|
|
|
2025-12-27 00:00:28 +00:00
|
|
|
drawShop(shopkeeper, player, selectedIndex, mode = 'BUY') {
|
2025-12-25 01:29:57 +00:00
|
|
|
const ctx = this.ctx;
|
2025-12-27 00:00:28 +00:00
|
|
|
const width = 500;
|
|
|
|
|
const height = 400;
|
2025-12-25 01:29:57 +00:00
|
|
|
const x = (ctx.canvas.width - width) / 2;
|
|
|
|
|
const y = (ctx.canvas.height - height) / 2;
|
|
|
|
|
|
|
|
|
|
// Background
|
|
|
|
|
ctx.fillStyle = '#000000';
|
|
|
|
|
ctx.fillRect(x, y, width, height);
|
|
|
|
|
ctx.strokeStyle = '#ffffff';
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.strokeRect(x, y, width, height);
|
|
|
|
|
|
|
|
|
|
// Title
|
|
|
|
|
ctx.fillStyle = '#ffffff';
|
|
|
|
|
ctx.font = '20px monospace';
|
|
|
|
|
ctx.textAlign = 'center';
|
2025-12-27 00:00:28 +00:00
|
|
|
ctx.fillText(`${shopkeeper.shopName} - ${mode}`, x + width / 2, y + 30);
|
2025-12-25 01:29:57 +00:00
|
|
|
|
|
|
|
|
// Items
|
|
|
|
|
ctx.font = '16px monospace';
|
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
|
let itemY = y + 70;
|
|
|
|
|
|
2026-01-01 03:42:46 +00:00
|
|
|
const list = mode === 'BUY' ? shopkeeper.inventory : this.game.getGroupedInventory();
|
2025-12-25 01:29:57 +00:00
|
|
|
|
2025-12-27 00:00:28 +00:00
|
|
|
if (list.length === 0) {
|
|
|
|
|
ctx.fillStyle = '#888888';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.fillText(mode === 'BUY' ? 'Sold Out' : 'Nothing to sell', x + width / 2, itemY + 20);
|
|
|
|
|
} else {
|
|
|
|
|
list.forEach((entry, index) => {
|
|
|
|
|
const isSelected = index === selectedIndex;
|
2026-01-01 03:42:46 +00:00
|
|
|
const item = mode === 'BUY' ? entry.item : entry.item;
|
|
|
|
|
const count = mode === 'SELL' ? entry.count : 1;
|
2025-12-27 00:00:28 +00:00
|
|
|
|
|
|
|
|
let price = 0;
|
|
|
|
|
if (mode === 'BUY') {
|
|
|
|
|
price = entry.price;
|
|
|
|
|
} else {
|
|
|
|
|
// Calc sell price
|
|
|
|
|
let baseValue = 40;
|
|
|
|
|
if (item.type === 'weapon') baseValue = 100 * item.level;
|
|
|
|
|
if (item.type === 'shield') baseValue = 40 * item.level;
|
|
|
|
|
if (item.type === 'potion') baseValue = 20;
|
|
|
|
|
price = Math.floor(baseValue / 4);
|
|
|
|
|
}
|
2025-12-25 01:29:57 +00:00
|
|
|
|
2025-12-27 00:00:28 +00:00
|
|
|
if (isSelected) {
|
|
|
|
|
ctx.fillStyle = '#333333';
|
|
|
|
|
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
|
|
|
|
ctx.fillStyle = '#ffff00';
|
|
|
|
|
ctx.fillText('>', x + 25, itemY + 10);
|
|
|
|
|
} else {
|
2026-01-01 03:42:46 +00:00
|
|
|
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);
|
2025-12-27 00:00:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.textAlign = 'right';
|
|
|
|
|
ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
|
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
|
|
|
|
|
|
itemY += 30;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-25 01:29:57 +00:00
|
|
|
|
|
|
|
|
// Player Gold
|
|
|
|
|
ctx.fillStyle = '#ffd700';
|
|
|
|
|
ctx.textAlign = 'center';
|
2025-12-27 00:00:28 +00:00
|
|
|
ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 50);
|
2025-12-25 01:29:57 +00:00
|
|
|
|
|
|
|
|
// Instructions
|
|
|
|
|
ctx.fillStyle = '#888888';
|
|
|
|
|
ctx.font = '12px monospace';
|
2026-01-01 03:42:46 +00:00
|
|
|
ctx.fillText('Tab: Mode | Up/Down: Sel | Enter: Action | Esc: Exit', x + width / 2, y + height - 20);
|
2025-12-25 01:29:57 +00:00
|
|
|
|
|
|
|
|
// Reset
|
|
|
|
|
ctx.textAlign = 'start';
|
|
|
|
|
}
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|