step 5.4: done
This commit is contained in:
@@ -290,6 +290,256 @@ button:hover { background: #c73650; }
|
||||
color: #eadfce;
|
||||
}
|
||||
|
||||
/* ===== Tabs ===== */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border, #3a2f27);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: transparent;
|
||||
color: var(--muted, #a59a8a);
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text, #eadfce);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--accent, #e2822b);
|
||||
border-bottom-color: var(--accent, #e2822b);
|
||||
}
|
||||
|
||||
/* ===== Inventory layout ===== */
|
||||
.inventory-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.inventory-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Equipment panel ===== */
|
||||
.equipment-panel h4 {
|
||||
color: var(--accent, #e2822b);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.equipment-slots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.equip-slot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slot-label {
|
||||
color: var(--muted, #a59a8a);
|
||||
text-transform: capitalize;
|
||||
min-width: 2.5em;
|
||||
}
|
||||
|
||||
.slot-item {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.slot-item .item-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.slot-item .item-stats {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted, #a59a8a);
|
||||
}
|
||||
|
||||
.btn-unequip {
|
||||
background: transparent;
|
||||
color: var(--danger, #c0392b);
|
||||
border: 1px solid var(--danger, #c0392b);
|
||||
padding: 0.15rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-unequip:hover {
|
||||
background: var(--danger, #c0392b);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.slot-empty {
|
||||
color: var(--muted, #a59a8a);
|
||||
font-style: italic;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ===== Inventory grid ===== */
|
||||
.inventory-grid-panel h4 {
|
||||
color: var(--accent, #e2822b);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 0.5rem;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.inv-item {
|
||||
background: #0f3460;
|
||||
border: 1px solid var(--border, #3a2f27);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.inv-item:hover {
|
||||
border-color: var(--accent, #e2822b);
|
||||
}
|
||||
|
||||
.inv-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.inv-item-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.inv-item-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.inv-item-rarity {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inv-item-stats {
|
||||
font-size: 0.7rem;
|
||||
color: var(--success, #4f9d69);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.inv-item-type {
|
||||
font-size: 0.65rem;
|
||||
color: var(--muted, #a59a8a);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.inv-item-qty {
|
||||
position: absolute;
|
||||
bottom: 0.3rem;
|
||||
right: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted, #a59a8a);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Rarity colors */
|
||||
.rarity-common { color: #b8b0a3; }
|
||||
.rarity-uncommon { color: #5fb36b; }
|
||||
.rarity-rare { color: #4a8fe0; }
|
||||
.rarity-epic { color: #a45ee0; }
|
||||
.rarity-legendary { color: #f2a23a; }
|
||||
|
||||
.bg-rarity-common { background: rgba(184, 176, 163, 0.2); color: #b8b0a3; }
|
||||
.bg-rarity-uncommon { background: rgba(95, 179, 107, 0.2); color: #5fb36b; }
|
||||
.bg-rarity-rare { background: rgba(74, 143, 224, 0.2); color: #4a8fe0; }
|
||||
.bg-rarity-epic { background: rgba(164, 94, 224, 0.2); color: #a45ee0; }
|
||||
.bg-rarity-legendary { background: rgba(242, 162, 58, 0.2); color: #f2a23a; }
|
||||
|
||||
/* ===== Tooltip ===== */
|
||||
.inv-item .tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1a1a2e;
|
||||
border: 1px solid var(--border, #3a2f27);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
min-width: 170px;
|
||||
max-width: 220px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.inv-item:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tooltip-name {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.tooltip-type {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted, #a59a8a);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.tooltip-stats {
|
||||
font-size: 0.75rem;
|
||||
color: var(--success, #4f9d69);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tooltip-effect {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent, #e2822b);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.tooltip-level {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted, #a59a8a);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* ===== Toast notifications ===== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
|
||||
+23
-4
@@ -72,11 +72,30 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Action panel -->
|
||||
<!-- Action panel (tabs: Hunt / Inventory) -->
|
||||
<section class="panel action-panel" id="action-panel">
|
||||
<h3>Hunt</h3>
|
||||
<div id="zone-list" class="zone-list">
|
||||
<p class="loading">Loading zones...</p>
|
||||
<div class="tabs" role="tablist">
|
||||
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
|
||||
<button class="tab" data-tab="inventory" role="tab" aria-selected="false">Inventory</button>
|
||||
</div>
|
||||
<div id="hunt-tab">
|
||||
<div id="zone-list" class="zone-list">
|
||||
<p class="loading">Loading zones...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inventory-tab" style="display:none;">
|
||||
<div class="inventory-container">
|
||||
<div class="equipment-panel">
|
||||
<h4>Equipment</h4>
|
||||
<div id="equipment-slots" class="equipment-slots"></div>
|
||||
</div>
|
||||
<div class="inventory-grid-panel">
|
||||
<h4>Items</h4>
|
||||
<div id="inventory-grid" class="inventory-grid">
|
||||
<p class="loading">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+243
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user