LyssaGame/src/UI.js

384 lines
14 KiB
JavaScript
Raw Normal View History

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');
2025-12-27 00:00:28 +00:00
this.inventoryLink = document.getElementById('inventory-link');
if (this.inventoryLink) {
this.inventoryLink.onclick = (e) => {
e.preventDefault();
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));
const inRadius = dist <= 3.5; // Radius of 3 (using 3.5 for better circle approximation)
if (isLit || inRadius) {
this.drawTile(x * tileSize, y * tileSize, tile, tileSize);
} else {
// Draw nothing or a very dark tile for "unseen" areas
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
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));
if (!(isLit || dist <= 3.5)) continue;
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));
if (!(isLit || dist <= 3.5)) continue;
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) {
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-mana').textContent = `${player.mana}/${player.maxMana}`;
document.getElementById('stat-int').textContent = player.stats.int;
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}) ]`;
}
}
drawInventory(player, selectedIndex) {
const ctx = this.ctx;
const width = 500;
const height = 400;
const x = (ctx.canvas.width - width) / 2;
const y = (ctx.canvas.height - height) / 2;
// Background
ctx.fillStyle = '#000000';
ctx.fillRect(x, y, width, height);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// Title
ctx.fillStyle = '#ffffff';
ctx.font = '20px monospace';
ctx.textAlign = 'center';
ctx.fillText('Inventory', x + width / 2, y + 30);
// Items
ctx.font = '16px monospace';
ctx.textAlign = 'left';
let itemY = y + 70;
if (player.inventory.length === 0) {
ctx.fillStyle = '#888888';
ctx.textAlign = 'center';
ctx.fillText('Empty', x + width / 2, itemY + 20);
} else {
player.inventory.forEach((item, index) => {
const isSelected = index === selectedIndex;
const isEquipped = player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item;
if (isSelected) {
ctx.fillStyle = '#333333';
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
ctx.fillStyle = '#ffff00';
ctx.fillText('>', x + 25, itemY + 10);
} else {
ctx.fillStyle = '#aaaaaa';
}
let displayName = item.name;
if (isEquipped) displayName += " (E)";
ctx.fillText(displayName, x + 50, itemY + 10);
itemY += 30;
});
}
// Instructions
ctx.fillStyle = '#888888';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText('1: Laser | 2: Fireball | 3: Light | 4: Heal | 5: Return | Up/Down: Select | Enter: Use/Equip | Esc/i: Close', x + width / 2, y + height - 20);
// Reset
ctx.textAlign = 'start';
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}`;
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
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;
2025-12-27 00:00:28 +00:00
const list = mode === 'BUY' ? shopkeeper.inventory : player.inventory;
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;
const item = mode === 'BUY' ? entry.item : entry;
let price = 0;
if (mode === 'BUY') {
price = entry.price;
} else {
// Calc sell price
let baseValue = 40;
if (item.type === 'weapon') baseValue = 100 * item.level;
if (item.type === 'shield') baseValue = 40 * item.level;
if (item.type === 'potion') baseValue = 20;
price = Math.floor(baseValue / 4);
}
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 {
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;
});
}
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';
2025-12-27 00:00:28 +00:00
ctx.fillText('Tab: Toggle Buy/Sell | Up/Down: Select | 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
}