step 4.5: done
This commit is contained in:
@@ -174,3 +174,147 @@ button:hover { background: #c73650; }
|
||||
margin-top: 0.25rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ===== Zone cards ===== */
|
||||
.zone-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.zone-card {
|
||||
background: #0f3460;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.zone-card .zone-name {
|
||||
font-weight: bold;
|
||||
color: #e94560;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.zone-card .zone-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #a0a0a0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.zone-card .zone-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.zone-card .zone-locked-msg {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.btn-hunt {
|
||||
margin-top: 0.5rem;
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-hunt:hover:not(:disabled) {
|
||||
background: #c73650;
|
||||
}
|
||||
|
||||
.btn-hunt:disabled {
|
||||
background: #444;
|
||||
color: #888;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ===== Combat log ===== */
|
||||
.combat-log-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background: #0f3460;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.log-result {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
min-width: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-result.win {
|
||||
color: #4f9d69;
|
||||
}
|
||||
|
||||
.log-result.loss {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
color: #eadfce;
|
||||
}
|
||||
|
||||
/* ===== Result panel ===== */
|
||||
.result-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.result-panel h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.result-panel p {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.2rem 0;
|
||||
color: #eadfce;
|
||||
}
|
||||
|
||||
/* ===== Toast notifications ===== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
color: #fff;
|
||||
z-index: 1000;
|
||||
animation: toastIn 0.3s ease;
|
||||
max-width: 90vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: #4a7fb0;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(1rem); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
+18
-2
@@ -74,8 +74,24 @@
|
||||
|
||||
<!-- Action panel -->
|
||||
<section class="panel action-panel" id="action-panel">
|
||||
<h3>Actions</h3>
|
||||
<p class="placeholder">Hunting and other actions will appear here in the next phase.</p>
|
||||
<h3>Hunt</h3>
|
||||
<div id="zone-list" class="zone-list">
|
||||
<p class="loading">Loading zones...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Combat log panel -->
|
||||
<section class="panel combat-panel" id="combat-panel">
|
||||
<h3>Combat Log</h3>
|
||||
<div id="combat-log-list" class="combat-log-list">
|
||||
<p class="loading">Loading...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Result panel -->
|
||||
<section class="panel result-panel" id="result-panel" style="display:none;">
|
||||
<h3 id="result-title"></h3>
|
||||
<div id="result-body"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+257
-6
@@ -1,7 +1,8 @@
|
||||
// Game dashboard: load character profile and populate stats.
|
||||
// Game dashboard: load character profile, zones, hunt, and combat log.
|
||||
|
||||
let energyInterval = null;
|
||||
let energyData = null; // store latest server energy data for sync
|
||||
let isHunting = false;
|
||||
|
||||
/**
|
||||
* Refresh energy state from the server at a fixed interval.
|
||||
@@ -98,7 +99,7 @@ async function loadCharacter() {
|
||||
document.getElementById('char-name').textContent = character.name;
|
||||
document.getElementById('char-class').textContent = character.class.charAt(0).toUpperCase() + character.class.slice(1);
|
||||
document.getElementById('char-level').textContent = character.level;
|
||||
document.getElementById('char-hp').textContent = `${character.hp_current} / ${derived.maxHp}`;
|
||||
document.getElementById('char-hp').textContent = `${character.hp_current} / ${character.maxHp}`;
|
||||
document.getElementById('char-energy').textContent = `${energy.current} / ${energy.max}`;
|
||||
document.getElementById('char-gold').textContent = character.gold;
|
||||
document.getElementById('char-xp').textContent = `${character.xp} / ${xpToNext(character.level)}`;
|
||||
@@ -124,8 +125,260 @@ function xpToNext(level) {
|
||||
return Math.floor(100 * Math.pow(level, 1.5));
|
||||
}
|
||||
|
||||
// Start periodic energy sync from server (every 60s)
|
||||
startEnergySync();
|
||||
// ===== Zone list =====
|
||||
async function loadZones() {
|
||||
const zoneList = document.getElementById('zone-list');
|
||||
try {
|
||||
const data = await api.get('/zones');
|
||||
if (!data || !data.ok) {
|
||||
zoneList.innerHTML = '';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'placeholder';
|
||||
p.textContent = 'Failed to load zones.';
|
||||
zoneList.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
const zones = data.data;
|
||||
zoneList.innerHTML = '';
|
||||
|
||||
for (const zone of zones) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'zone-card';
|
||||
|
||||
const zoneName = document.createElement('div');
|
||||
zoneName.className = 'zone-name';
|
||||
zoneName.textContent = zone.name;
|
||||
|
||||
const zoneDesc = document.createElement('div');
|
||||
zoneDesc.className = 'zone-desc';
|
||||
zoneDesc.textContent = zone.description;
|
||||
|
||||
const zoneMeta = document.createElement('div');
|
||||
zoneMeta.className = 'zone-meta';
|
||||
zoneMeta.textContent = `Lv ${zone.min_level}+ · ${zone.energy_cost}⚡`;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-hunt';
|
||||
btn.textContent = `Hunt (${zone.energy_cost}⚡)`;
|
||||
btn.disabled = !zone.unlocked;
|
||||
btn.dataset.zoneId = zone.id;
|
||||
|
||||
if (!zone.unlocked) {
|
||||
const lockMsg = document.createElement('div');
|
||||
lockMsg.className = 'zone-locked-msg';
|
||||
lockMsg.textContent = `Reach level ${zone.min_level}`;
|
||||
card.appendChild(zoneName);
|
||||
card.appendChild(lockMsg);
|
||||
} else {
|
||||
card.appendChild(zoneName);
|
||||
card.appendChild(zoneDesc);
|
||||
card.appendChild(zoneMeta);
|
||||
card.appendChild(btn);
|
||||
}
|
||||
|
||||
zoneList.appendChild(card);
|
||||
}
|
||||
|
||||
// Attach hunt handlers
|
||||
zoneList.querySelectorAll('.btn-hunt').forEach((btn) => {
|
||||
btn.addEventListener('click', handleHunt);
|
||||
});
|
||||
} catch (_) {
|
||||
zoneList.innerHTML = '';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'placeholder';
|
||||
p.textContent = 'Failed to load zones.';
|
||||
zoneList.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Hunt =====
|
||||
async function handleHunt(e) {
|
||||
if (isHunting) return;
|
||||
const btn = e.target;
|
||||
const zoneId = Number(btn.dataset.zoneId);
|
||||
if (isNaN(zoneId)) return;
|
||||
|
||||
isHunting = true;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Hunting...';
|
||||
|
||||
// Hide previous result
|
||||
document.getElementById('result-panel').style.display = 'none';
|
||||
|
||||
try {
|
||||
const data = await api.post('/hunt', { zoneId });
|
||||
if (!data || !data.ok) {
|
||||
showToast(data?.error?.message || 'Hunt failed.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = data.data;
|
||||
displayHuntResult(result);
|
||||
loadCharacter();
|
||||
loadCombatLog();
|
||||
loadZones(); // refresh energy cost buttons
|
||||
} catch (err) {
|
||||
showToast('Hunt failed.', 'error');
|
||||
} finally {
|
||||
isHunting = false;
|
||||
// Re-enable all hunt buttons
|
||||
document.querySelectorAll('.btn-hunt').forEach((b) => {
|
||||
b.disabled = false;
|
||||
// Restore text from zone card labels
|
||||
const zoneCard = b.closest('.zone-card');
|
||||
if (zoneCard) {
|
||||
const meta = zoneCard.querySelector('.zone-meta');
|
||||
if (meta) {
|
||||
const cost = meta.textContent.match(/(\d+)⚡/);
|
||||
if (cost) b.textContent = `Hunt (${cost[1]}⚡)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Display hunt result =====
|
||||
function displayHuntResult(result) {
|
||||
const panel = document.getElementById('result-panel');
|
||||
const title = document.getElementById('result-title');
|
||||
const body = document.getElementById('result-body');
|
||||
|
||||
panel.style.display = 'block';
|
||||
|
||||
const won = result.winner === 'hero';
|
||||
title.textContent = won ? 'Victory!' : 'Defeat';
|
||||
title.style.color = won ? '' : '';
|
||||
|
||||
body.innerHTML = '';
|
||||
|
||||
// Monster info
|
||||
const monsterLine = document.createElement('p');
|
||||
monsterLine.textContent = `Fought: ${result.monster.name} (Lv.${result.monster.level})`;
|
||||
body.appendChild(monsterLine);
|
||||
|
||||
// Rewards
|
||||
const rewards = document.createElement('p');
|
||||
rewards.textContent = [];
|
||||
const parts = [];
|
||||
if (result.xpGained) parts.push(`+${result.xpGained} XP`);
|
||||
if (result.goldGained) parts.push(`+${result.goldGained} Gold`);
|
||||
if (parts.length) rewards.textContent = parts.join(', ');
|
||||
if (result.loot && result.loot.length > 0) {
|
||||
const lootParts = result.loot.map((l) => `${l.name}${l.quantity > 1 ? `x${l.quantity}` : ''}`);
|
||||
const lootLine = document.createElement('p');
|
||||
lootLine.textContent = `Loot: ${lootParts.join(', ')}`;
|
||||
body.appendChild(lootLine);
|
||||
}
|
||||
body.appendChild(rewards);
|
||||
|
||||
// Level up
|
||||
if (result.leveledUp) {
|
||||
const lvLine = document.createElement('p');
|
||||
lvLine.style.fontWeight = 'bold';
|
||||
lvLine.textContent = `⬆ Level up! Now level ${result.newLevel}`;
|
||||
body.appendChild(lvLine);
|
||||
}
|
||||
|
||||
// HP remaining
|
||||
const hpLine = document.createElement('p');
|
||||
hpLine.textContent = `HP: ${result.hpRemaining} / ${result.hpMax}`;
|
||||
body.appendChild(hpLine);
|
||||
|
||||
// Energy remaining
|
||||
const energyLine = document.createElement('p');
|
||||
energyLine.textContent = `Energy: ${result.energy}`;
|
||||
body.appendChild(energyLine);
|
||||
|
||||
// Gold lost on defeat
|
||||
if (result.goldLost > 0) {
|
||||
const lostLine = document.createElement('p');
|
||||
lostLine.style.color = '#c0392b';
|
||||
lostLine.textContent = `Lost ${result.goldLost} gold to defeat.`;
|
||||
body.appendChild(lostLine);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Combat log =====
|
||||
async function loadCombatLog() {
|
||||
const logList = document.getElementById('combat-log-list');
|
||||
try {
|
||||
const data = await api.get('/combat-log?limit=10');
|
||||
if (!data || !data.ok) {
|
||||
logList.innerHTML = '';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'placeholder';
|
||||
p.textContent = 'No combat history.';
|
||||
logList.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = data.data;
|
||||
logList.innerHTML = '';
|
||||
|
||||
if (logs.length === 0) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'placeholder';
|
||||
p.textContent = 'No combat history.';
|
||||
logList.appendChild(p);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const log of logs) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
|
||||
const resultTag = document.createElement('span');
|
||||
resultTag.className = `log-result ${log.won ? 'win' : 'loss'}`;
|
||||
resultTag.textContent = log.won ? '✓' : '✗';
|
||||
|
||||
const logText = document.createElement('span');
|
||||
logText.className = 'log-text';
|
||||
const parts = [`${log.monsterName} (Lv.${log.monsterLevel})`];
|
||||
if (log.won) {
|
||||
if (log.xpGained) parts.push(`+${log.xpGained} XP`);
|
||||
if (log.goldGained) parts.push(`+${log.goldGained} G`);
|
||||
} else {
|
||||
parts.push('defeated');
|
||||
}
|
||||
logText.textContent = parts.join(' · ');
|
||||
|
||||
entry.appendChild(resultTag);
|
||||
entry.appendChild(logText);
|
||||
logList.appendChild(entry);
|
||||
}
|
||||
} catch (_) {
|
||||
logList.innerHTML = '';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'placeholder';
|
||||
p.textContent = 'Failed to load combat log.';
|
||||
logList.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Toast notifications =====
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toasts
|
||||
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 () => {
|
||||
await loadCharacter();
|
||||
await loadZones();
|
||||
await loadCombatLog();
|
||||
startEnergySync();
|
||||
});
|
||||
|
||||
// Logout handler
|
||||
document.getElementById('logout-btn')?.addEventListener('click', async (e) => {
|
||||
@@ -135,5 +388,3 @@ document.getElementById('logout-btn')?.addEventListener('click', async (e) => {
|
||||
} catch (_) {}
|
||||
window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadCharacter);
|
||||
|
||||
Reference in New Issue
Block a user