feat(sasch40): polish retro game layout, clean tavern scene, and improve mobile HUD
- Hide GameBoy D-Pad and action buttons during RPG and dialogue modes - Add prequel intro landing screen for QR code arrivals with Haumdaucher lore - Clean baked-in text from tavern scene background and streamline navigation bar - Add safe-zone margins and HTML HUD in platformer and party express acts - Optimize Certificate modal layout and scoring breakdown for mobile screens - Add automated end-to-end browser playtest script with Puppeteer - Accelerate Docker build via native pre-compilation
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
scripts
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -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
|
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
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ PLATFORM ?= linux/amd64
|
|||||||
|
|
||||||
IMAGE ?= $(REGISTRY)/$(IMAGE_NAME):$(TAG)
|
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:
|
dev:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
compile:
|
||||||
|
npm run build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker build --platform $(PLATFORM) -t $(IMAGE_NAME):$(TAG) .
|
docker build --platform $(PLATFORM) -t $(IMAGE_NAME):$(TAG) .
|
||||||
|
|
||||||
@@ -20,3 +23,7 @@ tag:
|
|||||||
|
|
||||||
push:
|
push:
|
||||||
docker push $(IMAGE)
|
docker push $(IMAGE)
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
kubectl rollout restart deployment/sasch40 -n web
|
||||||
|
kubectl rollout status deployment/sasch40 -n web
|
||||||
|
|||||||
+341
@@ -17,6 +17,7 @@
|
|||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"puppeteer-core": "^25.12.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0"
|
||||||
@@ -536,6 +537,35 @@
|
|||||||
"node": "^22.20 || ^24.12 || >=25"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.63.4",
|
"version": "4.63.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz",
|
||||||
@@ -1139,6 +1169,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1166,6 +1222,56 @@
|
|||||||
"url": "https://www.paypal.me/kirilvatev"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1179,6 +1285,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/entities": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
@@ -1233,6 +1353,16 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@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": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"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": "^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": {
|
"node_modules/he": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
@@ -1317,6 +1470,23 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/muggle-string": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||||
@@ -1418,6 +1588,24 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/rollup": {
|
||||||
"version": "4.63.4",
|
"version": "4.63.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz",
|
||||||
@@ -1473,6 +1661,39 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -1490,6 +1711,13 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -1656,6 +1884,119 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=5.0.0"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"puppeteer-core": "^25.12.0",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 987 KiB After Width: | Height: | Size: 194 KiB |
@@ -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();
|
||||||
@@ -150,56 +150,56 @@ function printOrSave() {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background: #1a0f0a;
|
background: #1a0f0a;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px;
|
padding: 8px 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-paper {
|
.certificate-paper {
|
||||||
background: #fdf6e2;
|
background: #fdf6e2;
|
||||||
border: 6px double #854d0e;
|
border: 4px double #854d0e;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 16px 14px;
|
padding: 10px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #451a03;
|
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;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.royal-seal {
|
.royal-seal {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-title {
|
.cert-title {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 8px;
|
||||||
color: #78350f;
|
color: #78350f;
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-subtitle {
|
.cert-subtitle {
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 17px;
|
font-size: 14.5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.king-portrait-wrapper {
|
.king-portrait-wrapper {
|
||||||
margin: 0 auto 10px auto;
|
margin: 0 auto 6px auto;
|
||||||
width: 90px;
|
width: 68px;
|
||||||
height: 90px;
|
height: 68px;
|
||||||
border: 3px solid #d4af37;
|
border: 2.5px solid #d4af37;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
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 {
|
.king-portrait {
|
||||||
@@ -210,88 +210,88 @@ function printOrSave() {
|
|||||||
|
|
||||||
.decree-text {
|
.decree-text {
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
margin: 4px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.honoree-name {
|
.honoree-name {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: #991b1b;
|
color: #991b1b;
|
||||||
margin: 6px 0 2px 0;
|
margin: 4px 0 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.honoree-title {
|
.honoree-title {
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.decree-details {
|
.decree-details {
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 1.3;
|
line-height: 1.25;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-stats {
|
.cert-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: rgba(217, 119, 6, 0.15);
|
background: rgba(217, 119, 6, 0.15);
|
||||||
border: 1px solid #d97706;
|
border: 1px solid #d97706;
|
||||||
border-radius: 6px;
|
border-radius: 5px;
|
||||||
padding: 4px;
|
padding: 3px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-val {
|
.stat-val {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 8.5px;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-lbl {
|
.stat-lbl {
|
||||||
font-size: 9px;
|
font-size: 8px;
|
||||||
color: #78350f;
|
color: #78350f;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-banner {
|
.score-banner {
|
||||||
background: #1e1b18;
|
background: #1e1b18;
|
||||||
border: 3px solid #ffd700;
|
border: 2px solid #ffd700;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 10px 8px;
|
padding: 6px 6px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-rank-title {
|
.score-rank-title {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
text-shadow: 0 0 8px currentColor;
|
text-shadow: 0 0 6px currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-number {
|
.score-number {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
margin: 4px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-subtitle {
|
.score-subtitle {
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #fef08a;
|
color: #fef08a;
|
||||||
}
|
}
|
||||||
@@ -300,21 +300,21 @@ function printOrSave() {
|
|||||||
background: rgba(254, 243, 199, 0.7);
|
background: rgba(254, 243, 199, 0.7);
|
||||||
border: 1px solid #d97706;
|
border: 1px solid #d97706;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
font-family: 'MedievalSharp', serif;
|
font-family: 'MedievalSharp', serif;
|
||||||
font-size: 12px;
|
font-size: 11.5px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breakdown-header {
|
.breakdown-header {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 8px;
|
font-size: 7.5px;
|
||||||
color: #78350f;
|
color: #78350f;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px dashed #b45309;
|
border-bottom: 1px dashed #b45309;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breakdown-row {
|
.breakdown-row {
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ function toggleInventory() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Mini Bottom Status Bar for Adventure/Role Play Mode -->
|
<!-- Mini Bottom Status Bar for Adventure/Role Play Mode (Hidden on Finale Certificate) -->
|
||||||
<div v-else class="adventure-bottom-bar">
|
<div v-else-if="store.currentAct !== 'finale'" class="adventure-bottom-bar">
|
||||||
<div class="adv-stat">
|
<div class="adv-stat">
|
||||||
<span class="adv-icon">👑</span>
|
<span class="adv-icon">👑</span>
|
||||||
<span class="adv-val">{{ store.kronkorken }} Korken</span>
|
<span class="adv-val">{{ store.kronkorken }} Korken</span>
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|||||||
let animId: number | null = null
|
let animId: number | null = null
|
||||||
|
|
||||||
let wagonX = 60
|
let wagonX = 60
|
||||||
let wagonY = 220
|
let wagonY = 330
|
||||||
let speed = 3.5
|
let speed = 3.5
|
||||||
let distance = 0
|
let distance = 0
|
||||||
const maxDistance = 2000
|
const maxDistance = 2000
|
||||||
|
|
||||||
interface Guest {
|
interface Guest {
|
||||||
|
id: string
|
||||||
x: number
|
x: number
|
||||||
name: string
|
name: string
|
||||||
served: boolean
|
served: boolean
|
||||||
@@ -21,23 +22,22 @@ interface Guest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let guests: Guest[] = [
|
let guests: Guest[] = [
|
||||||
{ x: 300, name: 'Hansi 🧔', served: false, avatarColor: '#6366f1' },
|
{ id: 'hansi', x: 300, name: 'Hansi 🧔', served: false, avatarColor: '#6366f1' },
|
||||||
{ x: 550, name: 'Viktor (Vampir) 🧛', served: false, avatarColor: '#881337' },
|
{ id: 'viktor', x: 550, name: 'Viktor (Vampir) 🧛', served: false, avatarColor: '#881337' },
|
||||||
{ x: 800, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
|
{ id: 'albin', x: 800, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
|
||||||
{ x: 1050, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
|
{ id: 'fabian', x: 1050, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
|
||||||
{ x: 1300, name: 'Maier Flo (Ton) 🎧', served: false, avatarColor: '#7c3aed' },
|
{ id: 'maierflo', x: 1300, name: 'Maier Flo (Ton) 🎧', served: false, avatarColor: '#7c3aed' },
|
||||||
{ x: 1550, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
|
{ id: 'guate', x: 1550, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
|
||||||
{ x: 1800, name: 'Markus & Nathalie 🥂', served: false, avatarColor: '#db2777' }
|
{ id: 'markus_nat', x: 1800, name: 'Markus & Nathalie 🥂', served: false, avatarColor: '#db2777' }
|
||||||
]
|
]
|
||||||
|
|
||||||
function throwBeer() {
|
function throwBeer() {
|
||||||
sound.playGulp()
|
sound.playGulp()
|
||||||
// Check if any guest is near
|
// Check if any guest is near
|
||||||
guests.forEach(g => {
|
guests.forEach(g => {
|
||||||
if (!g.served && Math.abs(distance + wagonX - g.x) < 80) {
|
if (!g.served && Math.abs(distance + wagonX - g.x) < 90) {
|
||||||
g.served = true
|
g.served = true
|
||||||
store.addBeer()
|
store.serveGuest()
|
||||||
store.addKronkorken(3)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -56,58 +56,111 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|||||||
|
|
||||||
// Sunset Party Sky
|
// Sunset Party Sky
|
||||||
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
||||||
grad.addColorStop(0, '#4a154b')
|
grad.addColorStop(0, '#31103f')
|
||||||
grad.addColorStop(0.5, '#993355')
|
grad.addColorStop(0.5, '#7a2048')
|
||||||
grad.addColorStop(1, '#ffaa44')
|
grad.addColorStop(0.8, '#c2410c')
|
||||||
|
grad.addColorStop(1, '#f59e0b')
|
||||||
ctx.fillStyle = grad
|
ctx.fillStyle = grad
|
||||||
ctx.fillRect(0, 0, w, h)
|
ctx.fillRect(0, 0, w, h)
|
||||||
|
|
||||||
// Distant party silhouettes with laser beams
|
// Distant party silhouettes with laser beams
|
||||||
const now = Date.now() * 0.003
|
const now = Date.now() * 0.003
|
||||||
ctx.strokeStyle = `hsl(${(now * 50) % 360}, 100%, 70%)`
|
ctx.strokeStyle = `hsl(${(now * 60) % 360}, 100%, 75%)`
|
||||||
ctx.lineWidth = 1.5
|
ctx.lineWidth = 2
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(w / 2, 80)
|
ctx.moveTo(w / 2, 100)
|
||||||
ctx.lineTo(w * Math.sin(now), h)
|
ctx.lineTo(w * ((Math.sin(now) + 1) / 2), 350)
|
||||||
ctx.moveTo(w / 2, 80)
|
ctx.moveTo(w / 2, 100)
|
||||||
ctx.lineTo(w * Math.cos(now), h)
|
ctx.lineTo(w * ((Math.cos(now) + 1) / 2), 350)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// Cobblestone road
|
// Cobblestone road
|
||||||
ctx.fillStyle = '#292524'
|
ctx.fillStyle = '#292524'
|
||||||
ctx.fillRect(0, 240, w, 80)
|
ctx.fillRect(0, 350, w, 130)
|
||||||
ctx.fillStyle = '#44403c'
|
ctx.fillStyle = '#44403c'
|
||||||
for (let r = 0; r < w; r += 30) {
|
for (let r = 0; r < w + 30; r += 30) {
|
||||||
ctx.fillRect((r - (distance % 30)), 240, 26, 4)
|
ctx.fillRect((r - (distance % 30)), 350, 24, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Guests along the road
|
// Draw Guests along the road
|
||||||
guests.forEach(g => {
|
guests.forEach(g => {
|
||||||
const screenX = g.x - distance
|
const screenX = g.x - distance
|
||||||
if (screenX > -50 && screenX < w + 50) {
|
if (screenX > -60 && screenX < w + 60) {
|
||||||
|
// Custom sprite details per guest
|
||||||
|
if (g.id === 'viktor') {
|
||||||
|
// Vampire cape
|
||||||
|
ctx.fillStyle = '#4c0519'
|
||||||
|
ctx.fillRect(screenX - 4, 305, 28, 42)
|
||||||
|
}
|
||||||
|
|
||||||
// Guest body
|
// Guest body
|
||||||
ctx.fillStyle = g.avatarColor
|
ctx.fillStyle = g.avatarColor
|
||||||
ctx.fillRect(screenX, 195, 20, 45)
|
ctx.fillRect(screenX, 305, 20, 45)
|
||||||
|
|
||||||
// Head
|
// Head
|
||||||
ctx.fillStyle = '#fed7aa'
|
ctx.fillStyle = '#fed7aa'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(screenX + 10, 185, 10, 0, Math.PI * 2)
|
ctx.arc(screenX + 10, 295, 10, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
|
// Character specifics
|
||||||
|
if (g.id === 'hansi') {
|
||||||
|
// Moustache
|
||||||
|
ctx.fillStyle = '#451a03'
|
||||||
|
ctx.fillRect(screenX + 6, 296, 8, 2.5)
|
||||||
|
} else if (g.id === 'viktor') {
|
||||||
|
// Fangs
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.fillRect(screenX + 8, 297, 2, 3)
|
||||||
|
ctx.fillRect(screenX + 12, 297, 2, 3)
|
||||||
|
} else if (g.id === 'albin') {
|
||||||
|
// Gym Headband
|
||||||
|
ctx.fillStyle = '#38bdf8'
|
||||||
|
ctx.fillRect(screenX + 2, 288, 16, 3)
|
||||||
|
} else if (g.id === 'fabian') {
|
||||||
|
// Beer mug in hand
|
||||||
|
ctx.fillStyle = '#f59e0b'
|
||||||
|
ctx.fillRect(screenX + 20, 312, 8, 10)
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.fillRect(screenX + 19, 309, 10, 3)
|
||||||
|
} else if (g.id === 'maierflo') {
|
||||||
|
// Headphones
|
||||||
|
ctx.strokeStyle = '#c084fc'
|
||||||
|
ctx.lineWidth = 2.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(screenX + 10, 295, 12, Math.PI, 0)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.fillStyle = '#9333ea'
|
||||||
|
ctx.fillRect(screenX - 3, 293, 4, 6)
|
||||||
|
ctx.fillRect(screenX + 19, 293, 4, 6)
|
||||||
|
} else if (g.id === 'guate') {
|
||||||
|
// Bushy dark beard
|
||||||
|
ctx.fillStyle = '#1c1917'
|
||||||
|
ctx.fillRect(screenX + 4, 297, 12, 9)
|
||||||
|
} else if (g.id === 'markus_nat') {
|
||||||
|
// Second head for couple
|
||||||
|
ctx.fillStyle = '#fbcfe8'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(screenX + 24, 296, 9, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.fillStyle = '#db2777'
|
||||||
|
ctx.fillRect(screenX + 16, 307, 16, 43)
|
||||||
|
}
|
||||||
|
|
||||||
// Name & icon tag
|
// Name & icon tag
|
||||||
ctx.fillStyle = '#fff'
|
ctx.fillStyle = '#fff'
|
||||||
ctx.font = 'bold 9px MedievalSharp, serif'
|
ctx.font = 'bold 9px MedievalSharp, serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.fillText(g.name, screenX + 10, 168)
|
ctx.fillText(g.name, screenX + 10, 278)
|
||||||
|
|
||||||
if (g.served) {
|
if (g.served) {
|
||||||
ctx.fillStyle = '#22c55e'
|
ctx.fillStyle = '#22c55e'
|
||||||
ctx.font = 'bold 12px sans-serif'
|
ctx.font = 'bold 11px sans-serif'
|
||||||
ctx.fillText('🍻 PROST!', screenX + 10, 154)
|
ctx.fillText('🍻 PROST!', screenX + 10, 264)
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#fbbf24'
|
ctx.fillStyle = '#fbbf24'
|
||||||
ctx.font = '10px monospace'
|
ctx.font = '9px "Press Start 2P", monospace'
|
||||||
ctx.fillText('DURST!', screenX + 10, 154)
|
ctx.fillText('DURST!', screenX + 10, 264)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -119,55 +172,85 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|||||||
|
|
||||||
// Wagon wooden bed
|
// Wagon wooden bed
|
||||||
ctx.fillStyle = '#78350f'
|
ctx.fillStyle = '#78350f'
|
||||||
ctx.fillRect(wx - 20, wy + bob, 90, 24)
|
ctx.fillRect(wx - 20, wy + bob, 95, 24)
|
||||||
ctx.fillStyle = '#b45309'
|
ctx.fillStyle = '#b45309'
|
||||||
ctx.fillRect(wx - 18, wy + bob + 3, 86, 18)
|
ctx.fillRect(wx - 18, wy + bob + 3, 91, 18)
|
||||||
|
|
||||||
// Wheels
|
// Wooden spoke wheels
|
||||||
ctx.fillStyle = '#451a03'
|
const wheelSpin = (distance * 0.1) % (Math.PI * 2)
|
||||||
ctx.beginPath()
|
;[-6, 60].forEach(wheelOffset => {
|
||||||
ctx.arc(wx - 6, wy + 24, 10, 0, Math.PI * 2)
|
ctx.save()
|
||||||
ctx.arc(wx + 54, wy + 24, 10, 0, Math.PI * 2)
|
ctx.translate(wx + wheelOffset, wy + 24)
|
||||||
ctx.fill()
|
ctx.rotate(wheelSpin)
|
||||||
ctx.strokeStyle = '#ffd700'
|
ctx.fillStyle = '#451a03'
|
||||||
ctx.lineWidth = 2
|
ctx.beginPath()
|
||||||
ctx.stroke()
|
ctx.arc(0, 0, 11, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = '#ffd700'
|
||||||
|
ctx.lineWidth = 2.5
|
||||||
|
ctx.stroke()
|
||||||
|
// Spokes
|
||||||
|
ctx.strokeStyle = '#b45309'
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(-11, 0); ctx.lineTo(11, 0)
|
||||||
|
ctx.moveTo(0, -11); ctx.lineTo(0, 11)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
})
|
||||||
|
|
||||||
// Sascha standing on wagon with crown, sunglasses & beer mug
|
// Sascha standing on wagon with crown, sunglasses & beer mug
|
||||||
ctx.fillStyle = '#dc2626' // Cape
|
ctx.fillStyle = '#dc2626' // Cape
|
||||||
ctx.fillRect(wx + 10, wy - 30 + bob, 22, 34)
|
ctx.fillRect(wx + 12, wy - 30 + bob, 22, 34)
|
||||||
ctx.fillStyle = '#1d4ed8' // Tunic
|
ctx.fillStyle = '#1d4ed8' // Tunic
|
||||||
ctx.fillRect(wx + 14, wy - 28 + bob, 16, 22)
|
ctx.fillRect(wx + 16, wy - 28 + bob, 16, 22)
|
||||||
ctx.fillStyle = '#fcd34d' // Face
|
ctx.fillStyle = '#fcd34d' // Face
|
||||||
ctx.fillRect(wx + 15, wy - 42 + bob, 14, 14)
|
ctx.fillRect(wx + 17, wy - 42 + bob, 14, 14)
|
||||||
// Sunglasses
|
// Sunglasses
|
||||||
ctx.fillStyle = '#0f172a'
|
ctx.fillStyle = '#0f172a'
|
||||||
ctx.fillRect(wx + 17, wy - 38 + bob, 5, 3)
|
ctx.fillRect(wx + 19, wy - 38 + bob, 5, 3)
|
||||||
ctx.fillRect(wx + 23, wy - 38 + bob, 5, 3)
|
ctx.fillRect(wx + 25, wy - 38 + bob, 5, 3)
|
||||||
// Crown
|
// Crown
|
||||||
ctx.fillStyle = '#facc15'
|
ctx.fillStyle = '#facc15'
|
||||||
ctx.fillRect(wx + 14, wy - 48 + bob, 16, 6)
|
ctx.fillRect(wx + 16, wy - 48 + bob, 16, 6)
|
||||||
|
|
||||||
// Giant Bass Speaker on the back of the wagon
|
// Giant Bass Speaker on the back of the wagon
|
||||||
ctx.fillStyle = '#18181b'
|
ctx.fillStyle = '#18181b'
|
||||||
ctx.fillRect(wx - 16, wy - 34 + bob, 24, 34)
|
ctx.fillRect(wx - 16, wy - 34 + bob, 24, 34)
|
||||||
ctx.fillStyle = '#f43f5e'
|
ctx.fillStyle = '#f43f5e'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(wx - 4, wy - 18 + bob, 8 + Math.abs(Math.sin(Date.now() * 0.02) * 3), 0, Math.PI * 2)
|
const bassPulse = Math.abs(Math.sin(Date.now() * 0.02) * 4)
|
||||||
|
ctx.arc(wx - 4, wy - 18 + bob, 8 + bassPulse, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = '#ffd700'
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// DJ Console in front
|
||||||
|
ctx.fillStyle = '#27272a'
|
||||||
|
ctx.fillRect(wx + 44, wy - 14 + bob, 26, 16)
|
||||||
|
// Turntable
|
||||||
|
ctx.fillStyle = '#09090b'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(wx + 57, wy - 6 + bob, 6, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.fillStyle = '#ef4444'
|
||||||
|
ctx.fillRect(wx + 56, wy - 7 + bob, 2, 2)
|
||||||
|
|
||||||
// Hof-DJs Banner
|
// Hof-DJs Banner
|
||||||
ctx.fillStyle = '#ffd700'
|
ctx.fillStyle = '#ffd700'
|
||||||
ctx.font = 'bold 9px "Press Start 2P", monospace'
|
ctx.font = 'bold 8.5px "Press Start 2P", monospace'
|
||||||
ctx.textAlign = 'left'
|
ctx.textAlign = 'center'
|
||||||
ctx.fillText('135 BPM EXPRESS', 10, 25)
|
ctx.fillText('🎧 135 BPM PARTY EXPRESS 🎧', w / 2, 24)
|
||||||
|
|
||||||
// Progress Bar
|
// Progress Bar
|
||||||
const progress = Math.min(1, distance / maxDistance)
|
const progress = Math.min(1, distance / maxDistance)
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.5)'
|
const barMargin = 24
|
||||||
ctx.fillRect(10, 35, w - 20, 8)
|
const barWidth = w - barMargin * 2
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.6)'
|
||||||
|
ctx.fillRect(barMargin, 34, barWidth, 8)
|
||||||
ctx.fillStyle = '#22c55e'
|
ctx.fillStyle = '#22c55e'
|
||||||
ctx.fillRect(10, 35, (w - 20) * progress, 8)
|
ctx.fillRect(barMargin, 34, barWidth * progress, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
@@ -190,7 +273,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="express-container">
|
<div class="express-container">
|
||||||
<canvas ref="canvasRef" width="360" height="320" class="game-canvas"></canvas>
|
<canvas ref="canvasRef" width="360" height="480" class="game-canvas"></canvas>
|
||||||
|
|
||||||
<div class="express-controls">
|
<div class="express-controls">
|
||||||
<button class="throw-beer-btn" @click="throwBeer">
|
<button class="throw-beer-btn" @click="throwBeer">
|
||||||
@@ -214,7 +297,7 @@ onUnmounted(() => {
|
|||||||
.game-canvas {
|
.game-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface Particle {
|
|||||||
|
|
||||||
let player = {
|
let player = {
|
||||||
x: 60,
|
x: 60,
|
||||||
y: 200,
|
y: 350,
|
||||||
w: 32,
|
w: 32,
|
||||||
h: 48,
|
h: 48,
|
||||||
vx: 0,
|
vx: 0,
|
||||||
@@ -62,33 +62,33 @@ let keys = {
|
|||||||
function initLevel() {
|
function initLevel() {
|
||||||
platforms = [
|
platforms = [
|
||||||
// Ground base
|
// Ground base
|
||||||
{ x: 0, y: 280, w: 700, h: 50, type: 'roof' },
|
{ x: 0, y: 400, w: 700, h: 80, type: 'roof' },
|
||||||
{ x: 780, y: 280, w: 600, h: 50, type: 'roof' },
|
{ x: 780, y: 400, w: 600, h: 80, type: 'roof' },
|
||||||
{ x: 1460, y: 280, w: 700, h: 50, type: 'roof' },
|
{ x: 1460, y: 400, w: 700, h: 80, type: 'roof' },
|
||||||
{ x: 2240, y: 280, w: 600, h: 50, type: 'roof' },
|
{ x: 2240, y: 400, w: 600, h: 80, type: 'roof' },
|
||||||
|
|
||||||
// Platforms
|
// Platforms
|
||||||
{ x: 180, y: 210, w: 100, h: 18, type: 'wood' },
|
{ x: 180, y: 330, w: 100, h: 18, type: 'wood' },
|
||||||
{ x: 340, y: 160, w: 120, h: 18, type: 'wood' },
|
{ x: 340, y: 260, w: 120, h: 18, type: 'wood' },
|
||||||
{ x: 520, y: 210, w: 90, h: 18, type: 'barrel' },
|
{ x: 520, y: 330, w: 90, h: 18, type: 'barrel' },
|
||||||
|
|
||||||
{ x: 860, y: 200, w: 110, h: 18, type: 'wood' },
|
{ x: 860, y: 310, w: 110, h: 18, type: 'wood' },
|
||||||
{ x: 1040, y: 150, w: 130, h: 18, type: 'wood' },
|
{ x: 1040, y: 240, w: 130, h: 18, type: 'wood' },
|
||||||
{ x: 1220, y: 210, w: 80, h: 18, type: 'barrel' },
|
{ x: 1220, y: 320, w: 80, h: 18, type: 'barrel' },
|
||||||
|
|
||||||
{ x: 1560, y: 190, w: 120, h: 18, type: 'wood' },
|
{ x: 1560, y: 290, w: 120, h: 18, type: 'wood' },
|
||||||
{ x: 1740, y: 140, w: 140, h: 18, type: 'wood' },
|
{ x: 1740, y: 220, w: 140, h: 18, type: 'wood' },
|
||||||
{ x: 1950, y: 200, w: 90, h: 18, type: 'barrel' },
|
{ x: 1950, y: 300, w: 90, h: 18, type: 'barrel' },
|
||||||
|
|
||||||
{ x: 2320, y: 190, w: 130, h: 18, type: 'wood' },
|
{ x: 2320, y: 280, w: 130, h: 18, type: 'wood' },
|
||||||
{ x: 2500, y: 140, w: 160, h: 18, type: 'wood' }
|
{ x: 2500, y: 220, w: 160, h: 18, type: 'wood' }
|
||||||
]
|
]
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
// Scatter 35 bottle caps and 5 beers
|
// Scatter 35 bottle caps and 5 beers
|
||||||
for (let i = 0; i < 35; i++) {
|
for (let i = 0; i < 35; i++) {
|
||||||
const px = 120 + i * 72
|
const px = 120 + i * 72
|
||||||
const py = 120 + (i % 3) * 45
|
const py = 220 + (i % 3) * 60
|
||||||
items.push({
|
items.push({
|
||||||
x: px,
|
x: px,
|
||||||
y: py,
|
y: py,
|
||||||
@@ -102,7 +102,7 @@ function initLevel() {
|
|||||||
;[450, 1100, 1800, 2450].forEach(bx => {
|
;[450, 1100, 1800, 2450].forEach(bx => {
|
||||||
items.push({
|
items.push({
|
||||||
x: bx,
|
x: bx,
|
||||||
y: 110,
|
y: 190,
|
||||||
type: 'beer',
|
type: 'beer',
|
||||||
collected: false,
|
collected: false,
|
||||||
bobOffset: 0
|
bobOffset: 0
|
||||||
@@ -185,8 +185,8 @@ function updatePhysics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent falling to oblivion (respawn smoothly with cheer)
|
// Prevent falling to oblivion (respawn smoothly with cheer)
|
||||||
if (player.y > 360) {
|
if (player.y > 460) {
|
||||||
player.y = 150
|
player.y = 250
|
||||||
player.vy = 0
|
player.vy = 0
|
||||||
player.x = Math.max(60, player.x - 120)
|
player.x = Math.max(60, player.x - 120)
|
||||||
}
|
}
|
||||||
@@ -254,9 +254,9 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
|
|||||||
for (let m = -1; m < 5; m++) {
|
for (let m = -1; m < 5; m++) {
|
||||||
const mx = m * 240 - (cameraX * 0.2) % 240
|
const mx = m * 240 - (cameraX * 0.2) % 240
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(mx, 260)
|
ctx.moveTo(mx, 400)
|
||||||
ctx.lineTo(mx + 120, 110)
|
ctx.lineTo(mx + 120, 200)
|
||||||
ctx.lineTo(mx + 240, 260)
|
ctx.lineTo(mx + 240, 400)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +264,11 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
|
|||||||
ctx.fillStyle = 'rgba(44, 24, 16, 0.65)'
|
ctx.fillStyle = 'rgba(44, 24, 16, 0.65)'
|
||||||
for (let c = -1; c < 4; c++) {
|
for (let c = -1; c < 4; c++) {
|
||||||
const cx = c * 320 - (cameraX * 0.35) % 320
|
const cx = c * 320 - (cameraX * 0.35) % 320
|
||||||
ctx.fillRect(cx + 60, 120, 50, 140)
|
ctx.fillRect(cx + 60, 220, 50, 180)
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(cx + 50, 120)
|
ctx.moveTo(cx + 50, 220)
|
||||||
ctx.lineTo(cx + 85, 80)
|
ctx.lineTo(cx + 85, 160)
|
||||||
ctx.lineTo(cx + 120, 120)
|
ctx.lineTo(cx + 120, 220)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,13 +352,13 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
|
|||||||
|
|
||||||
// Draw Goal Gate: Taverne 'Zum wummernden Ritter'
|
// Draw Goal Gate: Taverne 'Zum wummernden Ritter'
|
||||||
ctx.fillStyle = '#3e2723'
|
ctx.fillStyle = '#3e2723'
|
||||||
ctx.fillRect(levelWidth - 140, 160, 110, 120)
|
ctx.fillRect(levelWidth - 140, 270, 110, 130)
|
||||||
ctx.fillStyle = '#ffd700'
|
ctx.fillStyle = '#ffd700'
|
||||||
ctx.font = 'bold 9px MedievalSharp, serif'
|
ctx.font = 'bold 9px MedievalSharp, serif'
|
||||||
ctx.fillText('ZUM WUMMERNDEN RITTER', levelWidth - 135, 175)
|
ctx.fillText('ZUM WUMMERNDEN RITTER', levelWidth - 135, 285)
|
||||||
// Glowing door
|
// Glowing door
|
||||||
ctx.fillStyle = '#f59e0b'
|
ctx.fillStyle = '#f59e0b'
|
||||||
ctx.fillRect(levelWidth - 105, 200, 40, 80)
|
ctx.fillRect(levelWidth - 105, 320, 40, 80)
|
||||||
|
|
||||||
// Draw Player: König Sascha (Pixel King with sunglasses, crown & ermine coat)
|
// Draw Player: König Sascha (Pixel King with sunglasses, crown & ermine coat)
|
||||||
const px = player.x
|
const px = player.x
|
||||||
@@ -424,18 +424,7 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
|
|||||||
ctx.arc(px + 24, py + 16, 4, 0, Math.PI * 2)
|
ctx.arc(px + 24, py + 16, 4, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
ctx.restore()
|
ctx.restore() // End camera translation
|
||||||
|
|
||||||
// On-screen HUD
|
|
||||||
ctx.fillStyle = 'rgba(26, 15, 10, 0.75)'
|
|
||||||
ctx.fillRect(8, 8, 160, 28)
|
|
||||||
ctx.strokeStyle = '#d4af37'
|
|
||||||
ctx.lineWidth = 1.5
|
|
||||||
ctx.strokeRect(8, 8, 160, 28)
|
|
||||||
|
|
||||||
ctx.fillStyle = '#ffd700'
|
|
||||||
ctx.font = '10px "Press Start 2P", monospace'
|
|
||||||
ctx.fillText(`KRONEN: ${store.kronkorken}`, 16, 26)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function gameLoop() {
|
function gameLoop() {
|
||||||
@@ -484,14 +473,17 @@ onUnmounted(() => {
|
|||||||
<canvas
|
<canvas
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
width="360"
|
width="360"
|
||||||
height="320"
|
height="480"
|
||||||
class="game-canvas"
|
class="game-canvas"
|
||||||
@touchstart.prevent="onJump"
|
@touchstart.prevent="onJump"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<div class="level-overlay">
|
<div class="platformer-hud-top">
|
||||||
<span class="level-title">AKTE II: DIE HOPFEN-PFADE</span>
|
<div class="hud-korken-badge">👑 {{ store.kronkorken }} KORKEN</div>
|
||||||
<span class="level-hint">Tippe oder drücke SPRUNG zum Hüpfen</span>
|
<div class="level-overlay">
|
||||||
|
<span class="level-title">AKTE II: DIE HOPFEN-PFADE</span>
|
||||||
|
<span class="level-hint">Tippe oder drücke SPRUNG zum Hüpfen</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -511,19 +503,38 @@ onUnmounted(() => {
|
|||||||
.game-canvas {
|
.game-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-overlay {
|
.platformer-hud-top {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 8px;
|
||||||
right: 8px;
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-korken-badge {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #ffd700;
|
||||||
|
background: rgba(26, 15, 10, 0.85);
|
||||||
|
border: 1.5px solid #d4af37;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.level-title {
|
.level-title {
|
||||||
|
|||||||
@@ -33,13 +33,16 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="prologue-container">
|
<div class="prologue-container">
|
||||||
<!-- Balcony Visual Scene -->
|
<!-- Balcony Visual Scene with Regal Portrait Frame -->
|
||||||
<div class="scene-visual">
|
<div class="scene-visual">
|
||||||
<img
|
<div class="balcony-backdrop"></div>
|
||||||
:src="currentSpeaker().portrait"
|
<div class="speaker-frame">
|
||||||
:alt="currentSpeaker().name"
|
<img
|
||||||
class="speaker-visual"
|
:src="currentSpeaker().portrait"
|
||||||
/>
|
:alt="currentSpeaker().name"
|
||||||
|
class="speaker-visual"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="scene-tag">
|
<div class="scene-tag">
|
||||||
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
|
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,21 +89,43 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
.scene-visual {
|
.scene-visual {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 140px;
|
min-height: 160px;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #2b1810;
|
background: radial-gradient(circle at 50% 40%, #4a2818 0%, #1a0f0a 100%);
|
||||||
border-bottom: 2px solid #d4af37;
|
border-bottom: 2px solid #d4af37;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balcony-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 0, 0, 0.3) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 0, 0, 0.3) 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-frame {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 3px double #ffd700;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.8), 0 0 10px rgba(212, 175, 55, 0.4);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #2b1810;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.speaker-visual {
|
.speaker-visual {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center center;
|
object-position: center top;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ function openCharacterDialog(charId: string) {
|
|||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (charId === 'nat') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Servus Sascha! Schau dir meinen Wildschwein-Harnisch an – der glänzt wie ein Kronjuwel! Ich hab dir schon 3 kühle Helle und 5 Kronkorken gesichert, bevor die Meute alles wegsäuft!',
|
||||||
|
actionLabel: 'Sauber Josef! (+5 Kronkorken & Helles)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(5)
|
||||||
|
store.addBeer()
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +176,7 @@ function proceedToExpress() {
|
|||||||
|
|
||||||
<!-- Interactive Hotspots on the visual scene with Avatar Thumbnails -->
|
<!-- Interactive Hotspots on the visual scene with Avatar Thumbnails -->
|
||||||
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
||||||
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Verena" />
|
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
|
||||||
<span class="hotspot-badge">Verena 👑</span>
|
<span class="hotspot-badge">Verena 👑</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -196,6 +210,11 @@ function proceedToExpress() {
|
|||||||
<span class="hotspot-badge">Mane 🥾</span>
|
<span class="hotspot-badge">Mane 🥾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
|
||||||
|
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
|
||||||
|
<span class="hotspot-badge">NAT 🐗</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
||||||
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
|
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
|
||||||
<span class="hotspot-badge">Mario 🤹</span>
|
<span class="hotspot-badge">Mario 🤹</span>
|
||||||
@@ -203,13 +222,16 @@ function proceedToExpress() {
|
|||||||
|
|
||||||
<!-- Bottom Scene Navigation & Duel -->
|
<!-- Bottom Scene Navigation & Duel -->
|
||||||
<div class="scene-bottom-nav">
|
<div class="scene-bottom-nav">
|
||||||
<button class="duel-trigger" @click="startGstanzlDuel">
|
<div class="tavern-bar-title">🍻 TAVERNE ZUM WUMMERNDEN RITTER 🍻</div>
|
||||||
⚔️ Gstanzl-Fechten
|
<div class="tavern-nav-buttons">
|
||||||
</button>
|
<button class="duel-trigger" @click="startGstanzlDuel">
|
||||||
|
⚔️ Gstanzl-Fechten
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="next-level-btn" @click="proceedToExpress">
|
<button class="next-level-btn" @click="proceedToExpress">
|
||||||
➡️ Zum Fest-Konvoi!
|
➡️ Zum Fest-Konvoi!
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -353,14 +375,15 @@ function proceedToExpress() {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-verena { top: 72%; left: 54%; }
|
.hotspot-verena { top: 15%; left: 80%; }
|
||||||
.hotspot-armend { top: 62%; left: 24%; }
|
.hotspot-armend { top: 62%; left: 24%; }
|
||||||
.hotspot-lutz { top: 38%; left: 22%; }
|
.hotspot-lutz { top: 38%; left: 20%; }
|
||||||
.hotspot-mirror { top: 30%; left: 63%; }
|
.hotspot-mirror { top: 32%; left: 63%; }
|
||||||
.hotspot-amy { top: 52%; left: 84%; }
|
.hotspot-amy { top: 52%; left: 82%; }
|
||||||
.hotspot-pue { top: 16%; left: 35%; }
|
.hotspot-pue { top: 20%; left: 35%; }
|
||||||
.hotspot-mane { top: 44%; left: 68%; }
|
.hotspot-mane { top: 45%; left: 66%; }
|
||||||
.hotspot-mario { top: 74%; left: 86%; }
|
.hotspot-nat { top: 63%; left: 85%; }
|
||||||
|
.hotspot-mario { top: 36%; left: 90%; }
|
||||||
|
|
||||||
.pulse {
|
.pulse {
|
||||||
animation: pulseAnim 1.5s infinite;
|
animation: pulseAnim 1.5s infinite;
|
||||||
@@ -373,13 +396,37 @@ function proceedToExpress() {
|
|||||||
|
|
||||||
.scene-bottom-nav {
|
.scene-bottom-nav {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 0;
|
||||||
left: 8px;
|
left: 0;
|
||||||
right: 8px;
|
right: 0;
|
||||||
|
height: 84px;
|
||||||
|
background: linear-gradient(180deg, #451a03 0%, #26140b 100%);
|
||||||
|
border-top: 3px solid #b45309;
|
||||||
|
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.95);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tavern-bar-title {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7px;
|
||||||
|
color: #fef08a;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tavern-nav-buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.duel-trigger {
|
.duel-trigger {
|
||||||
|
|||||||
Reference in New Issue
Block a user