2025-11-30 01:33:30 +00:00
|
|
|
export class UI {
|
|
|
|
|
constructor(ctx, game) {
|
|
|
|
|
this.ctx = ctx;
|
|
|
|
|
this.game = game;
|
|
|
|
|
this.messageLog = document.getElementById('message-log');
|
2025-12-25 01:29:57 +00:00
|
|
|
this.messageLog = document.getElementById('message-log');
|
2025-11-30 01:33:30 +00:00
|
|
|
this.inventoryList = document.getElementById('inventory-list');
|
2025-12-25 01:29:57 +00:00
|
|
|
this.spellsList = document.getElementById('spells-list');
|
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];
|
|
|
|
|
this.drawTile(x * tileSize, y * tileSize, tile, tileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw Items
|
|
|
|
|
if (items) {
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
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) {
|
|
|
|
|
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) {
|
|
|
|
|
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) {
|
|
|
|
|
document.getElementById('stat-hp').textContent = player.hp;
|
|
|
|
|
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-gold').textContent = player.gold;
|
|
|
|
|
document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`;
|
|
|
|
|
document.getElementById('stat-int').textContent = player.stats.int;
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateInventory(player) {
|
|
|
|
|
this.inventoryList.innerHTML = '';
|
|
|
|
|
player.inventory.forEach((item, index) => {
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
li.textContent = item.name;
|
|
|
|
|
if (player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item) {
|
|
|
|
|
li.textContent += " (E)";
|
|
|
|
|
}
|
|
|
|
|
li.style.cursor = 'pointer';
|
|
|
|
|
li.onclick = () => {
|
|
|
|
|
this.game.useItem(index);
|
|
|
|
|
};
|
|
|
|
|
this.inventoryList.appendChild(li);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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}`;
|
|
|
|
|
this.spellsList.appendChild(li);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 01:33:30 +00:00
|
|
|
drawTile(screenX, screenY, tile, size) {
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
} else if (tile === '>') {
|
2025-12-25 01:29:57 +00:00
|
|
|
// Stairs Down
|
2025-11-30 01:33:30 +00:00
|
|
|
this.ctx.fillStyle = '#202020';
|
|
|
|
|
this.ctx.fillRect(screenX, screenY, size, size);
|
|
|
|
|
this.ctx.fillStyle = '#ffffff';
|
|
|
|
|
this.ctx.fillText('>', screenX + size / 4, screenY);
|
2025-12-25 01:29:57 +00:00
|
|
|
} 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);
|
2025-11-30 01:33:30 +00:00
|
|
|
} else {
|
|
|
|
|
// Floor
|
|
|
|
|
this.ctx.fillStyle = '#202020';
|
|
|
|
|
this.ctx.fillRect(screenX, screenY, size, size);
|
|
|
|
|
this.ctx.fillStyle = '#404040';
|
|
|
|
|
this.ctx.fillText('.', screenX + size / 4, screenY);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
this.ctx.fillStyle = entity.color;
|
|
|
|
|
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
|
|
|
|
|
}
|
2025-12-25 01:29:57 +00:00
|
|
|
|
|
|
|
|
drawShop(shopkeeper, player, selectedIndex) {
|
|
|
|
|
const ctx = this.ctx;
|
|
|
|
|
const width = 400;
|
|
|
|
|
const height = 300;
|
|
|
|
|
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(shopkeeper.shopName, x + width / 2, y + 30);
|
|
|
|
|
|
|
|
|
|
// Items
|
|
|
|
|
ctx.font = '16px monospace';
|
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
|
let itemY = y + 70;
|
|
|
|
|
|
|
|
|
|
shopkeeper.inventory.forEach((entry, index) => {
|
|
|
|
|
const item = entry.item;
|
|
|
|
|
const price = entry.price;
|
|
|
|
|
const isSelected = index === selectedIndex;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
ctx.fillStyle = '#333333';
|
|
|
|
|
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
|
|
|
|
ctx.fillStyle = '#ffff00';
|
|
|
|
|
ctx.fillText('>', x + 25, itemY + 10);
|
|
|
|
|
} else {
|
|
|
|
|
ctx.fillStyle = '#aaaaaa';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.fillText(`${item.name}`, x + 50, itemY + 10);
|
|
|
|
|
ctx.textAlign = 'right';
|
|
|
|
|
ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
|
|
|
|
|
ctx.textAlign = 'left';
|
|
|
|
|
|
|
|
|
|
itemY += 30;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Player Gold
|
|
|
|
|
ctx.fillStyle = '#ffd700';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 30);
|
|
|
|
|
|
|
|
|
|
// Instructions
|
|
|
|
|
ctx.fillStyle = '#888888';
|
|
|
|
|
ctx.font = '12px monospace';
|
|
|
|
|
ctx.fillText('Up/Down: Select | Enter: Buy | Esc: Exit', x + width / 2, y + height - 10);
|
|
|
|
|
|
|
|
|
|
// Reset
|
|
|
|
|
ctx.textAlign = 'start';
|
|
|
|
|
}
|
2025-11-30 01:33:30 +00:00
|
|
|
}
|