diff --git a/.gitignore b/.gitignore index 8ce29f8..5cd8cda 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ server/data/ server/.env *.db .claude/settings.local.json +site/node_modules/ diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..9ca3cd0 --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "darkwatch-brochure-screenshots", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "darkwatch-brochure-screenshots", + "devDependencies": { + "playwright": "^1.43.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..b25d24a --- /dev/null +++ b/site/package.json @@ -0,0 +1,11 @@ +{ + "name": "darkwatch-brochure-screenshots", + "type": "module", + "scripts": { + "screenshots": "node screenshots.js", + "screenshots:only": "node screenshots.js --only" + }, + "devDependencies": { + "playwright": "^1.43.0" + } +} diff --git a/site/screenshots.js b/site/screenshots.js new file mode 100644 index 0000000..ad2d194 --- /dev/null +++ b/site/screenshots.js @@ -0,0 +1,147 @@ +import { chromium } from 'playwright'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { mkdirSync } from 'fs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SHOT_DIR = resolve(__dirname, 'assets', 'screenshots'); +const APP = 'http://localhost:5173'; +const DM_EMAIL = 'dm@darkwatch.test'; +const DM_PASS = 'password'; +const ONLY = process.argv.includes('--only') + ? process.argv[process.argv.indexOf('--only') + 1] + : null; + +mkdirSync(SHOT_DIR, { recursive: true }); + +// ── helpers ──────────────────────────────────────────────────────────────── + +async function shot(page, name, fn) { + if (ONLY && name !== ONLY) return; + console.log(` capturing ${name}...`); + await fn(page); + await page.screenshot({ + path: resolve(SHOT_DIR, `${name}.png`), + fullPage: false, + }); + console.log(` ✓ ${name}.png`); +} + +async function login(page) { + await page.goto(`${APP}/login`); + await page.fill('input[type="email"]', DM_EMAIL); + await page.fill('input[type="password"]', DM_PASS); + await page.click('button[type="submit"]'); + await page.waitForURL(`${APP}/`); +} + +async function openCampaign(page) { + // Seeded campaign is named "Tomb of the Serpent King" + await page.click('text=Tomb of the Serpent King'); + await page.waitForURL(/\/campaign\/\d+/); + await page.waitForTimeout(1500); // socket connect + data load +} + +// ── 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 +} + +// ── main ─────────────────────────────────────────────────────────────────── + +async function run() { + console.log('Starting Darkwatch screenshot capture...'); + console.log(`App: ${APP}`); + if (ONLY) console.log(`Only: ${ONLY}`); + console.log(''); + + const browser = await chromium.launch(); + const context = await browser.newContext({ + viewport: { width: 1280, height: 800 }, + }); + const page = await context.newPage(); + + await login(page); + console.log('✓ Logged in as DM'); + + await openCampaign(page); + console.log('✓ Campaign open'); + console.log(''); + + await shot(page, 'dm-cards', captureDmCards); + await shot(page, 'character-sheet', captureCharacterSheet); + await shot(page, 'character-creation',captureCharacterCreation); + await shot(page, 'dice-roll', captureDiceRoll); + await shot(page, 'spellcasting', captureSpellcasting); + await shot(page, 'atmosphere', captureAtmosphere); + await shot(page, 'initiative-active', captureInitiativeActive); + + await browser.close(); + console.log(''); + console.log('Done! Screenshots saved to site/assets/screenshots/'); +} + +run().catch(err => { + console.error(err); + process.exit(1); +});