238 lines
8.6 KiB
JavaScript
238 lines
8.6 KiB
JavaScript
import { Router } from "express";
|
|
import db from "../db.js";
|
|
import { broadcastToCampaign } from "../socket.js";
|
|
const router = Router({ mergeParams: true });
|
|
const DEFAULT_STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
|
// GET /api/campaigns/:campaignId/characters — list characters in a campaign
|
|
router.get("/", (req, res) => {
|
|
const { campaignId } = req.params;
|
|
const characters = db
|
|
.prepare("SELECT * FROM characters WHERE campaign_id = ? ORDER BY name")
|
|
.all(campaignId);
|
|
const stmtStats = db.prepare("SELECT stat_name, value FROM character_stats WHERE character_id = ?");
|
|
const stmtGear = db.prepare("SELECT * FROM character_gear WHERE character_id = ?");
|
|
const stmtTalents = db.prepare("SELECT * FROM character_talents WHERE character_id = ?");
|
|
const enriched = characters.map((char) => ({
|
|
...char,
|
|
stats: stmtStats.all(char.id),
|
|
gear: stmtGear.all(char.id),
|
|
talents: stmtTalents.all(char.id),
|
|
}));
|
|
res.json(enriched);
|
|
});
|
|
// POST /api/campaigns/:campaignId/characters — create a character
|
|
router.post("/", (req, res) => {
|
|
const { campaignId } = req.params;
|
|
const { name, class: charClass, ancestry, hp_max } = req.body;
|
|
if (!name || !name.trim()) {
|
|
res.status(400).json({ error: "Character name is required" });
|
|
return;
|
|
}
|
|
const insertChar = db.prepare(`
|
|
INSERT INTO characters (campaign_id, name, class, ancestry, hp_current, hp_max)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`);
|
|
const insertStat = db.prepare("INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, 10)");
|
|
const result = insertChar.run(campaignId, name.trim(), charClass || "Fighter", ancestry || "Human", hp_max || 0, hp_max || 0);
|
|
const characterId = result.lastInsertRowid;
|
|
for (const stat of DEFAULT_STATS) {
|
|
insertStat.run(characterId, stat);
|
|
}
|
|
const character = db
|
|
.prepare("SELECT * FROM characters WHERE id = ?")
|
|
.get(characterId);
|
|
const stats = db
|
|
.prepare("SELECT stat_name, value FROM character_stats WHERE character_id = ?")
|
|
.all(characterId);
|
|
const enriched = {
|
|
...character,
|
|
stats,
|
|
gear: [],
|
|
talents: [],
|
|
};
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(campaignId), "character:created", enriched);
|
|
res.status(201).json(enriched);
|
|
});
|
|
// PATCH /api/characters/:id — update character fields
|
|
router.patch("/:id", (req, res) => {
|
|
const { id } = req.params;
|
|
const allowedFields = [
|
|
"name",
|
|
"class",
|
|
"ancestry",
|
|
"level",
|
|
"xp",
|
|
"hp_current",
|
|
"hp_max",
|
|
"ac",
|
|
"alignment",
|
|
"title",
|
|
"notes",
|
|
];
|
|
const updates = [];
|
|
const values = [];
|
|
for (const field of allowedFields) {
|
|
if (req.body[field] !== undefined) {
|
|
updates.push(`${field} = ?`);
|
|
values.push(req.body[field]);
|
|
}
|
|
}
|
|
if (updates.length === 0) {
|
|
res.status(400).json({ error: "No valid fields to update" });
|
|
return;
|
|
}
|
|
values.push(id);
|
|
db.prepare(`UPDATE characters SET ${updates.join(", ")} WHERE id = ?`).run(...values);
|
|
const character = db
|
|
.prepare("SELECT * FROM characters WHERE id = ?")
|
|
.get(id);
|
|
if (!character) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "character:updated", {
|
|
id: Number(id),
|
|
...req.body,
|
|
});
|
|
res.json(character);
|
|
});
|
|
// DELETE /api/characters/:id — delete a character
|
|
router.delete("/:id", (req, res) => {
|
|
const character = db
|
|
.prepare("SELECT * FROM characters WHERE id = ?")
|
|
.get(req.params.id);
|
|
if (!character) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
db.prepare("DELETE FROM characters WHERE id = ?").run(req.params.id);
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "character:deleted", {
|
|
id: Number(req.params.id),
|
|
});
|
|
res.status(204).end();
|
|
});
|
|
// PATCH /api/characters/:id/stats/:statName — update a single stat
|
|
router.patch("/:id/stats/:statName", (req, res) => {
|
|
const { id, statName } = req.params;
|
|
const { value } = req.body;
|
|
const upper = statName.toUpperCase();
|
|
if (!DEFAULT_STATS.includes(upper)) {
|
|
res.status(400).json({ error: "Invalid stat name" });
|
|
return;
|
|
}
|
|
db.prepare("UPDATE character_stats SET value = ? WHERE character_id = ? AND stat_name = ?").run(value, id, upper);
|
|
const character = db
|
|
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
.get(id);
|
|
if (!character) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "stat:updated", {
|
|
characterId: Number(id),
|
|
statName: upper,
|
|
value,
|
|
});
|
|
res.json({ characterId: Number(id), statName: upper, value });
|
|
});
|
|
// POST /api/characters/:id/gear — add gear
|
|
router.post("/:id/gear", (req, res) => {
|
|
const { id } = req.params;
|
|
const { name, type, slot_count, properties } = req.body;
|
|
if (!name || !name.trim()) {
|
|
res.status(400).json({ error: "Gear name is required" });
|
|
return;
|
|
}
|
|
const result = db
|
|
.prepare("INSERT INTO character_gear (character_id, name, type, slot_count, properties) VALUES (?, ?, ?, ?, ?)")
|
|
.run(id, name.trim(), type || "gear", slot_count ?? 1, JSON.stringify(properties || {}));
|
|
const gear = db
|
|
.prepare("SELECT * FROM character_gear WHERE id = ?")
|
|
.get(result.lastInsertRowid);
|
|
const character = db
|
|
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
.get(id);
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "gear:added", {
|
|
characterId: Number(id),
|
|
gear,
|
|
});
|
|
res.status(201).json(gear);
|
|
});
|
|
// DELETE /api/characters/:id/gear/:gearId — remove gear
|
|
router.delete("/:id/gear/:gearId", (req, res) => {
|
|
const { id, gearId } = req.params;
|
|
const character = db
|
|
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
.get(id);
|
|
if (!character) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
const result = db
|
|
.prepare("DELETE FROM character_gear WHERE id = ? AND character_id = ?")
|
|
.run(gearId, id);
|
|
if (result.changes === 0) {
|
|
res.status(404).json({ error: "Gear not found" });
|
|
return;
|
|
}
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "gear:removed", {
|
|
characterId: Number(id),
|
|
gearId: Number(gearId),
|
|
});
|
|
res.status(204).end();
|
|
});
|
|
// POST /api/characters/:id/talents — add talent
|
|
router.post("/:id/talents", (req, res) => {
|
|
const { id } = req.params;
|
|
const { name, description, effect } = req.body;
|
|
if (!name || !name.trim()) {
|
|
res.status(400).json({ error: "Talent name is required" });
|
|
return;
|
|
}
|
|
const result = db
|
|
.prepare("INSERT INTO character_talents (character_id, name, description, effect) VALUES (?, ?, ?, ?)")
|
|
.run(id, name.trim(), description || "", JSON.stringify(effect || {}));
|
|
const talent = db
|
|
.prepare("SELECT * FROM character_talents WHERE id = ?")
|
|
.get(result.lastInsertRowid);
|
|
const character = db
|
|
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
.get(id);
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "talent:added", {
|
|
characterId: Number(id),
|
|
talent,
|
|
});
|
|
res.status(201).json(talent);
|
|
});
|
|
// DELETE /api/characters/:id/talents/:talentId — remove talent
|
|
router.delete("/:id/talents/:talentId", (req, res) => {
|
|
const { id, talentId } = req.params;
|
|
const character = db
|
|
.prepare("SELECT campaign_id FROM characters WHERE id = ?")
|
|
.get(id);
|
|
if (!character) {
|
|
res.status(404).json({ error: "Character not found" });
|
|
return;
|
|
}
|
|
const result = db
|
|
.prepare("DELETE FROM character_talents WHERE id = ? AND character_id = ?")
|
|
.run(talentId, id);
|
|
if (result.changes === 0) {
|
|
res.status(404).json({ error: "Talent not found" });
|
|
return;
|
|
}
|
|
const io = req.app.get("io");
|
|
broadcastToCampaign(io, Number(character.campaign_id), "talent:removed", {
|
|
characterId: Number(id),
|
|
talentId: Number(talentId),
|
|
});
|
|
res.status(204).end();
|
|
});
|
|
export default router;
|