step 7.3: wip (not ticked)
This commit is contained in:
@@ -76,6 +76,7 @@
|
|||||||
<section class="panel action-panel" id="action-panel">
|
<section class="panel action-panel" id="action-panel">
|
||||||
<div class="tabs" role="tablist">
|
<div class="tabs" role="tablist">
|
||||||
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
|
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
|
||||||
|
<button class="tab" data-tab="expedition" role="tab" aria-selected="false">Expedition</button>
|
||||||
<button class="tab" data-tab="inventory" role="tab" aria-selected="false">Inventory</button>
|
<button class="tab" data-tab="inventory" role="tab" aria-selected="false">Inventory</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="hunt-tab">
|
<div id="hunt-tab">
|
||||||
@@ -83,6 +84,10 @@
|
|||||||
<p class="loading">Loading zones...</p>
|
<p class="loading">Loading zones...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="expedition-tab" style="display:none;">
|
||||||
|
<div id="expedition-status"></div>
|
||||||
|
<div id="expedition-defs" class="expedition-defs"></div>
|
||||||
|
</div>
|
||||||
<div id="inventory-tab" style="display:none;">
|
<div id="inventory-tab" style="display:none;">
|
||||||
<div class="inventory-container">
|
<div class="inventory-container">
|
||||||
<div class="equipment-panel">
|
<div class="equipment-panel">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function switchTab(tab) {
|
|||||||
t.setAttribute('aria-selected', isActive);
|
t.setAttribute('aria-selected', isActive);
|
||||||
});
|
});
|
||||||
document.getElementById('hunt-tab').style.display = tab === 'hunt' ? '' : 'none';
|
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';
|
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 =====
|
// ===== Toast notifications =====
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
// Remove existing toasts
|
// Remove existing toasts
|
||||||
|
|||||||
Reference in New Issue
Block a user