From f853fdbce39f4dea07f7a2b08d5d3feffa29d1fd Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sun, 12 Apr 2026 01:32:24 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20death:recovery-roll=20=E2=80=94=20add=20?= =?UTF-8?q?is=5Fdead=20guard,=20try/catch,=20ownership=20check,=20subtype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/socket.ts | 159 +++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 73 deletions(-) diff --git a/server/src/socket.ts b/server/src/socket.ts index efef1b8..ecc9974 100644 --- a/server/src/socket.ts +++ b/server/src/socket.ts @@ -158,89 +158,102 @@ export function setupSocket(io: Server) { campaignId: number; characterId: number; }) => { - const userId = socket.data.user?.userId; + try { + const userId = socket.data.user?.userId; - // Verify caller is DM - const [memberRows] = await db.execute( - "SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?", - [data.campaignId, userId] - ); - if (memberRows.length === 0 || memberRows[0].role !== "dm") return; + // Verify caller is DM + const [memberRows] = await db.execute( + "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( - "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( - "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( - `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( - "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'", + // Verify character has a Dying condition + const [dyingRows] = await db.execute( + "SELECT id FROM character_conditions WHERE character_id = ? AND name = 'Dying'", [data.characterId] ); + if (dyingRows.length === 0) return; - const [updatedChar] = await db.execute( - "SELECT * FROM characters WHERE id = ?", + // Verify character is not permanently dead + const [deadCheck] = await db.execute( + "SELECT is_dead FROM characters WHERE id = ?", [data.characterId] ); - const [updatedConditions] = await db.execute( - "SELECT * FROM character_conditions WHERE character_id = ?", + if (deadCheck.length === 0 || deadCheck[0].is_dead) return; + + // Get character info for roll log + const [charRows] = await db.execute( + "SELECT name, color, campaign_id FROM characters WHERE id = ?", [data.characterId] ); - io.to(`campaign:${data.campaignId}`).emit("character:updated", { - ...updatedChar[0], - conditions: updatedConditions, + if (charRows.length === 0) return; + const char = charRows[0]; + + if (char.campaign_id !== data.campaignId) return; + + // 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( + `INSERT INTO roll_log + (campaign_id, character_id, character_name, character_color, type, subtype, label, + dice_expression, rolls, modifier, total, advantage, disadvantage, nat20) + VALUES (?, ?, ?, ?, 'custom', 'death-save', ?, '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( + "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( + "SELECT * FROM characters WHERE id = ?", + [data.characterId] + ); + const [updatedConditions] = await db.execute( + "SELECT * FROM character_conditions WHERE character_id = ?", + [data.characterId] + ); + io.to(`campaign:${data.campaignId}`).emit("character:updated", { + ...updatedChar[0], + conditions: updatedConditions, + }); + } + } catch (err) { + console.error("[death:recovery-roll]", err); } });