step 7.3: wip (not ticked)
This commit is contained in:
@@ -17,6 +17,7 @@ function switchTab(tab) {
|
||||
t.setAttribute('aria-selected', isActive);
|
||||
});
|
||||
document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none';
|
||||
document.getElementById('expedition-tab').style.display = tab === 'expedition' ? '' : 'none';
|
||||
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
|
||||
}
|
||||
|
||||
@@ -593,6 +594,196 @@ async function handleUnequip(slot) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Expedition UI =====
|
||||
let expeditionInterval = null;
|
||||
|
||||
function formatTimeRemaining(ms) {
|
||||
if (ms <= 0) return 'Finished';
|
||||
const totalSec = Math.ceil(ms / 1000);
|
||||
const mins = Math.floor(totalSec / 60);
|
||||
const secs = totalSec % 60;
|
||||
if (mins >= 60) {
|
||||
const hours = Math.floor(mins / 60);
|
||||
const remMins = mins % 60;
|
||||
return `${hours}h ${remMins}m`;
|
||||
}
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatDuration(sec) {
|
||||
if (sec < 3600) return `${sec / 60} min`;
|
||||
const hours = sec / 3600;
|
||||
if (hours < 24) return `${hours}h`;
|
||||
return `${hours / 24}d`;
|
||||
}
|
||||
|
||||
async function loadExpeditions() {
|
||||
try {
|
||||
const data = await api.get('/expeditions/active');
|
||||
if (!data || !data.ok) return;
|
||||
|
||||
const statusEl = document.getElementById('expedition-status');
|
||||
const defsEl = document.getElementById('expedition-defs');
|
||||
|
||||
if (data.data) {
|
||||
renderActiveExpedition(statusEl, defsEl, data.data);
|
||||
} else {
|
||||
statusEl.innerHTML = '';
|
||||
renderExpeditionDefs(defsEl);
|
||||
}
|
||||
} catch (_) {
|
||||
const statusEl = document.getElementById('expedition-status');
|
||||
statusEl.innerHTML = '<p class="placeholder">Failed to load expedition data.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveExpedition(statusEl, defsEl, expeditionData) {
|
||||
defsEl.innerHTML = '';
|
||||
|
||||
const { expedition, definition, timeRemaining, isFinished } = expeditionData;
|
||||
const elapsed = (definition.durationSec * 1000) - timeRemaining;
|
||||
const pct = Math.min(100, Math.max(0, (elapsed / (definition.durationSec * 1000)) * 100));
|
||||
|
||||
let html = `<div class="expedition-active-card">`;
|
||||
html += `<div class="expedition-name">${definition.name}</div>`;
|
||||
html += `<div class="expedition-desc">${definition.description}</div>`;
|
||||
|
||||
// Progress bar
|
||||
html += `<div class="expedition-progress">`;
|
||||
html += `<div class="expedition-progress-bar">`;
|
||||
html += `<div class="expedition-progress-fill" style="width:${pct}%"></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="expedition-progress-text">${formatTimeRemaining(timeRemaining)}</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
if (isFinished) {
|
||||
html += `<button class="btn btn-expedition btn-claim" data-expedition-id="${expedition.id}">Claim Rewards</button>`;
|
||||
html += `<div class="expedition-rewards-preview">Rewards: ${definition.xpReward} XP, ${definition.goldMin}–${definition.goldMax} Gold</div>`;
|
||||
} else {
|
||||
html += `<div class="expedition-info">Duration: ${formatDuration(definition.durationSec)} · Cost: ${definition.energyCost}⚡</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
statusEl.innerHTML = html;
|
||||
|
||||
// Attach claim handler
|
||||
const claimBtn = statusEl.querySelector('.btn-claim');
|
||||
if (claimBtn) {
|
||||
claimBtn.addEventListener('click', handleClaimExpedition);
|
||||
}
|
||||
}
|
||||
|
||||
function renderExpeditionDefs(container) {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Fetch expedition definitions from the server
|
||||
api.get('/expeditions/defs').then(defsData => {
|
||||
if (!defsData || !defsData.ok || !defsData.data) return;
|
||||
|
||||
const defs = defsData.data;
|
||||
for (const def of defs) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'expedition-def-card';
|
||||
|
||||
const nameEl = document.createElement('div');
|
||||
nameEl.className = 'expedition-name';
|
||||
nameEl.textContent = def.name;
|
||||
|
||||
const descEl = document.createElement('div');
|
||||
descEl.className = 'expedition-desc';
|
||||
descEl.textContent = def.description;
|
||||
|
||||
const infoEl = document.createElement('div');
|
||||
infoEl.className = 'expedition-info';
|
||||
infoEl.textContent = `${formatDuration(def.durationSec)} · ${def.energyCost}⚡ · Lv${def.min_level}+`;
|
||||
|
||||
const rewardsEl = document.createElement('div');
|
||||
rewardsEl.className = 'expedition-rewards';
|
||||
rewardsEl.textContent = `+${def.xpReward} XP, ${def.goldMin}–${def.gold_max} Gold`;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-expedition';
|
||||
btn.textContent = `Start Expedition (${def.energyCost}⚡)`;
|
||||
btn.disabled = window.heroLevel < def.min_level;
|
||||
btn.dataset.defId = def.id;
|
||||
|
||||
if (window.heroLevel < def.min_level) {
|
||||
btn.title = `Requires level ${def.min_level}`;
|
||||
}
|
||||
|
||||
card.appendChild(nameEl);
|
||||
card.appendChild(descEl);
|
||||
card.appendChild(infoEl);
|
||||
card.appendChild(rewardsEl);
|
||||
card.appendChild(btn);
|
||||
container.appendChild(card);
|
||||
|
||||
btn.addEventListener('click', handleStartExpedition);
|
||||
}
|
||||
}).catch(() => {
|
||||
container.innerHTML = '<p class="placeholder">Failed to load expedition options.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
async function handleStartExpedition(e) {
|
||||
const btn = e.target;
|
||||
const defId = Number(btn.dataset.defId);
|
||||
if (isNaN(defId)) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Starting...';
|
||||
|
||||
try {
|
||||
const data = await api.post('/expeditions/start', { defId });
|
||||
if (!data || !data.ok) {
|
||||
showToast(data?.error?.message || 'Failed to start expedition.', 'error');
|
||||
return;
|
||||
}
|
||||
showToast('Expedition started!', 'info');
|
||||
loadExpeditions();
|
||||
loadCharacter(); // refresh energy
|
||||
} catch (err) {
|
||||
showToast('Failed to start expedition.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClaimExpedition(e) {
|
||||
const btn = e.target;
|
||||
const expeditionId = Number(btn.dataset.expeditionId);
|
||||
if (isNaN(expeditionId)) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Claiming...';
|
||||
|
||||
try {
|
||||
const data = await api.post('/expeditions/claim', { expeditionId });
|
||||
if (!data || !data.ok) {
|
||||
showToast(data?.error?.message || 'Failed to claim expedition.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const { xpGained, goldGained } = data.data;
|
||||
showToast(`Expedition complete! +${xpGained} XP, +${goldGained} Gold`, 'info');
|
||||
loadExpeditions();
|
||||
loadCharacter(); // refresh gold/XP
|
||||
} catch (err) {
|
||||
showToast('Failed to claim expedition.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically refresh expedition progress while on the expedition tab.
|
||||
* Updates the progress bar and time remaining every 5 seconds.
|
||||
*/
|
||||
function startExpeditionSync() {
|
||||
if (expeditionInterval) clearInterval(expeditionInterval);
|
||||
expeditionInterval = setInterval(() => {
|
||||
if (currentTab === 'expedition') {
|
||||
loadExpeditions();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ===== Toast notifications =====
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
|
||||
Reference in New Issue
Block a user