feat: death:recovery-roll socket handler — d20 save, 18+ stands at 1 HP

This commit is contained in:
Aaron Wood 2026-04-12 01:29:21 -04:00
parent 0cf3e4583c
commit 43a5b85dcf

View file

@ -154,6 +154,96 @@ export function setupSocket(io: Server) {
io.to(`campaign:${campaignId}`).emit("atmosphere:update", atmosphere); io.to(`campaign:${campaignId}`).emit("atmosphere:update", atmosphere);
}); });
socket.on("death:recovery-roll", async (data: {
campaignId: number;
characterId: number;
}) => {
const userId = socket.data.user?.userId;
// Verify caller is DM
const [memberRows] = await db.execute<RowDataPacket[]>(
"SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?",
[data.campaignId, userId]
);
if (memberRows.length === 0 || memberRows[0].role !== "dm") return;
// Verify character has a Dying condition
const [dyingRows] = await db.execute<RowDataPacket[]>(
"SELECT id FROM character_conditions WHERE character_id = ? AND name = 'Dying'",
[data.characterId]
);
if (dyingRows.length === 0) return;
// Get character info for roll log
const [charRows] = await db.execute<RowDataPacket[]>(
"SELECT name, color, campaign_id FROM characters WHERE id = ?",
[data.characterId]
);
if (charRows.length === 0) return;
const char = charRows[0];
// Roll d20 server-side
const result = rollDice("1d20");
const roll = result.total;
const nat20 = roll === 20;
const success = roll >= 18;
// Log to roll_log with "Death Save" label (success gets suffix)
const label = success ? "Death Save \u2014 stands at 1 HP!" : "Death Save";
const [insertResult] = await db.execute<import("mysql2").ResultSetHeader>(
`INSERT INTO roll_log
(campaign_id, character_id, character_name, character_color, type, label,
dice_expression, rolls, modifier, total, advantage, disadvantage, nat20)
VALUES (?, ?, ?, ?, 'custom', ?, '1d20', ?, 0, ?, 0, 0, ?)`,
[
data.campaignId,
data.characterId,
char.name,
char.color,
label,
JSON.stringify(result.rolls),
roll,
nat20 ? 1 : 0,
]
);
const [savedRows] = await db.execute<RowDataPacket[]>(
"SELECT * FROM roll_log WHERE id = ?",
[insertResult.insertId]
);
io.to(`campaign:${data.campaignId}`).emit("roll:result", {
...savedRows[0],
rolls: result.rolls,
advantage: false,
disadvantage: false,
nat20,
});
// On 18+: heal to 1 HP and clear Dying condition
if (success) {
await db.execute("UPDATE characters SET hp_current = 1 WHERE id = ?", [data.characterId]);
await db.execute(
"DELETE FROM character_conditions WHERE character_id = ? AND name = 'Dying'",
[data.characterId]
);
const [updatedChar] = await db.execute<RowDataPacket[]>(
"SELECT * FROM characters WHERE id = ?",
[data.characterId]
);
const [updatedConditions] = await db.execute<RowDataPacket[]>(
"SELECT * FROM character_conditions WHERE character_id = ?",
[data.characterId]
);
io.to(`campaign:${data.campaignId}`).emit("character:updated", {
...updatedChar[0],
conditions: updatedConditions,
});
}
});
socket.on("disconnect", () => { socket.on("disconnect", () => {
// Rooms are cleaned up automatically by Socket.IO // Rooms are cleaned up automatically by Socket.IO
}); });