diff --git a/site/assets/screenshots/atmosphere.png b/site/assets/screenshots/atmosphere.png new file mode 100644 index 0000000..c663fb7 Binary files /dev/null and b/site/assets/screenshots/atmosphere.png differ diff --git a/site/assets/screenshots/character-creation.png b/site/assets/screenshots/character-creation.png new file mode 100644 index 0000000..26789d8 Binary files /dev/null and b/site/assets/screenshots/character-creation.png differ diff --git a/site/assets/screenshots/character-sheet.png b/site/assets/screenshots/character-sheet.png new file mode 100644 index 0000000..b6dfb85 Binary files /dev/null and b/site/assets/screenshots/character-sheet.png differ diff --git a/site/assets/screenshots/dice-roll.png b/site/assets/screenshots/dice-roll.png new file mode 100644 index 0000000..ff42f4f Binary files /dev/null and b/site/assets/screenshots/dice-roll.png differ diff --git a/site/assets/screenshots/dm-cards.png b/site/assets/screenshots/dm-cards.png new file mode 100644 index 0000000..e9c818f Binary files /dev/null and b/site/assets/screenshots/dm-cards.png differ diff --git a/site/assets/screenshots/initiative-active.png b/site/assets/screenshots/initiative-active.png new file mode 100644 index 0000000..2daf14a Binary files /dev/null and b/site/assets/screenshots/initiative-active.png differ diff --git a/site/assets/screenshots/spellcasting.png b/site/assets/screenshots/spellcasting.png new file mode 100644 index 0000000..4f54c66 Binary files /dev/null and b/site/assets/screenshots/spellcasting.png differ diff --git a/site/screenshots.js b/site/screenshots.js index ad2d194..8ae9292 100644 --- a/site/screenshots.js +++ b/site/screenshots.js @@ -42,69 +42,195 @@ async function openCampaign(page) { await page.waitForTimeout(1500); // socket connect + data load } +// Navigate back to a clean campaign view (no modals, no combat) +async function resetToCleanCampaign(page) { + const url = page.url(); + await page.goto(url); + await page.waitForTimeout(1500); +} + // ── captures ─────────────────────────────────────────────────────────────── -async function captureCharacterSheet(page) { - // DM view — click the first character card to open full sheet - // Selector: first .characterCard element in the card grid - // Adjust class name if different in the actual DOM - const card = page.locator('[class*="characterCard"]').first(); - await card.click(); - await page.waitForTimeout(500); -} - -async function captureCharacterCreation(page) { - // Open character creation wizard and advance to step 2 (stat rolling) - await page.click('button:has-text("New Character")'); - await page.waitForTimeout(400); - // Fill required fields on step 1 so Next is enabled - const nameInput = page.locator('input[placeholder*="name" i]').first(); - await nameInput.fill('Screenshot Hero'); - // Advance to step 2 - await page.click('button:has-text("Next")'); - await page.waitForTimeout(400); -} - -async function captureDiceRoll(page) { - // Roll a d20 — find the d20 button in the dice panel - await page.click('button:has-text("d20")'); - await page.waitForTimeout(2000); // wait for 3D animation to settle -} - -async function captureSpellcasting(page) { - // Open the Spells tab/panel for the first Wizard or Priest character - // The spell panel may be a tab within the character sheet - await page.click('text=Spells'); - await page.waitForTimeout(400); -} - -async function captureAtmosphere(page) { - // Open atmosphere panel and enable fog - await page.click('button[title*="tmosphere" i], button:has-text("Atmosphere")'); - await page.waitForTimeout(300); - await page.click('button:has-text("Fog")'); - await page.waitForTimeout(600); // wait for fog to render -} - -async function captureInitiativeActive(page) { - // Click the Combat button to open the start combat modal - await page.click('button:has-text("Combat")'); - await page.waitForTimeout(300); - // Start combat with whatever defaults are present - await page.click('button:has-text("Start Combat")'); - await page.waitForTimeout(500); - // Roll initiative for enemies (DM roll) - const rollBtn = page.locator('button:has-text("Roll")').first(); - await rollBtn.click(); - await page.waitForTimeout(400); -} - async function captureDmCards(page) { // The DM compact card grid is the default campaign view. // Navigate back to campaign root to ensure no modals are open. - const url = page.url(); - await page.goto(url); - await page.waitForTimeout(1500); // let socket reconnect + await resetToCleanCampaign(page); +} + +async function captureCharacterSheet(page) { + // Character cards have an inline border-left-width style unique to them. + // CSS modules hash class names, so we use the stable inline style instead. + // Click the top of the card (header/name area) — the middle of the card + // has a vitalsRow with stopPropagation that would block the sheet from opening. + const card = page.locator('[style*="border-left-width: 3px"]').first(); + await card.waitFor({ state: 'visible', timeout: 5000 }); + await card.click({ position: { x: 80, y: 18 } }); // top portion = avatar/name row + await page.waitForTimeout(800); + // Screenshot is taken here (modal open, stat block visible) + // Close the modal after so subsequent captures start clean +} + +async function captureCharacterCreation(page) { + // Navigate back to ensure no modals are open (CharacterDetail overlay + // from the previous capture doesn't respond to Escape — needs a page reload) + await resetToCleanCampaign(page); + // Button text is "+ Add Character" + await page.click('button:has-text("Add Character")'); + await page.waitForTimeout(400); + // Step 1: fill name (placeholder: "Tharyn the Bold…") + const nameInput = page.locator('input[placeholder*="Tharyn"]').first(); + await nameInput.fill('Screenshot Hero'); + // Advance to step 2 (Ability Scores — shows the stat rolling UI) + await page.click('button:has-text("Next")'); + await page.waitForTimeout(400); + // Screenshot is taken here (stat grid + "Reroll All" button visible) + // Cancel the wizard after +} + +async function captureDiceRoll(page) { + // Close any open wizard/modal and return to campaign view + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + await resetToCleanCampaign(page); + + // Open the first character sheet — click the header area to avoid the + // vitalsRow which has stopPropagation + const card = page.locator('[style*="border-left-width: 3px"]').first(); + await card.waitFor({ state: 'visible', timeout: 5000 }); + await card.click({ position: { x: 80, y: 18 } }); + await page.waitForTimeout(800); + + // Click a stat dice button — title is "Roll 1d20±N (Shift: advantage...)" + // The stat block may be scrolled below the fold in the modal — use scrollIntoViewIfNeeded + const statDie = page.locator('button[title^="Roll 1d20"]').first(); + await statDie.waitFor({ state: 'attached', timeout: 5000 }); + await statDie.scrollIntoViewIfNeeded(); + await page.waitForTimeout(200); + await statDie.click(); + await page.waitForTimeout(2200); // wait for 3D dice animation to settle + // Screenshot captures the DiceTray overlay + roll log +} + +async function captureSpellcasting(page) { + // Need a Wizard or Priest character to show the spell panel. + // Re-use one if it already exists (avoids accumulating characters across runs). + await resetToCleanCampaign(page); + + let casterCard = page.locator('[style*="border-left-width: 3px"]') + .filter({ hasText: /Wizard|Priest/ }).first(); + + const hasCaster = await casterCard.isVisible().catch(() => false); + if (!hasCaster) { + // Create a Wizard character + await page.click('button:has-text("Add Character")'); + await page.waitForTimeout(400); + + const nameInput = page.locator('input[placeholder*="Tharyn"]').first(); + await nameInput.fill('Morgana'); + + // Select Wizard from the Class dropdown (trigger shows "Fighter" by default) + await page.locator('button:has-text("Fighter")').first().click(); + await page.waitForTimeout(200); + await page.locator('button:has-text("Wizard")').last().click(); + await page.waitForTimeout(200); + + // Step 1 → Step 2 (stats) + await page.click('button:has-text("Next")'); + await page.waitForTimeout(300); + // Step 2 → Step 3 (background) + await page.click('button:has-text("Next")'); + await page.waitForTimeout(300); + // Pick a random background and advance + await page.click('button:has-text("Random Background")'); + await page.waitForTimeout(200); + await page.click('button:has-text("Next")'); + await page.waitForTimeout(300); + // Create the character + await page.click('button:has-text("Create Character")'); + await page.waitForTimeout(1200); + + // Re-resolve the locator after creation + casterCard = page.locator('[style*="border-left-width: 3px"]') + .filter({ hasText: /Wizard|Priest/ }).first(); + } + + await casterCard.waitFor({ state: 'visible', timeout: 5000 }); + await casterCard.click({ position: { x: 80, y: 18 } }); + await page.waitForTimeout(600); + + // Enter edit mode to add a spell + await page.click('button:has-text("Edit")'); + await page.waitForTimeout(300); + + // Open the spell picker + await page.click('button:has-text("Add Spell")'); + await page.waitForTimeout(500); // wait for spell list to load + + // Pick the first available spell + const firstSpell = page.locator('[class*="pickerItem"]').first(); + await firstSpell.waitFor({ state: 'visible', timeout: 5000 }); + await firstSpell.click(); + await page.waitForTimeout(300); + + // Exit edit mode + await page.click('button:has-text("Done")'); + await page.waitForTimeout(300); + + // Cast the spell to get an exhausted state (shows the exhausted indicator) + const castBtn = page.locator('button:has-text("Cast")').first(); + await castBtn.waitFor({ state: 'visible', timeout: 3000 }); + await castBtn.click(); + await page.waitForTimeout(800); + + // Close any cast result modal + const resultClose = page.locator('button:has-text("Close"), button:has-text("OK")').first(); + if (await resultClose.isVisible().catch(() => false)) { + await resultClose.click(); + await page.waitForTimeout(200); + } + // Screenshot captures the spell list with slot/exhausted state visible +} + +async function captureAtmosphere(page) { + // Navigate back to ensure the campaign view is accessible (no open modals) + await resetToCleanCampaign(page); + // Open the atmosphere panel — trigger button has title="Atmosphere effects" + await page.click('button[title="Atmosphere effects"]'); + await page.waitForTimeout(300); + + // Toggle Fog on — the toggle button has aria-label="Toggle Fog" + await page.click('button[aria-label="Toggle Fog"]'); + await page.waitForTimeout(800); // wait for fog to render + // Screenshot captures fog effect over the campaign view + + // Close the atmosphere panel after so it doesn't clutter other captures + await page.keyboard.press('Escape'); + await page.waitForTimeout(200); +} + +async function captureInitiativeActive(page) { + await resetToCleanCampaign(page); + + // Open combat start modal — button text is "⚔ Combat" + await page.click('button:has-text("Combat")'); + await page.waitForTimeout(400); + + // Add an enemy so the Roll Initiative button is fully useful + // (characters are pre-selected by default, so canStart is already true) + 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('Goblin'); + const hpInput = page.locator('input[placeholder*="HP"], input[placeholder*="hp" i], input[type="number"]').first(); + if (await hpInput.isVisible().catch(() => false)) { + await hpInput.fill('5'); + } + } + + // Click "Roll Initiative ⚔" to start combat + await page.click('button:has-text("Roll Initiative")'); + await page.waitForTimeout(1200); // wait for initiative state to sync via socket + + // Screenshot captures the initiative tracker with party + enemies rolled } // ── main ───────────────────────────────────────────────────────────────────