diff --git a/server/src/index.ts b/server/src/index.ts index cbc8fbc..abd9cb1 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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; diff --git a/server/src/routes/characters.ts b/server/src/routes/characters.ts index fa35832..bd83589 100644 --- a/server/src/routes/characters.ts +++ b/server/src/routes/characters.ts @@ -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("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( + `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( + `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; diff --git a/server/src/routes/spells.ts b/server/src/routes/spells.ts new file mode 100644 index 0000000..080b56e --- /dev/null +++ b/server/src/routes/spells.ts @@ -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(sql, params); + res.json(rows); + } catch (err) { + next(err); + } +}); + +export default router;