diff --git a/projects/sasch40/frontend/.dockerignore b/projects/sasch40/frontend/.dockerignore new file mode 100644 index 0000000..c20f120 --- /dev/null +++ b/projects/sasch40/frontend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +scripts +*.log +.DS_Store diff --git a/projects/sasch40/frontend/Dockerfile b/projects/sasch40/frontend/Dockerfile index dc424d7..335fcb5 100644 --- a/projects/sasch40/frontend/Dockerfile +++ b/projects/sasch40/frontend/Dockerfile @@ -1,18 +1,6 @@ -FROM node:22-alpine AS build - -WORKDIR /app - -COPY package.json package-lock.json* ./ - -RUN npm install - -COPY . . - -RUN npm run build - FROM nginx:alpine -COPY --from=build /app/dist /usr/share/nginx/html +COPY dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 diff --git a/projects/sasch40/frontend/Makefile b/projects/sasch40/frontend/Makefile index 7991028..59f43ba 100644 --- a/projects/sasch40/frontend/Makefile +++ b/projects/sasch40/frontend/Makefile @@ -5,13 +5,16 @@ PLATFORM ?= linux/amd64 IMAGE ?= $(REGISTRY)/$(IMAGE_NAME):$(TAG) -.PHONY: build tag push all dev +.PHONY: build tag push all dev compile deploy -all: build tag push +all: compile build tag push deploy dev: npm run dev +compile: + npm run build + build: docker build --platform $(PLATFORM) -t $(IMAGE_NAME):$(TAG) . @@ -20,3 +23,7 @@ tag: push: docker push $(IMAGE) + +deploy: + kubectl rollout restart deployment/sasch40 -n web + kubectl rollout status deployment/sasch40 -n web diff --git a/projects/sasch40/frontend/package-lock.json b/projects/sasch40/frontend/package-lock.json index 6f85141..fd838b3 100644 --- a/projects/sasch40/frontend/package-lock.json +++ b/projects/sasch40/frontend/package-lock.json @@ -17,6 +17,7 @@ "@types/canvas-confetti": "^1.9.0", "@types/node": "^22.10.2", "@vitejs/plugin-vue": "^5.2.1", + "puppeteer-core": "^25.12.0", "typescript": "^5.7.2", "vite": "^6.0.3", "vue-tsc": "^2.2.0" @@ -536,6 +537,35 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@puppeteer/browsers": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.63.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", @@ -1139,6 +1169,32 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1166,6 +1222,56 @@ "url": "https://www.paypal.me/kirilvatev" } }, + "node_modules/chromium-bidi": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1179,6 +1285,20 @@ "dev": true, "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1233,6 +1353,16 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -1272,6 +1402,29 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1317,6 +1470,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -1418,6 +1588,24 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/puppeteer-core": { + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/rollup": { "version": "4.63.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", @@ -1473,6 +1661,39 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1490,6 +1711,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1656,6 +1884,119 @@ "peerDependencies": { "typescript": ">=5.0.0" } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/projects/sasch40/frontend/package.json b/projects/sasch40/frontend/package.json index 01984ef..4ae0389 100644 --- a/projects/sasch40/frontend/package.json +++ b/projects/sasch40/frontend/package.json @@ -18,6 +18,7 @@ "@types/canvas-confetti": "^1.9.0", "@types/node": "^22.10.2", "@vitejs/plugin-vue": "^5.2.1", + "puppeteer-core": "^25.12.0", "typescript": "^5.7.2", "vite": "^6.0.3", "vue-tsc": "^2.2.0" diff --git a/projects/sasch40/frontend/public/assets/scenes/tavern.jpg b/projects/sasch40/frontend/public/assets/scenes/tavern.jpg index 497d730..d35fbd5 100644 Binary files a/projects/sasch40/frontend/public/assets/scenes/tavern.jpg and b/projects/sasch40/frontend/public/assets/scenes/tavern.jpg differ diff --git a/projects/sasch40/frontend/scripts/playtest.mjs b/projects/sasch40/frontend/scripts/playtest.mjs new file mode 100644 index 0000000..e2066d9 --- /dev/null +++ b/projects/sasch40/frontend/scripts/playtest.mjs @@ -0,0 +1,288 @@ +import puppeteer from 'puppeteer-core'; +import path from 'path'; +import fs from 'fs'; + +const ARTIFACTS_DIR = '/Users/moritz/.gemini/antigravity/brain/26049ae8-3106-42e0-922f-356dbbfe162a'; +const SCREENSHOTS_DIR = path.join(ARTIFACTS_DIR, 'playtest_screenshots'); +if (!fs.existsSync(SCREENSHOTS_DIR)) { + fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true }); +} + +async function runPlaytest() { + console.log('🚀 Starting Chrome Playtest for Saschas Kronkorken Kreuzzug...'); + + const logs = []; + const errors = []; + const failedRequests = []; + + const browser = await puppeteer.launch({ + executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: 'new', + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu', + '--window-size=412,869' + ] + }); + + const page = await browser.newPage(); + await page.setViewport({ + width: 412, + height: 869, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true + }); + + page.on('console', msg => { + const text = msg.text(); + logs.push(`[${msg.type().toUpperCase()}] ${text}`); + if (msg.type() === 'error') { + errors.push(`Console Error: ${text}`); + } + }); + + page.on('pageerror', err => { + errors.push(`Page Error: ${err.toString()}`); + }); + + page.on('requestfailed', req => { + failedRequests.push(`Failed Request: ${req.url()} (${req.failure()?.errorText || 'unknown'})`); + }); + + page.on('response', resp => { + if (resp.status() >= 400) { + failedRequests.push(`HTTP ${resp.status()}: ${resp.url()}`); + } + }); + + try { + console.log('🌐 Navigating to https://sasch40.haumdaucher.de...'); + await page.goto('https://sasch40.haumdaucher.de', { waitUntil: 'networkidle0', timeout: 30000 }); + + // 1. INTRO SCREEN + console.log('📸 Capturing Intro Screen...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '01_intro_screen.png') }); + + // Inspect layout and controls presence in Intro + const introControlsVisible = await page.evaluate(() => { + const dpad = document.querySelector('.dpad'); + const intro = document.querySelector('.intro-screen'); + const chassis = document.querySelector('.gameboy-chassis'); + return { + hasDpad: !!dpad, + dpadOffsetHeight: dpad ? (dpad.offsetHeight > 0) : false, + hasIntro: !!intro, + layoutClass: chassis ? chassis.className : '' + }; + }); + console.log('Intro Layout Check:', introControlsVisible); + + // Click Start Button + console.log('🖱️ Clicking Start Button...'); + await page.click('.start-btn'); + await new Promise(r => setTimeout(r, 1000)); + + // 2. PROLOGUE SCREEN + console.log('📸 Capturing Prologue Screen...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') }); + + // Advance Prologue Dialogues + console.log('💬 Advancing Prologue Dialogues (clicking choice buttons)...'); + // Click choice 1 on 'start' + await page.waitForSelector('.choice-btn', { timeout: 5000 }); + const firstChoice = await page.$('.choice-btn'); + if (firstChoice) { + await firstChoice.click(); + await new Promise(r => setTimeout(r, 800)); + console.log('📸 Capturing Mop Appears Prologue Dialogue...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') }); + } + + // Click choice on 'mop_appears' -> triggers 'startPlatformer' + const mopChoice = await page.$('.choice-btn'); + if (mopChoice) { + await mopChoice.click(); + await new Promise(r => setTimeout(r, 1200)); + } + + // 3. PLATFORMER SCREEN + console.log('📸 Capturing Platformer (Jump & Run)...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_screen.png') }); + + const platformerControls = await page.evaluate(() => { + const dpad = document.querySelector('.dpad'); + const btnA = document.querySelector('.btn-a'); + const canvas = document.querySelector('canvas'); + return { + hasDpad: !!dpad, + hasBtnA: !!btnA, + hasCanvas: !!canvas, + canvasW: canvas?.width, + canvasH: canvas?.height + }; + }); + console.log('Platformer Controls Check:', platformerControls); + + // Test Jump & Move + console.log('🎮 Testing Jump & Move Controls in Platformer...'); + const btnA = await page.$('.btn-a'); + if (btnA) { + await btnA.click(); + await new Promise(r => setTimeout(r, 400)); + await btnA.click(); + await new Promise(r => setTimeout(r, 400)); + } + const dpadRight = await page.$('.dpad-right'); + if (dpadRight) { + await dpadRight.hover(); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03b_platformer_jump.png') }); + } + + // Directly set act to 'tavern' to test Tavern Point & Click + console.log('🍺 Advancing to Tavern Act...'); + await page.evaluate(() => { + const chassis = document.querySelector('.gameboy-chassis'); + // access pinia store from window if exposed, or trigger level finish + // We can click the level finish or evaluate store change + window.dispatchEvent(new CustomEvent('test-set-act', { detail: 'tavern' })); + }); + + // Check if act switched, or use page.evaluate to trigger store change + await page.evaluate(() => { + // Find Pinia root if possible + const app = document.querySelector('#app')?.__vue_app__; + if (app && app.config?.globalProperties?.$pinia) { + const pinia = app.config.globalProperties.$pinia; + // find game store + for (const [id, store] of pinia._s.entries()) { + if (id === 'game') { + store.setAct('tavern'); + } + } + } + }); + + await new Promise(r => setTimeout(r, 1000)); + console.log('📸 Capturing Tavern Screen...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_tavern_screen.png') }); + + // Test Tavern Hotspots + console.log('👥 Testing Verena Hotspot...'); + const verenaBtn = await page.$('.hotspot-verena'); + if (verenaBtn) { + await verenaBtn.click(); + await new Promise(r => setTimeout(r, 800)); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_verena_dialog.png') }); + const actionBtn = await page.$('.dialog-actions .action-btn'); + if (actionBtn) await actionBtn.click(); + await new Promise(r => setTimeout(r, 500)); + } + + console.log('🔨 Testing Armend Hotspot...'); + const armendBtn = await page.$('.hotspot-armend'); + if (armendBtn) { + await armendBtn.click(); + await new Promise(r => setTimeout(r, 800)); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_armend_dialog.png') }); + const actionBtn = await page.$('.dialog-actions .action-btn'); + if (actionBtn) await actionBtn.click(); + await new Promise(r => setTimeout(r, 500)); + } + + console.log('🪞 Testing Zuluman Magic Mirror Hotspot...'); + const mirrorBtn = await page.$('.hotspot-mirror'); + if (mirrorBtn) { + await mirrorBtn.click(); + await new Promise(r => setTimeout(r, 800)); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_zuluman_mirror.png') }); + const closeBtn = await page.$('.mirror-box .action-btn'); + if (closeBtn) await closeBtn.click(); + await new Promise(r => setTimeout(r, 500)); + } + + console.log('⚔️ Testing Gstanzl-Duel...'); + const duelBtn = await page.$('.duel-trigger'); + if (duelBtn) { + await duelBtn.click(); + await new Promise(r => setTimeout(r, 800)); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_gstanzl_duel.png') }); + + // Answer Gstanzl question + const correctOption = await page.$('.option-btn'); + if (correctOption) { + await correctOption.click(); + await new Promise(r => setTimeout(r, 600)); + } + } + + // 4. PARTY EXPRESS (Act IV) + console.log('🚂 Advancing to Party Express (Act IV)...'); + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + if (app && app.config?.globalProperties?.$pinia) { + const pinia = app.config.globalProperties.$pinia; + for (const [id, store] of pinia._s.entries()) { + if (id === 'game') store.setAct('party_express'); + } + } + }); + await new Promise(r => setTimeout(r, 1000)); + console.log('📸 Capturing Party Express...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_party_express.png') }); + + // 5. FINALE / CERTIFICATE (Act V) + console.log('👑 Advancing to Finale Certificate (Act V)...'); + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + if (app && app.config?.globalProperties?.$pinia) { + const pinia = app.config.globalProperties.$pinia; + for (const [id, store] of pinia._s.entries()) { + if (id === 'game') { + store.addKronkorken(25); + store.serveGuest(); + store.serveGuest(); + store.setAct('finale'); + } + } + } + }); + await new Promise(r => setTimeout(r, 1500)); + console.log('📸 Capturing Certificate Modal...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_certificate_screen.png') }); + + // Check images in document + const brokenImages = await page.evaluate(() => { + const imgs = Array.from(document.querySelectorAll('img')); + return imgs + .filter(img => !img.complete || img.naturalWidth === 0) + .map(img => ({ src: img.src, alt: img.alt })); + }); + + console.log('\n--- PLAYTEST REPORT ---'); + console.log('Total Console Logs:', logs.length); + console.log('Console Errors:', errors); + console.log('Failed Requests:', failedRequests); + console.log('Broken Images on Certificate Page:', brokenImages); + + const report = { + timestamp: new Date().toISOString(), + errors, + failedRequests, + brokenImages, + screenshots: fs.readdirSync(SCREENSHOTS_DIR) + }; + fs.writeFileSync(path.join(ARTIFACTS_DIR, 'playtest_report.json'), JSON.stringify(report, null, 2)); + + } catch (err) { + console.error('Playtest error:', err); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, 'error_state.png') }).catch(() => {}); + } finally { + await browser.close(); + console.log('🏁 Playtest browser closed.'); + } +} + +runPlaytest(); diff --git a/projects/sasch40/frontend/src/components/CertificateModal.vue b/projects/sasch40/frontend/src/components/CertificateModal.vue index f727e76..f37c77c 100644 --- a/projects/sasch40/frontend/src/components/CertificateModal.vue +++ b/projects/sasch40/frontend/src/components/CertificateModal.vue @@ -150,56 +150,56 @@ function printOrSave() { height: 100%; background: #1a0f0a; overflow-y: auto; - padding: 12px; + padding: 8px 10px; display: flex; flex-direction: column; align-items: center; - gap: 12px; + gap: 8px; } .certificate-paper { background: #fdf6e2; - border: 6px double #854d0e; - border-radius: 12px; - padding: 16px 14px; + border: 4px double #854d0e; + border-radius: 10px; + padding: 10px 10px; width: 100%; max-width: 330px; text-align: center; color: #451a03; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.7); position: relative; } .royal-seal { - font-size: 16px; + font-size: 14px; font-weight: bold; color: #b45309; - margin-bottom: 4px; + margin-bottom: 2px; } .cert-title { font-family: 'Press Start 2P', monospace; - font-size: 9px; + font-size: 8px; color: #78350f; - margin: 0 0 6px 0; + margin: 0 0 3px 0; } .cert-subtitle { font-family: 'MedievalSharp', serif; - font-size: 17px; + font-size: 14.5px; font-weight: bold; color: #b91c1c; - margin: 0 0 10px 0; + margin: 0 0 6px 0; } .king-portrait-wrapper { - margin: 0 auto 10px auto; - width: 90px; - height: 90px; - border: 3px solid #d4af37; + margin: 0 auto 6px auto; + width: 68px; + height: 68px; + border: 2.5px solid #d4af37; border-radius: 50%; overflow: hidden; - box-shadow: 0 4px 10px rgba(0,0,0,0.3); + box-shadow: 0 3px 8px rgba(0,0,0,0.3); } .king-portrait { @@ -210,88 +210,88 @@ function printOrSave() { .decree-text { font-family: 'MedievalSharp', serif; - font-size: 13px; - margin: 4px 0; + font-size: 12px; + margin: 2px 0; } .honoree-name { font-family: 'Press Start 2P', monospace; - font-size: 12px; + font-size: 11px; color: #991b1b; - margin: 6px 0 2px 0; + margin: 4px 0 2px 0; } .honoree-title { font-family: 'MedievalSharp', serif; - font-size: 14px; + font-size: 13px; font-style: italic; font-weight: bold; color: #b45309; - margin: 0 0 6px 0; + margin: 0 0 4px 0; } .decree-details { font-family: 'MedievalSharp', serif; - font-size: 13px; - line-height: 1.3; - margin: 0 0 10px 0; + font-size: 12px; + line-height: 1.25; + margin: 0 0 8px 0; } .cert-stats { display: grid; grid-template-columns: 1fr 1fr; - gap: 6px; - margin-bottom: 12px; + gap: 4px; + margin-bottom: 8px; } .stat-card { background: rgba(217, 119, 6, 0.15); border: 1px solid #d97706; - border-radius: 6px; - padding: 4px; + border-radius: 5px; + padding: 3px; display: flex; flex-direction: column; } .stat-val { font-family: 'Press Start 2P', monospace; - font-size: 9px; + font-size: 8.5px; color: #92400e; } .stat-lbl { - font-size: 9px; + font-size: 8px; color: #78350f; font-weight: bold; } .score-banner { background: #1e1b18; - border: 3px solid #ffd700; - border-radius: 8px; - padding: 10px 8px; - margin-bottom: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + border: 2px solid #ffd700; + border-radius: 6px; + padding: 6px 6px; + margin-bottom: 8px; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4); } .score-rank-title { font-family: 'Press Start 2P', monospace; - font-size: 9px; - margin-bottom: 4px; - text-shadow: 0 0 8px currentColor; + font-size: 8px; + margin-bottom: 2px; + text-shadow: 0 0 6px currentColor; } .score-number { font-family: 'Press Start 2P', monospace; - font-size: 14px; + font-size: 12px; color: #fff; - letter-spacing: 1px; - margin: 4px 0; + letter-spacing: 0.5px; + margin: 2px 0; } .score-subtitle { font-family: 'MedievalSharp', serif; - font-size: 13px; + font-size: 12px; font-style: italic; color: #fef08a; } @@ -300,21 +300,21 @@ function printOrSave() { background: rgba(254, 243, 199, 0.7); border: 1px solid #d97706; border-radius: 6px; - padding: 8px; - margin-bottom: 10px; + padding: 6px; + margin-bottom: 8px; font-family: 'MedievalSharp', serif; - font-size: 12px; + font-size: 11.5px; text-align: left; } .breakdown-header { font-family: 'Press Start 2P', monospace; - font-size: 8px; + font-size: 7.5px; color: #78350f; - margin-bottom: 6px; + margin-bottom: 4px; text-align: center; border-bottom: 1px dashed #b45309; - padding-bottom: 4px; + padding-bottom: 3px; } .breakdown-row { diff --git a/projects/sasch40/frontend/src/components/GameBoyFrame.vue b/projects/sasch40/frontend/src/components/GameBoyFrame.vue index 584012f..a148c76 100644 --- a/projects/sasch40/frontend/src/components/GameBoyFrame.vue +++ b/projects/sasch40/frontend/src/components/GameBoyFrame.vue @@ -144,8 +144,8 @@ function toggleInventory() { - -