Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a616244d4e |
+33
-29
@@ -19,6 +19,10 @@ function switchTab(tab) {
|
|||||||
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('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';
|
||||||
|
if (tab === 'expedition') {
|
||||||
|
loadExpeditions();
|
||||||
|
startExpeditionSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,6 +140,7 @@ async function loadCharacter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { character, derived, energy } = data;
|
const { character, derived, energy } = data;
|
||||||
|
window.heroLevel = character.level;
|
||||||
|
|
||||||
// Show stats, hide loading
|
// Show stats, hide loading
|
||||||
document.getElementById('hero-loading').style.display = 'none';
|
document.getElementById('hero-loading').style.display = 'none';
|
||||||
@@ -177,7 +182,7 @@ async function loadZones() {
|
|||||||
const zoneList = document.getElementById('zone-list');
|
const zoneList = document.getElementById('zone-list');
|
||||||
try {
|
try {
|
||||||
const data = await api.get('/zones');
|
const data = await api.get('/zones');
|
||||||
if (!data || !data.ok) {
|
if (!Array.isArray(data)) {
|
||||||
zoneList.innerHTML = '';
|
zoneList.innerHTML = '';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'placeholder';
|
p.className = 'placeholder';
|
||||||
@@ -186,7 +191,7 @@ async function loadZones() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const zones = data.data;
|
const zones = data;
|
||||||
zoneList.innerHTML = '';
|
zoneList.innerHTML = '';
|
||||||
|
|
||||||
for (const zone of zones) {
|
for (const zone of zones) {
|
||||||
@@ -256,18 +261,18 @@ async function handleHunt(e) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/hunt', { zoneId });
|
const data = await api.post('/hunt', { zoneId });
|
||||||
if (!data || !data.ok) {
|
if (!data) {
|
||||||
showToast(data?.error?.message || 'Hunt failed.', 'error');
|
showToast(data?.error?.message || 'Hunt failed.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = data.data;
|
const result = data;
|
||||||
displayHuntResult(result);
|
displayHuntResult(result);
|
||||||
loadCharacter();
|
loadCharacter();
|
||||||
loadCombatLog();
|
loadCombatLog();
|
||||||
loadZones(); // refresh energy cost buttons
|
loadZones(); // refresh energy cost buttons
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Hunt failed.', 'error');
|
showToast(err.message || 'Hunt failed.', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
isHunting = false;
|
isHunting = false;
|
||||||
// Re-enable all hunt buttons
|
// Re-enable all hunt buttons
|
||||||
@@ -352,7 +357,7 @@ async function loadCombatLog() {
|
|||||||
const logList = document.getElementById('combat-log-list');
|
const logList = document.getElementById('combat-log-list');
|
||||||
try {
|
try {
|
||||||
const data = await api.get('/combat-log?limit=10');
|
const data = await api.get('/combat-log?limit=10');
|
||||||
if (!data || !data.ok) {
|
if (!Array.isArray(data)) {
|
||||||
logList.innerHTML = '';
|
logList.innerHTML = '';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'placeholder';
|
p.className = 'placeholder';
|
||||||
@@ -361,7 +366,7 @@ async function loadCombatLog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = data.data;
|
const logs = data;
|
||||||
logList.innerHTML = '';
|
logList.innerHTML = '';
|
||||||
|
|
||||||
if (logs.length === 0) {
|
if (logs.length === 0) {
|
||||||
@@ -595,8 +600,8 @@ async function handleItemClick(item) {
|
|||||||
} else {
|
} else {
|
||||||
data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type });
|
data = await api.post('/inventory/equip', { itemId: item.itemId, slot: item.type });
|
||||||
}
|
}
|
||||||
if (!data || !data.ok) {
|
if (!data) {
|
||||||
showToast(data?.error?.message || 'Action failed.', 'error');
|
showToast('Action failed.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inventoryCache = data;
|
inventoryCache = data;
|
||||||
@@ -611,8 +616,8 @@ async function handleItemClick(item) {
|
|||||||
async function handleUnequip(slot) {
|
async function handleUnequip(slot) {
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/inventory/unequip', { slot });
|
const data = await api.post('/inventory/unequip', { slot });
|
||||||
if (!data || !data.ok) {
|
if (!data) {
|
||||||
showToast(data?.error?.message || 'Unequip failed.', 'error');
|
showToast('Unequip failed.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inventoryCache = data;
|
inventoryCache = data;
|
||||||
@@ -649,14 +654,13 @@ function formatDuration(sec) {
|
|||||||
|
|
||||||
async function loadExpeditions() {
|
async function loadExpeditions() {
|
||||||
try {
|
try {
|
||||||
const data = await api.get('/expeditions/active');
|
const data = await api.get('/expeditions/active'); // null when no active expedition
|
||||||
if (!data || !data.ok) return;
|
|
||||||
|
|
||||||
const statusEl = document.getElementById('expedition-status');
|
const statusEl = document.getElementById('expedition-status');
|
||||||
const defsEl = document.getElementById('expedition-defs');
|
const defsEl = document.getElementById('expedition-defs');
|
||||||
|
|
||||||
if (data.data) {
|
if (data) {
|
||||||
renderActiveExpedition(statusEl, defsEl, data.data);
|
renderActiveExpedition(statusEl, defsEl, data);
|
||||||
} else {
|
} else {
|
||||||
statusEl.innerHTML = '';
|
statusEl.innerHTML = '';
|
||||||
renderExpeditionDefs(defsEl);
|
renderExpeditionDefs(defsEl);
|
||||||
@@ -739,9 +743,9 @@ function renderExpeditionDefs(container) {
|
|||||||
|
|
||||||
// Fetch expedition definitions from the server
|
// Fetch expedition definitions from the server
|
||||||
api.get('/expeditions/defs').then(defsData => {
|
api.get('/expeditions/defs').then(defsData => {
|
||||||
if (!defsData || !defsData.ok || !defsData.data) return;
|
if (!Array.isArray(defsData)) return;
|
||||||
|
|
||||||
const defs = defsData.data;
|
const defs = defsData;
|
||||||
for (const def of defs) {
|
for (const def of defs) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'expedition-def-card';
|
card.className = 'expedition-def-card';
|
||||||
@@ -756,20 +760,20 @@ function renderExpeditionDefs(container) {
|
|||||||
|
|
||||||
const infoEl = document.createElement('div');
|
const infoEl = document.createElement('div');
|
||||||
infoEl.className = 'expedition-info';
|
infoEl.className = 'expedition-info';
|
||||||
infoEl.textContent = `${formatDuration(def.durationSec)} · ${def.energyCost}⚡ · Lv${def.min_level}+`;
|
infoEl.textContent = `${formatDuration(def.durationSec)} · ${def.energyCost}⚡ · Lv${def.minLevel}+`;
|
||||||
|
|
||||||
const rewardsEl = document.createElement('div');
|
const rewardsEl = document.createElement('div');
|
||||||
rewardsEl.className = 'expedition-rewards';
|
rewardsEl.className = 'expedition-rewards';
|
||||||
rewardsEl.textContent = `+${def.xpReward} XP, ${def.goldMin}–${def.gold_max} Gold`;
|
rewardsEl.textContent = `+${def.xpReward} XP, ${def.goldMin}–${def.goldMax} Gold`;
|
||||||
|
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'btn btn-expedition';
|
btn.className = 'btn btn-expedition';
|
||||||
btn.textContent = `Start Expedition (${def.energyCost}⚡)`;
|
btn.textContent = `Start Expedition (${def.energyCost}⚡)`;
|
||||||
btn.disabled = window.heroLevel < def.min_level;
|
btn.disabled = window.heroLevel < def.minLevel;
|
||||||
btn.dataset.defId = def.id;
|
btn.dataset.defId = def.id;
|
||||||
|
|
||||||
if (window.heroLevel < def.min_level) {
|
if (window.heroLevel < def.minLevel) {
|
||||||
btn.title = `Requires level ${def.min_level}`;
|
btn.title = `Requires level ${def.minLevel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.appendChild(nameEl);
|
card.appendChild(nameEl);
|
||||||
@@ -796,15 +800,15 @@ async function handleStartExpedition(e) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/expeditions/start', { defId });
|
const data = await api.post('/expeditions/start', { defId });
|
||||||
if (!data || !data.ok) {
|
if (!data) {
|
||||||
showToast(data?.error?.message || 'Failed to start expedition.', 'error');
|
showToast('Failed to start expedition.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast('Expedition started!', 'info');
|
showToast('Expedition started!', 'info');
|
||||||
loadExpeditions();
|
loadExpeditions();
|
||||||
loadCharacter(); // refresh energy
|
loadCharacter(); // refresh energy
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Failed to start expedition.', 'error');
|
showToast(err.message || 'Failed to start expedition.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,17 +822,17 @@ async function handleClaimExpedition(e) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/expeditions/claim', { expeditionId });
|
const data = await api.post('/expeditions/claim', { expeditionId });
|
||||||
if (!data || !data.ok) {
|
if (!data) {
|
||||||
showToast(data?.error?.message || 'Failed to claim expedition.', 'error');
|
showToast('Failed to claim expedition.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { xpGained, goldGained } = data.data;
|
const { xpGained, goldGained } = data;
|
||||||
showToast(`Expedition complete! +${xpGained} XP, +${goldGained} Gold`, 'info');
|
showToast(`Expedition complete! +${xpGained} XP, +${goldGained} Gold`, 'info');
|
||||||
loadExpeditions();
|
loadExpeditions();
|
||||||
loadCharacter(); // refresh gold/XP
|
loadCharacter(); // refresh gold/XP
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Failed to claim expedition.', 'error');
|
showToast(err.message || 'Failed to claim expedition.', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" required minlength="3" maxlength="20" autocomplete="username">
|
<input type="text" id="username" name="username" required minlength="3" maxlength="20" autocomplete="username">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input type="password" id="password" name="password" required minlength="8" maxlength="72" autocomplete="current-password">
|
<input type="password" id="password" name="password" required maxlength="72" autocomplete="current-password">
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Login</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="redirect">Don't have an account? <a href="/register.html">Register</a></p>
|
<p class="redirect">Don't have an account? <a href="/register.html">Register</a></p>
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ function createApp() {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Root: send visitors to the login page (there is no index.html)
|
||||||
|
app.get('/', (_req, res) => res.redirect('/login.html'));
|
||||||
|
|
||||||
// Auth routes (public)
|
// Auth routes (public)
|
||||||
app.post('/api/auth/register', registerLimiter, authRoutes.postRegister);
|
app.post('/api/auth/register', registerLimiter, authRoutes.postRegister);
|
||||||
app.post('/api/auth/login', loginLimiter, authRoutes.postLogin);
|
app.post('/api/auth/login', loginLimiter, authRoutes.postLogin);
|
||||||
|
|||||||
+7
-2
@@ -2,6 +2,11 @@ const rateLimit = require('express-rate-limit');
|
|||||||
const zod = require('zod');
|
const zod = require('zod');
|
||||||
const { AppError } = require('../utils/errors');
|
const { AppError } = require('../utils/errors');
|
||||||
const { register, verifyLogin } = require('../services/authService');
|
const { register, verifyLogin } = require('../services/authService');
|
||||||
|
const { getDb } = require('../db');
|
||||||
|
|
||||||
|
function userHasHero(userId) {
|
||||||
|
return !!getDb().prepare('SELECT 1 FROM characters WHERE user_id = ?').get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
// Validation schemas
|
// Validation schemas
|
||||||
const registerSchema = zod.strictObject({
|
const registerSchema = zod.strictObject({
|
||||||
@@ -63,7 +68,7 @@ async function handleLogin(req, res) {
|
|||||||
req.session.regenerate((err) => {
|
req.session.regenerate((err) => {
|
||||||
if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500);
|
if (err) throw new AppError('INTERNAL_ERROR', 'Session regeneration failed.', 500);
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
res.json({ ok: true, data: { user: { id: user.id, username: user.username }, hasHero: false } });
|
res.json({ ok: true, data: { user: { id: user.id, username: user.username }, hasHero: userHasHero(user.id) } });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +82,7 @@ async function handleLogout(req, res) {
|
|||||||
|
|
||||||
async function handleMe(req, res) {
|
async function handleMe(req, res) {
|
||||||
const userId = req.session.userId;
|
const userId = req.session.userId;
|
||||||
res.json({ ok: true, data: { user: { id: userId, username: req.session.username || '' }, hasHero: false } });
|
res.json({ ok: true, data: { user: { id: userId, username: req.session.username || '' }, hasHero: userHasHero(userId) } });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user