step 5.4: done

This commit is contained in:
bepis
2026-10-20 23:07:57 +11:00
parent bf3cb35e68
commit ae8ee02721
5 changed files with 540 additions and 23 deletions
+243 -1
View File
@@ -1,8 +1,24 @@
// Game dashboard: load character profile, zones, hunt, and combat log.
// Game dashboard: load character profile, zones, hunt, combat log, and inventory.
let energyInterval = null;
let energyData = null; // store latest server energy data for sync
let isHunting = false;
let currentTab = 'hunt';
let inventoryCache = null;
/**
* Switch active tab in the action panel.
*/
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab').forEach((t) => {
const isActive = t.dataset.tab === tab;
t.classList.toggle('active', isActive);
t.setAttribute('aria-selected', isActive);
});
document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none';
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
}
/**
* Refresh energy state from the server at a fixed interval.
@@ -357,6 +373,226 @@ async function loadCombatLog() {
}
}
// ===== Item display helpers =====
const ITEM_EMOJI = { weapon: '⚔️', armor: '🛡️', trinket: '💍', consumable: '🧪', material: '📦' };
function formatBonuses(bonuses) {
const parts = [];
if (bonuses.bonus_attack) parts.push(`+${bonuses.bonus_attack} ATK`);
if (bonuses.bonus_defense) parts.push(`+${bonuses.bonus_defense} DEF`);
if (bonuses.bonus_str) parts.push(`+${bonuses.bonus_str} STR`);
if (bonuses.bonus_agi) parts.push(`+${bonuses.bonus_agi} AGI`);
if (bonuses.bonus_int) parts.push(`+${bonuses.bonus_int} INT`);
if (bonuses.bonus_vit) parts.push(`+${bonuses.bonus_vit} VIT`);
return parts.join(', ');
}
function formatEffect(item) {
if (!item.effect) return '';
const match = item.effect.match(/^(heal|energy):(\d+)$/);
if (!match) return '';
const [_full, type, value] = match;
return type === 'heal' ? `Heals ${value} HP` : `Restores ${value} Energy`;
}
// ===== Inventory =====
const EQUIP_SLOTS = ['weapon', 'armor', 'trinket'];
async function loadInventory() {
try {
const data = await api.get('/inventory');
if (!data) return;
inventoryCache = data;
renderEquipmentPanel(data.items);
renderInventoryGrid(data.items);
} catch (_) {
// Character doesn't exist yet or not authenticated — skip.
}
}
function renderEquipmentPanel(items) {
const container = document.getElementById('equipment-slots');
container.innerHTML = '';
for (const slot of EQUIP_SLOTS) {
const slotDiv = document.createElement('div');
slotDiv.className = 'equip-slot';
const label = document.createElement('span');
label.className = 'slot-label';
label.textContent = slot;
const equipped = items.find((i) => i.equipped_slot === slot);
const itemInfo = document.createElement('span');
itemInfo.className = 'slot-item';
if (equipped) {
const nameSpan = document.createElement('span');
nameSpan.className = `item-name rarity-${equipped.rarity}`;
nameSpan.textContent = equipped.name;
itemInfo.appendChild(nameSpan);
const statsText = formatBonuses(equipped.bonuses);
if (statsText) {
const statsSpan = document.createElement('span');
statsSpan.className = 'item-stats';
statsSpan.textContent = statsText;
itemInfo.appendChild(statsSpan);
}
const unequipBtn = document.createElement('button');
unequipBtn.className = 'btn-unequip';
unequipBtn.textContent = '✕';
unequipBtn.title = `Unequip ${equipped.name}`;
unequipBtn.addEventListener('click', (e) => {
e.stopPropagation();
handleUnequip(slot);
});
itemInfo.appendChild(unequipBtn);
} else {
const emptySpan = document.createElement('span');
emptySpan.className = 'slot-empty';
emptySpan.textContent = 'Empty';
itemInfo.appendChild(emptySpan);
}
slotDiv.appendChild(label);
slotDiv.appendChild(itemInfo);
container.appendChild(slotDiv);
}
}
function renderInventoryGrid(items) {
const container = document.getElementById('inventory-grid');
container.innerHTML = '';
const unequipped = items.filter((i) => !i.equipped);
if (unequipped.length === 0) {
const p = document.createElement('p');
p.className = 'placeholder';
p.textContent = 'No items.';
container.appendChild(p);
return;
}
for (const item of unequipped) {
const card = document.createElement('div');
card.className = 'inv-item';
// Tooltip (always shown on hover)
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
const tooltipName = document.createElement('div');
tooltipName.className = `tooltip-name rarity-${item.rarity}`;
tooltipName.textContent = item.name;
tooltip.appendChild(tooltipName);
const tooltipType = document.createElement('div');
tooltipType.className = 'tooltip-type';
tooltipType.textContent = `${item.type}${item.quantity > 1 ? ` (x${item.quantity})` : ''}`;
tooltip.appendChild(tooltipType);
const statsText = formatBonuses(item.bonuses);
if (statsText) {
const tooltipStats = document.createElement('div');
tooltipStats.className = 'tooltip-stats';
tooltipStats.textContent = statsText;
tooltip.appendChild(tooltipStats);
}
const effectText = formatEffect(item);
if (effectText) {
const tooltipEffect = document.createElement('div');
tooltipEffect.className = 'tooltip-effect';
tooltipEffect.textContent = effectText;
tooltip.appendChild(tooltipEffect);
}
if (item.min_level > 1) {
const tooltipLevel = document.createElement('div');
tooltipLevel.className = 'tooltip-level';
tooltipLevel.textContent = `Requires Lv. ${item.min_level}`;
tooltip.appendChild(tooltipLevel);
}
card.appendChild(tooltip);
// Compact card content
const header = document.createElement('div');
header.className = 'inv-item-header';
const icon = document.createElement('span');
icon.className = 'inv-item-icon';
icon.textContent = ITEM_EMOJI[item.type] || '❓';
header.appendChild(icon);
const name = document.createElement('span');
name.className = 'inv-item-name rarity-' + item.rarity;
name.textContent = item.name;
header.appendChild(name);
const rarityBadge = document.createElement('span');
rarityBadge.className = `inv-item-rarity bg-rarity-${item.rarity}`;
rarityBadge.textContent = item.rarity;
header.appendChild(rarityBadge);
card.appendChild(header);
const stats = document.createElement('div');
stats.className = 'inv-item-stats';
stats.textContent = formatBonuses(item.bonuses) || formatEffect(item) || '—';
card.appendChild(stats);
if (item.quantity > 1) {
const qty = document.createElement('span');
qty.className = 'inv-item-qty';
qty.textContent = `x${item.quantity}`;
card.appendChild(qty);
}
card.addEventListener('click', () => handleItemClick(item));
container.appendChild(card);
}
}
async function handleItemClick(item) {
try {
let data;
if (item.type === 'consumable') {
data = await api.post('/inventory/use', { itemId: item.itemId });
} else {
data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type });
}
if (!data || !data.ok) {
showToast(data?.error?.message || 'Action failed.', 'error');
return;
}
inventoryCache = data;
renderEquipmentPanel(data.items);
renderInventoryGrid(data.items);
loadCharacter();
} catch (err) {
showToast(err.message || 'Action failed.', 'error');
}
}
async function handleUnequip(slot) {
try {
const data = await api.post('/inventory/unequip', { slot });
if (!data || !data.ok) {
showToast(data?.error?.message || 'Unequip failed.', 'error');
return;
}
inventoryCache = data;
renderEquipmentPanel(data.items);
renderInventoryGrid(data.items);
loadCharacter();
} catch (err) {
showToast(err.message || 'Unequip failed.', 'error');
}
}
// ===== Toast notifications =====
function showToast(message, type = 'info') {
// Remove existing toasts
@@ -377,7 +613,13 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadCharacter();
await loadZones();
await loadCombatLog();
await loadInventory();
startEnergySync();
// Tab click handlers
document.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
});
});
// Logout handler