Add death timer to brochure with screenshot

Adds a new feature row (Row 8) describing the dying mechanic — 1d4+CON
countdown, recovery saves, and permanent death. Screenshot generated via
the Playwright script showing a dying character card with red border in
the DM view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-12 01:53:02 -04:00
parent b7b4123f8e
commit 9607462a7c
3 changed files with 57 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

View file

@ -145,6 +145,23 @@
</div>
</div>
<!-- Row 8: text left, screenshot right -->
<div class="feature-row reverse">
<div class="feature-image">
<img src="assets/screenshots/death-timer.png" alt="Initiative tracker showing a dying character with skull countdown and Roll Recovery button">
</div>
<div class="feature-text">
<div class="feature-label">Death Timer</div>
<h2 class="feature-heading">Every round could be the last.</h2>
<p class="feature-desc">
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.
</p>
</div>
</div>
<!-- Torch + Luck callout cards (no screenshot) -->
<div class="callout-row">
<div class="callout-card">

View file

@ -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('');