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
+8
View File
@@ -4,6 +4,8 @@ const { createSessionMiddleware, requireAuth } = require('./middleware/session')
const { registerLimiter, loginLimiter, routes: authRoutes } = require('./routes/auth');
const { routes: characterRoutes } = require('./routes/character');
const { routes: huntRoutes } = require('./routes/hunt');
const { routes: zonesRoutes } = require('./routes/zones');
const { routes: combatLogRoutes } = require('./routes/combatLog');
const { AppError } = require('./utils/errors');
function createApp() {
@@ -44,6 +46,12 @@ function createApp() {
// Hunt routes (require session)
app.post('/api/hunt', requireAuth, huntRoutes.postHunt);
// Zone routes (require session)
app.get('/api/zones', requireAuth, zonesRoutes.getZones);
// Combat log routes (require session)
app.get('/api/combat-log', requireAuth, combatLogRoutes.getCombatLog);
// Central error handler
app.use((err, _req, res, _next) => {
// Log stack traces server-side only
+32
View File
@@ -0,0 +1,32 @@
// Combat log routes: GET /api/combat-log (recent fights for the logged-in user's character).
const zod = require('zod');
const { getCombatLog } = require('../services/combatLogService');
const { requireAuth } = require('../middleware/session');
const { getCharacterIdForUser } = require('../services/characterService');
function wrap(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
const logSchema = zod.object({
limit: zod.coerce.number().int().min(1).max(100).optional(),
});
async function handleGetCombatLog(req, res) {
const parsed = logSchema.safeParse(req.query);
if (!parsed.success) {
const { AppError } = require('../utils/errors');
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
}
const characterId = getCharacterIdForUser(req.session.userId);
const limit = parsed.data.limit ?? 20;
const logs = getCombatLog(characterId, limit);
res.json({ ok: true, data: logs });
}
module.exports = {
routes: {
getCombatLog: wrap(handleGetCombatLog),
},
};
+21
View File
@@ -0,0 +1,21 @@
// Zone routes: GET /api/zones (list zones with unlocked flag).
const zod = require('zod');
const { listZones } = require('../services/zoneService');
const { requireAuth } = require('../middleware/session');
const { getCharacterProfile } = require('../services/characterService');
function wrap(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
async function handleGetZones(req, res) {
const profile = getCharacterProfile(req.session.userId);
const zones = listZones(profile.character.level);
res.json({ ok: true, data: zones });
}
module.exports = {
routes: {
getZones: wrap(handleGetZones),
},
};
+45
View File
@@ -0,0 +1,45 @@
// Combat log service: retrieve recent combat log entries for a character.
const { getDb } = require('../db');
const stmtCache = new WeakMap();
function getStmts(db) {
let s = stmtCache.get(db);
if (!s) {
s = {
logs: db.prepare(
`SELECT cl.id, cl.monster_id, cl.won, cl.rounds, cl.xp_gained, cl.gold_gained, cl.created_at,
m.name AS monster_name, m.level AS monster_level
FROM combat_log cl
JOIN monsters m ON m.id = cl.monster_id
WHERE cl.character_id = ?
ORDER BY cl.id DESC
LIMIT ?`
),
};
stmtCache.set(db, s);
}
return s;
}
/**
* @param {number} characterId
* @param {number} [limit] - max entries, default 20
* @returns {Array}
*/
function getCombatLog(characterId, limit = 20) {
const db = getDb();
const s = getStmts(db);
return s.logs.all(characterId, limit).map((row) => ({
id: row.id,
monsterName: row.monster_name,
monsterLevel: row.monster_level,
won: Boolean(row.won),
rounds: row.rounds,
xpGained: row.xp_gained,
goldGained: row.gold_gained,
createdAt: row.created_at,
}));
}
module.exports = { getCombatLog };
+31
View File
@@ -0,0 +1,31 @@
// Zone service: list zones with unlocked flag for a given character level.
const { getDb } = require('../db');
// Prepared statements are cached PER database handle.
const stmtCache = new WeakMap();
function getStmts(db) {
let s = stmtCache.get(db);
if (!s) {
s = {
zones: db.prepare('SELECT id, slug, name, description, min_level, energy_cost FROM zones ORDER BY min_level, id'),
};
stmtCache.set(db, s);
}
return s;
}
/**
* @param {number} characterLevel - hero's current level
* @returns {Array<{id, slug, name, description, min_level, energy_cost, unlocked}>}
*/
function listZones(characterLevel) {
const db = getDb();
const s = getStmts(db);
return s.zones.all().map((zone) => ({
...zone,
unlocked: characterLevel >= zone.min_level,
}));
}
module.exports = { listZones };