import { Farmer, Gravekeeper } from './Entity.js'; export class UI { constructor(ctx, game) { console.log("UI Version 1.1 Loaded"); this.ctx = ctx; this.game = game; this.messageLog = document.getElementById('message-log'); this.spellsList = document.getElementById('spells-list'); this.inventoryLink = docugment.getElementById('inventory-link'); this.inventoryLabel = document.querySelector('#inventory-panel .group-box-label'); if (this.inventoryLink) { this.inventoryLink.onclick = (e) => { e.preventDefault(); this.game.toggleInventory(); }; } if (this.inventoryLabel) { this.inventoryLabel.style.cursor = 'pointer'; this.inventoryLabel.onclick = () => this.game.toggleInventory(); } 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; } renderMap(map, player, monsters, items, projectiles, tileSize) { // 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]; 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; 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); } else { // Draw nothing for unseen areas this.ctx.fillStyle = '#000000'; this.ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); } } } } // 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)); 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; 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 isLit = map.isLit(monster.x, monster.y); const dist = Math.sqrt(Math.pow(player.x - monster.x, 2) + Math.pow(player.y - monster.y, 2)); const 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; // 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); } } // Draw Projectiles if (projectiles) { for (const proj of projectiles) { 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; 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); } } } // 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) { 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; 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; 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) { // Now handled by canvas-based inventory box if (this.inventoryLink) { this.inventoryLink.textContent = `[ View Inventory (${player.inventory.length}) ]`; } } 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; 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; const groupedItems = this.game.getGroupedInventory(); if (groupedItems.length === 0) { ctx.fillStyle = '#888888'; ctx.textAlign = 'center'; ctx.fillText('Empty', x + width / 2, itemY + 20); } else { groupedItems.forEach((group, index) => { const isSelected = index === selectedIndex; 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'; ctx.fillRect(x + 20, itemY - 5, width - 40, 25); ctx.fillStyle = '#ffff00'; ctx.fillText('>', x + 25, itemY + 10); } else { // Color based on quality if identified if (!group.item.identified) { ctx.fillStyle = '#aaaaaa'; // Gray for unidentified } else if (group.item.isCursed) { ctx.fillStyle = '#ff4444'; // Red for cursed } else if (group.item.modifier > 0) { ctx.fillStyle = '#00ffff'; // Cyan for enchanted } else { ctx.fillStyle = '#aaaaaa'; } } let displayName = group.item.getDisplayName(); if (group.count > 1) displayName += ` (${group.count})`; if (isEquipped) displayName += " (E)"; // 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; }); } // Instructions ctx.fillStyle = '#888888'; ctx.font = '12px monospace'; ctx.textAlign = 'center'; 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'; } updateSpells(player) { if (!this.spellsList) return; this.spellsList.innerHTML = ''; 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, isMemorized = false) { const ctx = this.ctx; if (tile === '#') { 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 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 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 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); } } 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) { 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') { 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(`${shopkeeper.shopName} - ${mode}`, x + width / 2, y + 30); // Items ctx.font = '16px monospace'; ctx.textAlign = 'left'; let itemY = y + 70; const list = mode === 'BUY' ? shopkeeper.inventory : this.game.getGroupedInventory(); 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.item; const count = mode === 'SELL' ? entry.count : 1; 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); } if (isSelected) { ctx.fillStyle = '#333333'; ctx.fillRect(x + 20, itemY - 5, width - 40, 25); ctx.fillStyle = '#ffff00'; ctx.fillText('>', x + 25, itemY + 10); } else { if (mode === 'SELL') { if (!item.identified) ctx.fillStyle = '#aaaaaa'; else if (item.isCursed) ctx.fillStyle = '#ff4444'; else if (item.modifier > 0) ctx.fillStyle = '#00ffff'; else ctx.fillStyle = '#aaaaaa'; } else { 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.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 - 50); // Instructions ctx.fillStyle = '#888888'; ctx.font = '12px monospace'; ctx.fillText('Tab: Mode | Up/Down: Sel | Enter: Action | Esc: Exit', x + width / 2, y + height - 20); // Reset ctx.textAlign = 'start'; } }