step 4.5: done

This commit is contained in:
bepis
2026-10-20 21:31:12 +11:00
parent 9a308b697d
commit f26ca88d1e
11 changed files with 742 additions and 27 deletions
+144
View File
@@ -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
View File
@@ -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
View File
@@ -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);