feat: spell catalog and character spell management routes

This commit is contained in:
Aaron Wood 2026-04-11 11:34:41 -04:00
parent 7ad0f1410d
commit ff7f22d77b
3 changed files with 101 additions and 0 deletions

View file

@ -12,6 +12,7 @@ import characterRoutes from "./routes/characters.js";
import gameItemRoutes from "./routes/game-items.js";
import gameTalentRoutes from "./routes/game-talents.js";
import rollRoutes from "./routes/rolls.js";
import spellRoutes from "./routes/spells.js";
import authRoutes from "./routes/auth.js";
import db from "./db.js";
@ -38,6 +39,7 @@ app.use("/api/characters", characterRoutes);
app.use("/api/game-items", gameItemRoutes);
app.use("/api/game-talents", gameTalentRoutes);
app.use("/api/campaigns/:campaignId/rolls", rollRoutes);
app.use("/api/spells", spellRoutes);
const PORT = process.env.PORT ?? 3000;

View file

@ -437,4 +437,78 @@ router.delete("/:id/talents/:talentId", async (req, res) => {
}
});
async function getCharacterCampaignId(id: number): Promise<{ campaign_id: number }> {
const [rows] = await db.execute<RowDataPacket[]>("SELECT campaign_id FROM characters WHERE id = ?", [id]);
return rows[0] as { campaign_id: number };
}
// GET /api/characters/:id/spells
router.get("/:id/spells", requireAuth, async (req, res, next) => {
try {
const [rows] = await db.execute<RowDataPacket[]>(
`SELECT cs.id, cs.spell_id, cs.exhausted, cs.locked_until, cs.focus_active, cs.focus_started_at,
s.name, s.class, s.tier, s.casting_stat, s.duration, s.range, s.is_focus, s.description
FROM character_spells cs
JOIN spells s ON s.id = cs.spell_id
WHERE cs.character_id = ?
ORDER BY s.tier, s.name`,
[req.params.id]
);
res.json(rows);
} catch (err) { next(err); }
});
// POST /api/characters/:id/spells
router.post("/:id/spells", requireAuth, async (req, res, next) => {
try {
const { spell_id } = req.body;
await db.execute(
"INSERT IGNORE INTO character_spells (character_id, spell_id) VALUES (?, ?)",
[req.params.id, spell_id]
);
const [rows] = await db.execute<RowDataPacket[]>(
`SELECT cs.*, s.name, s.class, s.tier, s.casting_stat, s.duration, s.range, s.is_focus, s.description
FROM character_spells cs JOIN spells s ON s.id = cs.spell_id
WHERE cs.character_id = ? AND cs.spell_id = ?`,
[req.params.id, spell_id]
);
const io = req.app.get("io");
const char = await getCharacterCampaignId(Number(req.params.id));
io.to(String(char.campaign_id)).emit("spell:added", { characterId: Number(req.params.id), spell: rows[0] });
res.status(201).json(rows[0]);
} catch (err) { next(err); }
});
// DELETE /api/characters/:id/spells/:spellId
router.delete("/:id/spells/:spellId", requireAuth, async (req, res, next) => {
try {
await db.execute(
"DELETE FROM character_spells WHERE character_id = ? AND spell_id = ?",
[req.params.id, req.params.spellId]
);
const io = req.app.get("io");
const char = await getCharacterCampaignId(Number(req.params.id));
io.to(String(char.campaign_id)).emit("spell:removed", { characterId: Number(req.params.id), spellId: Number(req.params.spellId) });
res.json({ ok: true });
} catch (err) { next(err); }
});
// POST /api/characters/:id/rest
router.post("/:id/rest", requireAuth, async (req, res, next) => {
try {
await db.execute(
"UPDATE character_spells SET exhausted = 0, focus_active = 0, focus_started_at = NULL WHERE character_id = ? AND locked_until IS NULL",
[req.params.id]
);
await db.execute(
"DELETE FROM character_conditions WHERE character_id = ?",
[req.params.id]
);
const io = req.app.get("io");
const char = await getCharacterCampaignId(Number(req.params.id));
io.to(String(char.campaign_id)).emit("character:rested", { characterId: Number(req.params.id) });
res.json({ ok: true });
} catch (err) { next(err); }
});
export default router;

View file

@ -0,0 +1,25 @@
import { Router } from "express";
import type { RowDataPacket } from "mysql2";
import db from "../db.js";
import { requireAuth } from "../auth/middleware.js";
const router = Router();
// GET /api/spells — all spells, optionally filtered by class
router.get("/", requireAuth, async (req, res, next) => {
try {
const { class: spellClass } = req.query;
let sql = "SELECT * FROM spells ORDER BY class, tier, name";
const params: string[] = [];
if (spellClass && typeof spellClass === "string") {
sql = "SELECT * FROM spells WHERE class = ? OR class = 'both' ORDER BY tier, name";
params.push(spellClass);
}
const [rows] = await db.execute<RowDataPacket[]>(sql, params);
res.json(rows);
} catch (err) {
next(err);
}
});
export default router;