// Shop page: buy and sell items. let shopCache = null; let currentTab = 'buy'; // ─── API helpers ────────────────────────────────────────────────────────── const api = { get(path) { return fetch(`/api${path}`, { credentials: 'include' }).then(r => r.json()); }, post(path, body) { return fetch(`/api${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), }).then(r => r.json()); }, }; // ─── 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`; } // ─── Render: character info ────────────────────────────────────────────── function renderCharacterInfo(character) { document.getElementById('player-name').textContent = `${character.name} (Lv.${character.level})`; document.getElementById('player-gold').textContent = `💰 ${character.gold}`; } // ─── Render: shop stock (buy tab) ──────────────────────────────────────── function renderShopStock(items) { const container = document.getElementById('shop-stock'); container.innerHTML = ''; const purchasable = items.filter((i) => i.buyPrice > 0); if (purchasable.length === 0) { const p = document.createElement('p'); p.className = 'placeholder'; p.textContent = 'No items available.'; container.appendChild(p); return; } for (const item of purchasable) { const card = document.createElement('div'); card.className = 'shop-item'; const header = document.createElement('div'); header.className = 'shop-item-header'; const icon = document.createElement('span'); icon.className = 'shop-item-icon'; icon.textContent = ITEM_EMOJI[item.type] || '❓'; header.appendChild(icon); const name = document.createElement('span'); name.className = `shop-item-name rarity-${item.rarity}`; name.textContent = item.name; header.appendChild(name); const rarityBadge = document.createElement('span'); rarityBadge.className = `shop-item-rarity bg-rarity-${item.rarity}`; rarityBadge.textContent = item.rarity; header.appendChild(rarityBadge); card.appendChild(header); const stats = document.createElement('div'); stats.className = 'shop-item-stats'; stats.textContent = formatBonuses(item) || formatEffect(item) || '—'; card.appendChild(stats); const meta = document.createElement('div'); meta.className = 'shop-item-meta'; const levelReq = document.createElement('span'); levelReq.className = item.levelOk ? '' : 'shop-item-locked'; levelReq.textContent = item.levelOk ? `Lv.${item.levelReq}` : `Requires Lv.${item.levelReq}`; meta.appendChild(levelReq); const price = document.createElement('span'); price.className = 'shop-item-price'; price.textContent = `${item.buyPrice} G`; meta.appendChild(price); card.appendChild(meta); const buyBtn = document.createElement('button'); buyBtn.className = 'btn btn-shop-buy'; buyBtn.textContent = 'Buy'; buyBtn.disabled = !item.levelOk || !item.canAfford; buyBtn.dataset.itemId = item.itemId; buyBtn.title = !item.levelOk ? `Requires level ${item.levelReq}` : !item.canAfford ? `Need ${item.buyPrice} gold (have ${shopCache?.character?.gold ?? 0})` : ''; card.appendChild(buyBtn); container.appendChild(card); } container.querySelectorAll('.btn-shop-buy').forEach((btn) => { btn.addEventListener('click', handleBuy); }); } // ─── Render: sell list (sell tab) ──────────────────────────────────────── function renderSellList(materials) { const container = document.getElementById('sell-list'); container.innerHTML = ''; if (materials.length === 0) { const p = document.createElement('p'); p.className = 'placeholder'; p.textContent = 'No materials to sell.'; container.appendChild(p); return; } for (const item of materials) { const card = document.createElement('div'); card.className = 'shop-item'; const header = document.createElement('div'); header.className = 'shop-item-header'; const icon = document.createElement('span'); icon.className = 'shop-item-icon'; icon.textContent = ITEM_EMOJI[item.type] || '❓'; header.appendChild(icon); const name = document.createElement('span'); name.className = `shop-item-name rarity-${item.rarity}`; name.textContent = item.name; header.appendChild(name); const rarityBadge = document.createElement('span'); rarityBadge.className = `shop-item-rarity bg-rarity-${item.rarity}`; rarityBadge.textContent = item.rarity; header.appendChild(rarityBadge); card.appendChild(header); const meta = document.createElement('div'); meta.className = 'shop-item-meta'; const sellPrice = document.createElement('span'); sellPrice.className = 'shop-item-sell-price'; sellPrice.textContent = `${item.sellPrice} G each`; meta.appendChild(sellPrice); const qty = document.createElement('span'); qty.className = 'shop-item-qty-label'; qty.textContent = `x${item.quantity}`; meta.appendChild(qty); card.appendChild(meta); const sellBtn = document.createElement('button'); sellBtn.className = 'btn btn-shop-sell'; sellBtn.textContent = 'Sell'; sellBtn.dataset.itemId = item.itemId; card.appendChild(sellBtn); container.appendChild(card); } container.querySelectorAll('.btn-shop-sell').forEach((btn) => { btn.addEventListener('click', handleSell); }); } // ─── Buy handler ───────────────────────────────────────────────────────── async function handleBuy(e) { const btn = e.target; if (btn.disabled) return; const itemId = Number(btn.dataset.itemId); if (isNaN(itemId)) return; const qtyInput = document.getElementById('buy-qty'); const qty = Math.max(1, Math.min(99, parseInt(qtyInput.value, 10) || 1)); btn.disabled = true; btn.textContent = 'Buying...'; try { const data = await api.post('/shop/buy', { itemId, quantity: qty }); if (!data || !data.ok) { showToast(data?.error?.message || 'Purchase failed.', 'error'); return; } shopCache = data.data; renderCharacterInfo(data.data.character); renderShopStock(data.data.stock); showToast(`Bought ${data.data.stock.find(i => i.itemId === itemId)?.name || 'item'}.`, 'info'); } catch (err) { showToast('Purchase failed.', 'error'); } finally { btn.disabled = false; btn.textContent = 'Buy'; } } // ─── Sell handler ──────────────────────────────────────────────────────── async function handleSell(e) { const btn = e.target; if (btn.disabled) return; const itemId = Number(btn.dataset.itemId); if (isNaN(itemId)) return; btn.disabled = true; btn.textContent = 'Selling...'; try { const data = await api.post('/shop/sell', { itemId, quantity: 1 }); if (!data || !data.ok) { showToast(data?.error?.message || 'Sale failed.', 'error'); return; } shopCache = data.data; renderCharacterInfo(data.data.character); renderSellList(data.data.stock.filter((i) => i.buyPrice === 0)); const item = data.data.stock.find((i) => i.itemId === itemId); showToast(`Sold ${item?.name || 'item'}.`, 'info'); } catch (err) { showToast('Sale failed.', 'error'); } finally { btn.disabled = false; btn.textContent = 'Sell'; } } // ─── Tab switching ─────────────────────────────────────────────────────── 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('buy-tab').style.display = tab === 'buy' ? '' : 'none'; document.getElementById('sell-tab').style.display = tab === 'sell' ? '' : 'none'; } // ─── Toast notifications ───────────────────────────────────────────────── function showToast(message, type = 'info') { document.querySelectorAll('.toast').forEach((t) => t.remove()); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 4000); } // ─── Init ──────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { try { const me = await api.get('/auth/me'); if (!me || !me.ok) { window.location.href = '/login.html'; return; } if (!me.data.hasHero) { window.location.href = '/create-hero.html'; return; } const shop = await api.get('/shop'); if (!shop || !shop.ok) { showToast('Failed to load shop.', 'error'); return; } shopCache = shop.data; renderCharacterInfo(shop.data.character); renderShopStock(shop.data.stock); renderSellList(shop.data.stock.filter((i) => i.buyPrice === 0)); } catch (_) { window.location.href = '/login.html'; } document.querySelectorAll('.tab').forEach((tab) => { tab.addEventListener('click', () => switchTab(tab.dataset.tab)); }); }); // Logout handler document.getElementById('logout-btn')?.addEventListener('click', async (e) => { e.preventDefault(); try { await api.post('/auth/logout', {}); } catch (_) {} window.location.href = '/login.html'; });