step 10.8: wip (not ticked)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
@@ -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),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user