step 10.8: wip (not ticked)

This commit is contained in:
bepis
2026-10-23 00:38:55 +11:00
parent add65e9e5b
commit 46663d892e
9 changed files with 400 additions and 28 deletions
+1
View File
@@ -65,6 +65,7 @@ function createApp() {
// Character routes (require session)
app.post('/api/character', requireAuth, characterRoutes.postCharacter);
app.get('/api/character', requireAuth, characterRoutes.getCharacter);
app.post('/api/character/allocate', requireAuth, characterRoutes.postAllocate);
// Hunt routes (require session)
app.post('/api/hunt', requireAuth, huntRoutes.postHunt);
+2 -2
View File
@@ -73,8 +73,8 @@ function allocatePoints(currentStr, currentAgi, currentInt, currentVit, points,
if (!(key in allowed)) {
throw new Error(`Invalid stat: ${key}. Allowed: ${Object.keys(allowed).join(', ')}`);
}
if (typeof value !== 'number' || value < 1 || !Number.isInteger(value)) {
throw new Error(`Allocation for ${key} must be a positive integer.`);
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
throw new Error(`Allocation for ${key} must be a non-negative integer.`);
}
allowed[key] += value;
allocated += value;
+24 -2
View File
@@ -1,8 +1,8 @@
// Character routes: POST /api/character (create), GET /api/character (profile).
// Character routes: POST /api/character (create), GET /api/character (profile), POST /api/character/allocate.
const zod = require('zod');
const { AppError } = require('../utils/errors');
const { requireAuth } = require('../middleware/session');
const { createCharacter, getCharacterProfile } = require('../services/characterService');
const { createCharacter, getCharacterProfile, allocatePoints } = require('../services/characterService');
const { CLASS_KEYS } = require('../game/classes');
// Validation schemas
@@ -11,6 +11,15 @@ const createSchema = zod.strictObject({
class: zod.enum(CLASS_KEYS),
});
const allocateSchema = zod.strictObject({
str: zod.number().int().nonnegative().optional(),
agi: zod.number().int().nonnegative().optional(),
int: zod.number().int().nonnegative().optional(),
vit: zod.number().int().nonnegative().optional(),
}).refine(data => data.str !== undefined || data.agi !== undefined || data.int !== undefined || data.vit !== undefined, {
message: 'Must allocate at least one stat.',
});
function wrap(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
@@ -43,9 +52,22 @@ async function handleGet(req, res) {
res.json({ ok: true, data: profile });
}
async function handleAllocate(req, res) {
const parsed = allocateSchema.safeParse(req.body);
if (!parsed.success) {
throw new AppError('INVALID_FIELD', 'Request validation failed.', 400);
}
const userId = req.session.userId;
const result = allocatePoints(userId, parsed.data);
res.json({ ok: true, data: result });
}
module.exports = {
routes: {
postCharacter: wrap(handleCreate),
getCharacter: wrap(handleGet),
postAllocate: wrap(handleAllocate),
},
};
+99 -1
View File
@@ -185,4 +185,102 @@ function getCharacterIdForUser(userId) {
return row.id;
}
module.exports = { createCharacter, getCharacterProfile, spendEnergy, getCharacterIdForUser };
/**
* Allocate unspent stat points on a character. Runs in a transaction.
* @param {number} userId - the user's ID
* @param {object} allocation - { str?, agi?, int?, vit? } each a positive integer
* @returns {object} { character, derived } updated profile
*/
function allocatePoints(userId, allocation) {
const db = getDb();
const stmts = getStmts();
// Get character
const character = stmts.findByUserId.get(userId);
if (!character) {
throw new AppError('NO_CHARACTER', 'No character found.', 404);
}
// Validate allocation: must be non-negative integers, sum must not exceed unspent_points
const allowed = { str: 0, agi: 0, int: 0, vit: 0 };
let total = 0;
for (const [key, value] of Object.entries(allocation)) {
if (!(key in allowed)) {
throw new AppError('INVALID_FIELD', `Unknown stat: ${key}.`, 400);
}
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
throw new AppError('INVALID_FIELD', `Stat ${key} must be a non-negative integer.`, 400);
}
allowed[key] = value;
total += value;
}
if (total === 0) {
throw new AppError('VALIDATION_ERROR', 'Must allocate at least one point.', 400);
}
if (total > character.unspent_points) {
throw new AppError('NOT_ENOUGH_POINTS',
`Not enough points. Have ${character.unspent_points}, need ${total}.`, 422);
}
// Use existing allocatePoints from leveling.js
const { allocatePoints: allocatePointsPure } = require('../game/leveling');
const newStats = allocatePointsPure(
character.str, character.agi, character.int, character.vit,
total, allowed
);
const remainingPoints = character.unspent_points - total;
// Update character in a transaction
const update = db.transaction((str, agi, int, vit, remaining, cid) => {
db.prepare(
'UPDATE characters SET str = ?, agi = ?, int = ?, vit = ?, unspent_points = ? WHERE id = ?'
).run(str, agi, int, vit, remaining, cid);
});
update(newStats.str, newStats.agi, newStats.int, newStats.vit,
remainingPoints, character.id);
// Refresh character data
const updated = db.prepare('SELECT * FROM characters WHERE id = ?').get(character.id);
// Compute derived stats
const derived = require('../game/stats').calcDerived(
{ str: updated.str, agi: updated.agi, int: updated.int, vit: updated.vit },
updated.level,
updated.class
);
return {
character: {
id: updated.id,
name: updated.name,
class: updated.class,
level: updated.level,
xp: updated.xp,
gold: updated.gold,
str: updated.str,
agi: updated.agi,
int: updated.int,
vit: updated.vit,
unspent_points: updated.unspent_points,
hp_current: updated.hp_current,
maxHp: derived.maxHp,
kills: updated.kills,
deaths: updated.deaths,
},
derived: {
maxHp: derived.maxHp,
attack: derived.attack,
defense: derived.defense,
speed: derived.speed,
critChance: derived.critChance,
critMult: derived.critMult,
},
};
}
module.exports = { createCharacter, getCharacterProfile, spendEnergy, getCharacterIdForUser, allocatePoints };