diff --git a/site/assets/screenshots/death-timer.png b/site/assets/screenshots/death-timer.png new file mode 100644 index 0000000..9d3611a Binary files /dev/null and b/site/assets/screenshots/death-timer.png differ diff --git a/site/index.html b/site/index.html index 713c284..6ece4ae 100644 --- a/site/index.html +++ b/site/index.html @@ -145,6 +145,23 @@ + +
+
+ Initiative tracker showing a dying character with skull countdown and Roll Recovery button +
+
+
Death Timer
+

Every round could be the last.

+

+ When a character drops to 0 HP, a countdown begins: 1d4 + CON modifier rounds + before permanent death. The DM sees the timer on the card and can roll a d20 + recovery save from the initiative tracker — an 18 or higher lets the character + stand at 1 HP. Run out the clock and they're gone for good. +

+
+
+
diff --git a/site/screenshots.js b/site/screenshots.js index cd928e9..c2be443 100644 --- a/site/screenshots.js +++ b/site/screenshots.js @@ -245,6 +245,45 @@ async function captureInitiativeActive(page) { // Screenshot captures the initiative tracker with party + enemies rolled } +async function captureDeathTimer(page) { + await resetToCleanCampaign(page); + + // Start combat with an enemy so the initiative tracker is visible + await page.click('button:has-text("Combat")'); + await page.waitForTimeout(400); + + const enemyNameInput = page.locator('input[placeholder*="Goblin"], input[placeholder*="enemy" i], input[placeholder*="name" i]').first(); + if (await enemyNameInput.isVisible().catch(() => false)) { + await enemyNameInput.fill('Skeleton'); + const hpInput = page.locator('input[placeholder*="HP"], input[placeholder*="hp" i], input[type="number"]').first(); + if (await hpInput.isVisible().catch(() => false)) { + await hpInput.fill('8'); + } + } + + // Roll initiative to enter the active phase + await page.click('button:has-text("Roll Initiative")'); + await page.waitForTimeout(1500); + + // Bring the first character to 0 HP via the API — server rolls dying timer automatically + await page.evaluate(async () => { + const campaignId = Number(location.pathname.match(/\/campaign\/(\d+)/)?.[1]); + const res = await fetch(`/api/campaigns/${campaignId}/characters`, { credentials: 'include' }); + const chars = await res.json(); + if (chars.length > 0) { + await fetch(`/api/characters/${chars[0].id}`, { + method: 'PATCH', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hp_current: 0 }), + }); + } + }); + await page.waitForTimeout(1500); // wait for socket broadcast + React re-render + // Screenshot shows initiative tracker with dying character (skull tag + Roll Recovery button) + // and DM card with pulsing red border + skull countdown +} + // ── main ─────────────────────────────────────────────────────────────────── async function run() { @@ -273,6 +312,7 @@ async function run() { await shot(page, 'spellcasting', captureSpellcasting); await shot(page, 'atmosphere', captureAtmosphere); await shot(page, 'initiative-active', captureInitiativeActive); + await shot(page, 'death-timer', captureDeathTimer); await browser.close(); console.log('');