step 5.3: done
This commit is contained in:
@@ -57,6 +57,7 @@ function createApp() {
|
||||
app.get('/api/inventory', requireAuth, inventoryRoutes.getInventory);
|
||||
app.post('/api/inventory/equip', requireAuth, inventoryRoutes.postEquip);
|
||||
app.post('/api/inventory/unequip', requireAuth, inventoryRoutes.postUnequip);
|
||||
app.post('/api/inventory/use', requireAuth, inventoryRoutes.postUse);
|
||||
|
||||
// Central error handler
|
||||
app.use((err, _req, res, _next) => {
|
||||
|
||||
+20
-2
@@ -1,9 +1,9 @@
|
||||
// Inventory routes: list, equip, unequip.
|
||||
// Inventory routes: list, equip, unequip, use consumables.
|
||||
const zod = require('zod');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { requireAuth } = require('../middleware/session');
|
||||
const { getCharacterIdForUser } = require('../services/characterService');
|
||||
const { listInventory, equipItem, unequipItem } = require('../services/inventoryService');
|
||||
const { listInventory, equipItem, unequipItem, useConsumable } = require('../services/inventoryService');
|
||||
|
||||
// Validation schemas
|
||||
const slotSchema = zod.enum(['weapon', 'armor', 'trinket']);
|
||||
@@ -56,10 +56,28 @@ async function handleUnequip(req, res) {
|
||||
res.json({ ok: true, data: result });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/inventory/use — use a stackable consumable (heal/energy).
|
||||
*/
|
||||
async function handleUse(req, res) {
|
||||
const parsed = zod.strictObject({
|
||||
itemId: zod.number().int().positive(),
|
||||
}).safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
|
||||
}
|
||||
|
||||
const { itemId } = parsed.data;
|
||||
const characterId = getCharacterIdForUser(req.session.userId);
|
||||
const result = useConsumable(characterId, itemId);
|
||||
res.json({ ok: true, data: result });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
routes: {
|
||||
getInventory: wrap(handleListInventory),
|
||||
postEquip: wrap(handleEquip),
|
||||
postUnequip: wrap(handleUnequip),
|
||||
postUse: wrap(handleUse),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Inventory service: list, equip, unequip items.
|
||||
// Inventory service: list, equip, unequip, and use consumables.
|
||||
// Equipment bonuses feed into calcDerived() for stat updates.
|
||||
const { getDb } = require('../db');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const { calcDerived, ENERGY_MAX } = require('../game/stats');
|
||||
|
||||
const VALID_SLOTS = new Set(['weapon', 'armor', 'trinket']);
|
||||
|
||||
@@ -54,6 +55,31 @@ function getStmts(db) {
|
||||
character: db.prepare(
|
||||
'SELECT * FROM characters WHERE id = ?'
|
||||
),
|
||||
|
||||
// Get inventory row for a specific (character, item) — including quantity.
|
||||
inventoryRowForUse: db.prepare(
|
||||
'SELECT * FROM inventory WHERE character_id = ? AND item_id = ? AND quantity >= 1'
|
||||
),
|
||||
|
||||
// Decrement quantity by 1 (only when qty > 1 to avoid CHECK violation).
|
||||
decrementQty: db.prepare(
|
||||
'UPDATE inventory SET quantity = quantity - 1 WHERE id = ? AND quantity > 1'
|
||||
),
|
||||
|
||||
// Delete row when it's the last item (qty == 1).
|
||||
deleteLast: db.prepare(
|
||||
'DELETE FROM inventory WHERE id = ? AND quantity = 1'
|
||||
),
|
||||
|
||||
// Update character hp_current.
|
||||
updateHp: db.prepare(
|
||||
'UPDATE characters SET hp_current = ? WHERE id = ?'
|
||||
),
|
||||
|
||||
// Update character energy.
|
||||
updateEnergy: db.prepare(
|
||||
'UPDATE characters SET energy = ?, energy_updated_at = ? WHERE id = ?'
|
||||
),
|
||||
};
|
||||
stmtCache.set(db, s);
|
||||
}
|
||||
@@ -209,4 +235,74 @@ function unequipItem(characterId, slot) {
|
||||
return listInventory(characterId);
|
||||
}
|
||||
|
||||
module.exports = { listInventory, equipItem, unequipItem };
|
||||
/**
|
||||
* Use a stackable consumable item. Applies its effect (heal or energy),
|
||||
* decrements quantity, removes the row when quantity reaches 0.
|
||||
* @param {number} characterId
|
||||
* @param {number} itemId
|
||||
* @returns {object} updated inventory listing
|
||||
*/
|
||||
function useConsumable(characterId, itemId) {
|
||||
const db = getDb();
|
||||
const s = getStmts(db);
|
||||
|
||||
const ch = s.character.get(characterId);
|
||||
if (!ch) throw new AppError('NOT_FOUND', 'Character not found.', 404);
|
||||
|
||||
const item = s.itemById.get(itemId);
|
||||
if (!item) throw new AppError('NOT_FOUND', 'Item not found.', 404);
|
||||
|
||||
if (item.type !== 'consumable') {
|
||||
throw new AppError('VALIDATION_ERROR', `${item.name} is not a consumable.`, 400);
|
||||
}
|
||||
|
||||
if (!item.effect) {
|
||||
throw new AppError('VALIDATION_ERROR', `${item.name} has no effect.`, 400);
|
||||
}
|
||||
|
||||
// Parse effect: 'heal:N' or 'energy:N'
|
||||
const match = item.effect.match(/^(heal|energy):(\d+)$/);
|
||||
if (!match) {
|
||||
throw new AppError('VALIDATION_ERROR', `${item.name} has an invalid effect format.`, 400);
|
||||
}
|
||||
const [_full, effectType, effectValueStr] = match;
|
||||
const effectValue = parseInt(effectValueStr, 10);
|
||||
|
||||
// Find the inventory row (must have quantity >= 1).
|
||||
const invRow = s.inventoryRowForUse.get(characterId, item.id);
|
||||
if (!invRow) throw new AppError('NOT_FOUND', 'Item not in inventory or depleted.', 404);
|
||||
|
||||
// Apply effect inside a transaction.
|
||||
const applyEffect = db.transaction((rowId, qty, etype, evalue, charId, c) => {
|
||||
// Apply the effect.
|
||||
if (etype === 'heal') {
|
||||
const maxHp = calcDerived(
|
||||
{ str: c.str, agi: c.agi, int: c.int, vit: c.vit },
|
||||
c.level, c.class
|
||||
).maxHp;
|
||||
const newHp = Math.min(c.hp_current + evalue, maxHp);
|
||||
s.updateHp.run(newHp, charId);
|
||||
} else if (etype === 'energy') {
|
||||
const { computeEnergy } = require('../game/stats');
|
||||
const now = require('../utils/time').now();
|
||||
const energy = computeEnergy(c.energy, c.energy_updated_at, now);
|
||||
const newEnergy = Math.min(energy.energy + evalue, ENERGY_MAX);
|
||||
const newUpdatedAt = newEnergy >= ENERGY_MAX ? now : energy.updatedAtMs;
|
||||
s.updateEnergy.run(newEnergy, newUpdatedAt, charId);
|
||||
}
|
||||
|
||||
// Delete directly when qty==1; otherwise decrement.
|
||||
if (qty === 1) {
|
||||
s.deleteLast.run(rowId);
|
||||
} else {
|
||||
s.decrementQty.run(rowId);
|
||||
}
|
||||
});
|
||||
|
||||
applyEffect(invRow.id, invRow.quantity, effectType, effectValue, characterId, ch);
|
||||
|
||||
// Return updated listing.
|
||||
return listInventory(characterId);
|
||||
}
|
||||
|
||||
module.exports = { listInventory, equipItem, unequipItem, useConsumable };
|
||||
|
||||
Reference in New Issue
Block a user