feat(sasch40): multi-room tavern, authentic character portraits, pre-level start screens & bollerwagen redesign
|
After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 869 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 979 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 924 KiB After Width: | Height: | Size: 775 KiB |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 797 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 972 KiB |
@@ -9,7 +9,7 @@ if (!fs.existsSync(SCREENSHOTS_DIR)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runPlaytest() {
|
async function runPlaytest() {
|
||||||
console.log('🚀 Starting Chrome Playtest for Saschas Kronkorken Kreuzzug...');
|
console.log('🚀 Starting Comprehensive Chrome Playtest for Saschas Kronkorken Kreuzzug...');
|
||||||
|
|
||||||
const logs = [];
|
const logs = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
@@ -60,165 +60,206 @@ async function runPlaytest() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🌐 Navigating to https://sasch40.haumdaucher.de...');
|
console.log('🌐 Navigating to https://sasch40.haumdaucher.de...');
|
||||||
await page.goto('https://sasch40.haumdaucher.de', { waitUntil: 'networkidle0', timeout: 30000 });
|
await page.goto('https://sasch40.haumdaucher.de', { waitUntil: 'networkidle0', timeout: 35000 });
|
||||||
|
|
||||||
// 1. INTRO SCREEN
|
// 1. INTRO / PREQUEL SCREEN
|
||||||
console.log('📸 Capturing Intro Screen...');
|
console.log('📸 01: Capturing Intro Screen...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '01_intro_screen.png') });
|
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
|
// Click Start Button
|
||||||
console.log('🖱️ Clicking Start Button...');
|
console.log('🖱️ Clicking Start Button...');
|
||||||
await page.click('.start-btn');
|
await page.click('.start-btn');
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
// 2. PROLOGUE SCREEN
|
// 2. PROLOGUE SCREEN (Balcony Scene)
|
||||||
console.log('📸 Capturing Prologue Screen...');
|
console.log('📸 02: Capturing Prologue Screen (Verifying banner/portrait alignment)...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
|
||||||
|
|
||||||
// Advance Prologue Dialogues
|
// Advance Prologue Dialogues
|
||||||
console.log('💬 Advancing Prologue Dialogues (clicking choice buttons)...');
|
|
||||||
// Click choice 1 on 'start'
|
|
||||||
await page.waitForSelector('.choice-btn', { timeout: 5000 });
|
await page.waitForSelector('.choice-btn', { timeout: 5000 });
|
||||||
const firstChoice = await page.$('.choice-btn');
|
const firstChoice = await page.$('.choice-btn');
|
||||||
if (firstChoice) {
|
if (firstChoice) {
|
||||||
await firstChoice.click();
|
await firstChoice.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 800));
|
||||||
console.log('📸 Capturing Mop Appears Prologue Dialogue...');
|
console.log('📸 02b: Capturing Prologue Mop Dialog...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click choice on 'mop_appears' -> triggers 'startPlatformer'
|
// Advance to Platformer
|
||||||
const mopChoice = await page.$('.choice-btn');
|
const mopChoice = await page.$('.choice-btn');
|
||||||
if (mopChoice) {
|
if (mopChoice) {
|
||||||
await mopChoice.click();
|
await mopChoice.click();
|
||||||
await new Promise(r => setTimeout(r, 1200));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. PLATFORMER SCREEN
|
// 3. PLATFORMER PRE-LEVEL START SCREEN (Act II)
|
||||||
console.log('📸 Capturing Platformer (Jump & Run)...');
|
console.log('📸 03: Capturing Platformer Start Screen Modal...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_start_modal.png') });
|
||||||
|
|
||||||
const platformerControls = await page.evaluate(() => {
|
// Click Start Act Button on the modal
|
||||||
const dpad = document.querySelector('.dpad');
|
const startPlatformerBtn = await page.$('.start-act-btn');
|
||||||
const btnA = document.querySelector('.btn-a');
|
if (startPlatformerBtn) {
|
||||||
const canvas = document.querySelector('canvas');
|
await startPlatformerBtn.click();
|
||||||
return {
|
await new Promise(r => setTimeout(r, 800));
|
||||||
hasDpad: !!dpad,
|
}
|
||||||
hasBtnA: !!btnA,
|
|
||||||
hasCanvas: !!canvas,
|
|
||||||
canvasW: canvas?.width,
|
|
||||||
canvasH: canvas?.height
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log('Platformer Controls Check:', platformerControls);
|
|
||||||
|
|
||||||
// Test Jump & Move
|
// 4. PLATFORMER GAMEPLAY
|
||||||
console.log('🎮 Testing Jump & Move Controls in Platformer...');
|
console.log('📸 04: Capturing Platformer Gameplay...');
|
||||||
const btnA = await page.$('.btn-a');
|
const btnA = await page.$('.btn-a');
|
||||||
if (btnA) {
|
if (btnA) {
|
||||||
await btnA.click();
|
await btnA.click();
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
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') });
|
|
||||||
}
|
}
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_platformer_gameplay.png') });
|
||||||
|
|
||||||
// Directly set act to 'tavern' to test Tavern Point & Click
|
// 5. TAVERN: SCHANKRAUM (Gaststube)
|
||||||
console.log('🍺 Advancing to Tavern Act...');
|
console.log('🍺 Advancing to Tavern Act (Schankraum)...');
|
||||||
await page.evaluate(() => {
|
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__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
if (app && app.config?.globalProperties?.$pinia) {
|
if (app && app.config?.globalProperties?.$pinia) {
|
||||||
const pinia = app.config.globalProperties.$pinia;
|
const pinia = app.config.globalProperties.$pinia;
|
||||||
// find game store
|
|
||||||
for (const [id, store] of pinia._s.entries()) {
|
for (const [id, store] of pinia._s.entries()) {
|
||||||
if (id === 'game') {
|
if (id === 'game') store.setAct('tavern');
|
||||||
store.setAct('tavern');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 Capturing Tavern Screen...');
|
console.log('📸 05: Capturing Tavern Gaststube (Schankraum)...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_tavern_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_schankraum.png') });
|
||||||
|
|
||||||
// Test Tavern Hotspots
|
// Test Verena Hotspot
|
||||||
console.log('👥 Testing Verena Hotspot...');
|
console.log('👥 Testing Verena Dialog...');
|
||||||
const verenaBtn = await page.$('.hotspot-verena');
|
const verenaBtn = await page.$('.hotspot-verena');
|
||||||
if (verenaBtn) {
|
if (verenaBtn) {
|
||||||
await verenaBtn.click();
|
await verenaBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_verena_dialog.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_verena_dialog.png') });
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
if (actionBtn) await actionBtn.click();
|
if (actionBtn) await actionBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 500));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔨 Testing Armend Hotspot...');
|
// Test Armend Hotspot
|
||||||
|
console.log('🔨 Testing Armend Dialog...');
|
||||||
const armendBtn = await page.$('.hotspot-armend');
|
const armendBtn = await page.$('.hotspot-armend');
|
||||||
if (armendBtn) {
|
if (armendBtn) {
|
||||||
await armendBtn.click();
|
await armendBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_armend_dialog.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_armend_dialog.png') });
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
if (actionBtn) await actionBtn.click();
|
if (actionBtn) await actionBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 500));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🪞 Testing Zuluman Magic Mirror Hotspot...');
|
// 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws)
|
||||||
|
console.log('🎪 Navigating to Marktplatz...');
|
||||||
|
const marktplatzTab = await page.$('.room-tab:nth-child(2)');
|
||||||
|
if (marktplatzTab) {
|
||||||
|
await marktplatzTab.click();
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
console.log('📸 08: Capturing Marktplatz (Mario chainsaws)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') });
|
||||||
|
|
||||||
|
// Click Mario
|
||||||
|
const marioBtn = await page.$('.hotspot-mario');
|
||||||
|
if (marioBtn) {
|
||||||
|
await marioBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 09: Capturing Mario Dialog...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_tavern_mario_dialog.png') });
|
||||||
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
|
if (actionBtn) await actionBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. TAVERN: HINTERLAND (Mane hiking)
|
||||||
|
console.log('🌲 Navigating to Hinterland...');
|
||||||
|
const hinterlandTab = await page.$('.room-tab:nth-child(3)');
|
||||||
|
if (hinterlandTab) {
|
||||||
|
await hinterlandTab.click();
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
console.log('📸 10: Capturing Hinterland (Mane hiking)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_tavern_hinterland.png') });
|
||||||
|
|
||||||
|
// Click Mane
|
||||||
|
const maneBtn = await page.$('.hotspot-mane');
|
||||||
|
if (maneBtn) {
|
||||||
|
await maneBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 11: Capturing Mane Dialog...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11_tavern_mane_dialog.png') });
|
||||||
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
|
if (actionBtn) await actionBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap)
|
||||||
|
console.log('🗝️ Returning to Schankraum to trigger Secret Tap...');
|
||||||
|
const schankraumTab = await page.$('.room-tab:nth-child(1)');
|
||||||
|
if (schankraumTab) {
|
||||||
|
await schankraumTab.click();
|
||||||
|
await new Promise(r => setTimeout(r, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🪩 Triggering Secret Tap Easter Egg...');
|
||||||
|
const secretTap = await page.$('.secret-tap-hotspot');
|
||||||
|
if (secretTap) {
|
||||||
|
await secretTap.click();
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
console.log('📸 12: Capturing Secret VIP Hinterzimmer...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_hinterzimmer_vip.png') });
|
||||||
|
|
||||||
|
// Click Pü in Hinterzimmer
|
||||||
|
const pueBtn = await page.$('.hotspot-pue');
|
||||||
|
if (pueBtn) {
|
||||||
|
await pueBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 13: Capturing Pü Dialog...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '13_tavern_pue_dialog.png') });
|
||||||
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
|
if (actionBtn) await actionBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. ZULUMAN MAGIC MIRROR & GSTANZL DUEL
|
||||||
|
console.log('🪞 Testing Zuluman Magic Mirror...');
|
||||||
|
const backToGaststube = await page.$('.hotspot-door-back-vip');
|
||||||
|
if (backToGaststube) {
|
||||||
|
await backToGaststube.click();
|
||||||
|
await new Promise(r => setTimeout(r, 600));
|
||||||
|
}
|
||||||
const mirrorBtn = await page.$('.hotspot-mirror');
|
const mirrorBtn = await page.$('.hotspot-mirror');
|
||||||
if (mirrorBtn) {
|
if (mirrorBtn) {
|
||||||
await mirrorBtn.click();
|
await mirrorBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_zuluman_mirror.png') });
|
console.log('📸 14: Capturing Zuluman Mirror...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') });
|
||||||
const closeBtn = await page.$('.mirror-box .action-btn');
|
const closeBtn = await page.$('.mirror-box .action-btn');
|
||||||
if (closeBtn) await closeBtn.click();
|
if (closeBtn) await closeBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 500));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('⚔️ Testing Gstanzl-Duel...');
|
console.log('⚔️ Testing Gstanzl-Duel...');
|
||||||
const duelBtn = await page.$('.duel-trigger');
|
const duelBtn = await page.$('.duel-trigger');
|
||||||
if (duelBtn) {
|
if (duelBtn) {
|
||||||
await duelBtn.click();
|
await duelBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_gstanzl_duel.png') });
|
console.log('📸 15: Capturing Gstanzl Duel...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15_tavern_gstanzl_duel.png') });
|
||||||
// Answer Gstanzl question
|
const firstAnswer = await page.$('.choice-btn');
|
||||||
const correctOption = await page.$('.option-btn');
|
if (firstAnswer) {
|
||||||
if (correctOption) {
|
await firstAnswer.click();
|
||||||
await correctOption.click();
|
await new Promise(r => setTimeout(r, 500));
|
||||||
await new Promise(r => setTimeout(r, 600));
|
|
||||||
}
|
}
|
||||||
|
// Close duel if won or modal
|
||||||
|
const actionBtn = await page.$('.duel-box .action-btn');
|
||||||
|
if (actionBtn) await actionBtn.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. PARTY EXPRESS (Act IV)
|
// 10. PARTY EXPRESS (Act IV)
|
||||||
console.log('🚂 Advancing to Party Express (Act IV)...');
|
console.log('🚂 Advancing to Party Express (Act IV)...');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
@@ -230,10 +271,26 @@ async function runPlaytest() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 Capturing Party Express...');
|
console.log('📸 16: Capturing Party Express Start Screen Modal...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_party_express.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '16_party_express_start_modal.png') });
|
||||||
|
|
||||||
// 5. FINALE / CERTIFICATE (Act V)
|
// Start Party Express
|
||||||
|
const startExpressBtn = await page.$('.start-act-btn');
|
||||||
|
if (startExpressBtn) {
|
||||||
|
await startExpressBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📸 17: Capturing Party Express Gameplay (Redesigned Bollerwagen)...');
|
||||||
|
// Serve beer
|
||||||
|
const throwBeerBtn = await page.$('.throw-beer-btn');
|
||||||
|
if (throwBeerBtn) {
|
||||||
|
await throwBeerBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '17_party_express_gameplay.png') });
|
||||||
|
|
||||||
|
// 11. FINALE / CERTIFICATE (Act V)
|
||||||
console.log('👑 Advancing to Finale Certificate (Act V)...');
|
console.log('👑 Advancing to Finale Certificate (Act V)...');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
@@ -241,7 +298,7 @@ async function runPlaytest() {
|
|||||||
const pinia = app.config.globalProperties.$pinia;
|
const pinia = app.config.globalProperties.$pinia;
|
||||||
for (const [id, store] of pinia._s.entries()) {
|
for (const [id, store] of pinia._s.entries()) {
|
||||||
if (id === 'game') {
|
if (id === 'game') {
|
||||||
store.addKronkorken(25);
|
store.addKronkorken(35);
|
||||||
store.serveGuest();
|
store.serveGuest();
|
||||||
store.serveGuest();
|
store.serveGuest();
|
||||||
store.setAct('finale');
|
store.setAct('finale');
|
||||||
@@ -250,10 +307,10 @@ async function runPlaytest() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
console.log('📸 Capturing Certificate Modal...');
|
console.log('📸 18: Capturing Certificate Modal...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_certificate_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '18_certificate_screen.png') });
|
||||||
|
|
||||||
// Check images in document
|
// Verify all images loaded properly
|
||||||
const brokenImages = await page.evaluate(() => {
|
const brokenImages = await page.evaluate(() => {
|
||||||
const imgs = Array.from(document.querySelectorAll('img'));
|
const imgs = Array.from(document.querySelectorAll('img'));
|
||||||
return imgs
|
return imgs
|
||||||
@@ -265,7 +322,7 @@ async function runPlaytest() {
|
|||||||
console.log('Total Console Logs:', logs.length);
|
console.log('Total Console Logs:', logs.length);
|
||||||
console.log('Console Errors:', errors);
|
console.log('Console Errors:', errors);
|
||||||
console.log('Failed Requests:', failedRequests);
|
console.log('Failed Requests:', failedRequests);
|
||||||
console.log('Broken Images on Certificate Page:', brokenImages);
|
console.log('Broken Images:', brokenImages);
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ function toggleInventory() {
|
|||||||
<span></span><span></span><span></span>
|
<span></span><span></span><span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chassis-brand">
|
<div class="chassis-brand">
|
||||||
<span class="brand-text">KING SASCHA XL</span>
|
<span class="brand-text">KÖNIG SASCHAS KRONKORKENKREUZZUG</span>
|
||||||
<span v-if="store.currentAct !== 'intro'" class="brand-act">
|
<span v-if="store.currentAct !== 'intro'" class="brand-act">
|
||||||
{{ store.currentAct === 'platformer' ? '• AKTE II' : store.currentAct === 'tavern' ? '• AKTE III' : store.currentAct === 'party_express' ? '• AKTE IV' : '' }}
|
{{ store.currentAct === 'prologue' ? '• AKT I' : store.currentAct === 'platformer' ? '• AKT II' : store.currentAct === 'tavern' ? '• AKT III' : store.currentAct === 'party_express' ? '• AKT IV' : store.currentAct === 'finale' ? '• AKT V' : '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sound-toggle" @click="toggleAudio">
|
<button class="sound-toggle" @click="toggleAudio">
|
||||||
@@ -204,20 +204,23 @@ function toggleInventory() {
|
|||||||
.chassis-brand {
|
.chassis-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-text {
|
.brand-text {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 6.8px;
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.2px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-act {
|
.brand-act {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 7px;
|
font-size: 6px;
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-grill-left {
|
.speaker-grill-left {
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ import { sound } from '../audio/synthAudio'
|
|||||||
|
|
||||||
const store = useGameStore()
|
const store = useGameStore()
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const showExpressIntro = ref(true)
|
||||||
let animId: number | null = null
|
let animId: number | null = null
|
||||||
|
|
||||||
let wagonX = 60
|
let wagonX = 70
|
||||||
let wagonY = 330
|
let wagonY = 325
|
||||||
let speed = 3.5
|
let speed = 3.2
|
||||||
let distance = 0
|
let distance = 0
|
||||||
const maxDistance = 2000
|
const maxDistance = 2100
|
||||||
|
|
||||||
|
function startExpress() {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
showExpressIntro.value = false
|
||||||
|
}
|
||||||
|
|
||||||
interface Guest {
|
interface Guest {
|
||||||
id: string
|
id: string
|
||||||
@@ -24,18 +30,18 @@ interface Guest {
|
|||||||
let guests: Guest[] = [
|
let guests: Guest[] = [
|
||||||
{ id: 'hansi', x: 300, name: 'Hansi 🧔', served: false, avatarColor: '#6366f1' },
|
{ id: 'hansi', x: 300, name: 'Hansi 🧔', served: false, avatarColor: '#6366f1' },
|
||||||
{ id: 'viktor', x: 550, name: 'Viktor (Vampir) 🧛', served: false, avatarColor: '#881337' },
|
{ id: 'viktor', x: 550, name: 'Viktor (Vampir) 🧛', served: false, avatarColor: '#881337' },
|
||||||
{ id: 'albin', x: 800, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
|
{ id: 'albin', x: 820, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
|
||||||
{ id: 'fabian', x: 1050, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
|
{ id: 'fabian', x: 1100, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
|
||||||
{ id: 'maierflo', x: 1300, name: 'Maier Flo (Ton) 🎧', served: false, avatarColor: '#7c3aed' },
|
{ id: 'maierflo', x: 1380, name: 'Maier Flo (DJ) 🎧', served: false, avatarColor: '#7c3aed' },
|
||||||
{ id: 'guate', x: 1550, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
|
{ id: 'guate', x: 1650, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
|
||||||
{ id: 'markus_nat', x: 1800, name: 'Markus & Nathalie 🥂', served: false, avatarColor: '#db2777' }
|
{ id: 'markus_nat', x: 1920, 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 the wagon
|
||||||
guests.forEach(g => {
|
guests.forEach(g => {
|
||||||
if (!g.served && Math.abs(distance + wagonX - g.x) < 90) {
|
if (!g.served && Math.abs(distance + wagonX - g.x) < 95) {
|
||||||
g.served = true
|
g.served = true
|
||||||
store.serveGuest()
|
store.serveGuest()
|
||||||
}
|
}
|
||||||
@@ -43,6 +49,8 @@ function throwBeer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateGame() {
|
function updateGame() {
|
||||||
|
if (showExpressIntro.value) return // Don't advance while reading intro!
|
||||||
|
|
||||||
distance += speed
|
distance += speed
|
||||||
|
|
||||||
if (distance >= maxDistance) {
|
if (distance >= maxDistance) {
|
||||||
@@ -54,43 +62,68 @@ function updateGame() {
|
|||||||
function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
||||||
ctx.clearRect(0, 0, w, h)
|
ctx.clearRect(0, 0, w, h)
|
||||||
|
|
||||||
// Sunset Party Sky
|
// Sunset Party Sky Gradient
|
||||||
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
||||||
grad.addColorStop(0, '#31103f')
|
grad.addColorStop(0, '#1e082b')
|
||||||
grad.addColorStop(0.5, '#7a2048')
|
grad.addColorStop(0.4, '#4a154b')
|
||||||
grad.addColorStop(0.8, '#c2410c')
|
grad.addColorStop(0.7, '#831843')
|
||||||
|
grad.addColorStop(0.9, '#c2410c')
|
||||||
grad.addColorStop(1, '#f59e0b')
|
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
|
// Sweeping Festival Lasers & Party Beams
|
||||||
const now = Date.now() * 0.003
|
const now = Date.now() * 0.003
|
||||||
ctx.strokeStyle = `hsl(${(now * 60) % 360}, 100%, 75%)`
|
ctx.save()
|
||||||
ctx.lineWidth = 2
|
for (let b = 0; b < 3; b++) {
|
||||||
ctx.beginPath()
|
const angleOffset = b * (Math.PI / 3)
|
||||||
ctx.moveTo(w / 2, 100)
|
const targetX = w * 0.5 + Math.sin(now + angleOffset) * (w * 0.45)
|
||||||
ctx.lineTo(w * ((Math.sin(now) + 1) / 2), 350)
|
ctx.strokeStyle = `hsla(${((now * 50) + b * 90) % 360}, 100%, 75%, 0.45)`
|
||||||
ctx.moveTo(w / 2, 100)
|
ctx.lineWidth = 3
|
||||||
ctx.lineTo(w * ((Math.cos(now) + 1) / 2), 350)
|
ctx.beginPath()
|
||||||
ctx.stroke()
|
ctx.moveTo(w * 0.5, 90)
|
||||||
|
ctx.lineTo(targetX, 350)
|
||||||
// Cobblestone road
|
ctx.stroke()
|
||||||
ctx.fillStyle = '#292524'
|
|
||||||
ctx.fillRect(0, 350, w, 130)
|
|
||||||
ctx.fillStyle = '#44403c'
|
|
||||||
for (let r = 0; r < w + 30; r += 30) {
|
|
||||||
ctx.fillRect((r - (distance % 30)), 350, 24, 6)
|
|
||||||
}
|
}
|
||||||
|
ctx.restore()
|
||||||
|
|
||||||
|
// Medieval Castle silhouette on the horizon
|
||||||
|
ctx.fillStyle = '#170c14'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(0, 310)
|
||||||
|
ctx.lineTo(30, 290); ctx.lineTo(50, 290); ctx.lineTo(50, 310)
|
||||||
|
ctx.lineTo(110, 310); ctx.lineTo(130, 275); ctx.lineTo(160, 275); ctx.lineTo(170, 310)
|
||||||
|
ctx.lineTo(240, 310); ctx.lineTo(255, 295); ctx.lineTo(275, 295); ctx.lineTo(290, 310)
|
||||||
|
ctx.lineTo(w, 310); ctx.lineTo(w, 350); ctx.lineTo(0, 350)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Cobblestone Highway
|
||||||
|
ctx.fillStyle = '#1c1917'
|
||||||
|
ctx.fillRect(0, 350, w, 130)
|
||||||
|
ctx.fillStyle = '#292524'
|
||||||
|
for (let r = 0; r < w + 40; r += 32) {
|
||||||
|
const rx = (r - (distance % 32))
|
||||||
|
ctx.fillRect(rx, 352, 26, 6)
|
||||||
|
ctx.fillRect(rx + 12, 362, 26, 6)
|
||||||
|
}
|
||||||
|
// Curbs / Grass edge
|
||||||
|
ctx.fillStyle = '#365314'
|
||||||
|
ctx.fillRect(0, 348, w, 3)
|
||||||
|
|
||||||
// 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 > -60 && screenX < w + 60) {
|
if (screenX > -70 && screenX < w + 70) {
|
||||||
// Custom sprite details per guest
|
// Guest shadow
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.4)'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.ellipse(screenX + 10, 350, 14, 5, 0, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
if (g.id === 'viktor') {
|
if (g.id === 'viktor') {
|
||||||
// Vampire cape
|
// Red velvet vampire cape
|
||||||
ctx.fillStyle = '#4c0519'
|
ctx.fillStyle = '#4c0519'
|
||||||
ctx.fillRect(screenX - 4, 305, 28, 42)
|
ctx.fillRect(screenX - 5, 305, 30, 44)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guest body
|
// Guest body
|
||||||
@@ -107,12 +140,11 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|||||||
if (g.id === 'hansi') {
|
if (g.id === 'hansi') {
|
||||||
// Moustache
|
// Moustache
|
||||||
ctx.fillStyle = '#451a03'
|
ctx.fillStyle = '#451a03'
|
||||||
ctx.fillRect(screenX + 6, 296, 8, 2.5)
|
ctx.fillRect(screenX + 6, 297, 8, 2.5)
|
||||||
} else if (g.id === 'viktor') {
|
} else if (g.id === 'viktor') {
|
||||||
// Fangs
|
// Sunglasses & fangs
|
||||||
ctx.fillStyle = '#ffffff'
|
ctx.fillStyle = '#0f172a'
|
||||||
ctx.fillRect(screenX + 8, 297, 2, 3)
|
ctx.fillRect(screenX + 6, 293, 8, 3)
|
||||||
ctx.fillRect(screenX + 12, 297, 2, 3)
|
|
||||||
} else if (g.id === 'albin') {
|
} else if (g.id === 'albin') {
|
||||||
// Gym Headband
|
// Gym Headband
|
||||||
ctx.fillStyle = '#38bdf8'
|
ctx.fillStyle = '#38bdf8'
|
||||||
@@ -159,98 +191,209 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|||||||
ctx.fillText('🍻 PROST!', screenX + 10, 264)
|
ctx.fillText('🍻 PROST!', screenX + 10, 264)
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#fbbf24'
|
ctx.fillStyle = '#fbbf24'
|
||||||
ctx.font = '9px "Press Start 2P", monospace'
|
ctx.font = '8px "Press Start 2P", monospace'
|
||||||
ctx.fillText('DURST!', screenX + 10, 264)
|
ctx.fillText('DURST!', screenX + 10, 264)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Draw Royal Party Wagon (Bollerwagen)
|
// ================= REDESIGNED BOLLERWAGEN (RETRO PIXEL ART) =================
|
||||||
const wx = wagonX
|
const wx = wagonX
|
||||||
const wy = wagonY
|
const wy = wagonY
|
||||||
const bob = Math.sin(Date.now() * 0.015) * 2
|
const bob = showExpressIntro.value ? 0 : Math.sin(Date.now() * 0.015) * 2.5
|
||||||
|
|
||||||
// Wagon wooden bed
|
// 1. Shadow underneath the wagon
|
||||||
ctx.fillStyle = '#78350f'
|
ctx.fillStyle = 'rgba(0,0,0,0.5)'
|
||||||
ctx.fillRect(wx - 20, wy + bob, 95, 24)
|
ctx.beginPath()
|
||||||
|
ctx.ellipse(wx + 30, wy + 32, 54, 8, 0, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// 2. Heavy Rustic Oak Timber Bed with Iron Corner Brackets
|
||||||
|
ctx.fillStyle = '#451a03' // Dark wood outline
|
||||||
|
ctx.fillRect(wx - 26, wy + bob, 114, 26)
|
||||||
|
ctx.fillStyle = '#78350f' // Main oak plank
|
||||||
|
ctx.fillRect(wx - 24, wy + bob + 2, 110, 22)
|
||||||
|
ctx.fillStyle = '#92400e' // Highlight upper plank
|
||||||
|
ctx.fillRect(wx - 24, wy + bob + 4, 110, 8)
|
||||||
|
|
||||||
|
// Iron bands and golden rivets
|
||||||
|
ctx.fillStyle = '#1c1917'
|
||||||
|
ctx.fillRect(wx - 26, wy + bob, 6, 26)
|
||||||
|
ctx.fillRect(wx + 28, wy + bob, 5, 26)
|
||||||
|
ctx.fillRect(wx + 82, wy + bob, 6, 26)
|
||||||
|
ctx.fillStyle = '#ffd700'
|
||||||
|
;[wx - 23, wx + 30, wx + 85].forEach(rx => {
|
||||||
|
ctx.fillRect(rx, wy + bob + 4, 2, 2)
|
||||||
|
ctx.fillRect(rx, wy + bob + 14, 2, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Side-Mounted Hopfen Beer Keg with Brass Faucet
|
||||||
|
ctx.fillStyle = '#292524'
|
||||||
|
ctx.fillRect(wx + 5, wy + bob - 12, 22, 14)
|
||||||
ctx.fillStyle = '#b45309'
|
ctx.fillStyle = '#b45309'
|
||||||
ctx.fillRect(wx - 18, wy + bob + 3, 91, 18)
|
ctx.fillRect(wx + 6, wy + bob - 11, 20, 12)
|
||||||
|
ctx.fillStyle = '#ffd700' // Faucet
|
||||||
|
ctx.fillRect(wx + 26, wy + bob - 6, 5, 3)
|
||||||
|
ctx.fillRect(wx + 29, wy + bob - 3, 2, 4)
|
||||||
|
|
||||||
// Wooden spoke wheels
|
// 4. Giant 135-BPM Bass Subwoofer on the rear
|
||||||
const wheelSpin = (distance * 0.1) % (Math.PI * 2)
|
ctx.fillStyle = '#09090b'
|
||||||
;[-6, 60].forEach(wheelOffset => {
|
ctx.fillRect(wx - 22, wy - 36 + bob, 26, 38)
|
||||||
|
ctx.strokeStyle = '#ffd700'
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.strokeRect(wx - 22, wy - 36 + bob, 26, 38)
|
||||||
|
|
||||||
|
// Pulsing Bass Speaker Cone
|
||||||
|
const bassKick = Math.abs(Math.sin(Date.now() * 0.02) * 5)
|
||||||
|
ctx.fillStyle = '#e11d48'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(wx - 9, wy - 17 + bob, 8 + bassKick, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = '#f43f5e'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.stroke()
|
||||||
|
// Speaker center cap
|
||||||
|
ctx.fillStyle = '#ffd700'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(wx - 9, wy - 17 + bob, 3, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// 5. Hof-DJ Mixer Console & Turntable on the front
|
||||||
|
ctx.fillStyle = '#18181b'
|
||||||
|
ctx.fillRect(wx + 46, wy - 16 + bob, 34, 18)
|
||||||
|
ctx.strokeStyle = '#38bdf8'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeRect(wx + 46, wy - 16 + bob, 34, 18)
|
||||||
|
|
||||||
|
// Vinyl Turntable with spinning vinyl
|
||||||
|
ctx.fillStyle = '#0a0a0a'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(wx + 60, wy - 7 + bob, 7, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.fillStyle = '#ef4444' // Center vinyl label
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(wx + 60, wy - 7 + bob, 2.5, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Flashing Mixer LEDs
|
||||||
|
const ledFlash = Math.floor(Date.now() / 150) % 3
|
||||||
|
ctx.fillStyle = ledFlash === 0 ? '#22c55e' : '#15803d'
|
||||||
|
ctx.fillRect(wx + 72, wy - 12 + bob, 3, 3)
|
||||||
|
ctx.fillStyle = ledFlash === 1 ? '#eab308' : '#854d0e'
|
||||||
|
ctx.fillRect(wx + 72, wy - 8 + bob, 3, 3)
|
||||||
|
ctx.fillStyle = ledFlash === 2 ? '#ef4444' : '#7f1d1d'
|
||||||
|
ctx.fillRect(wx + 72, wy - 4 + bob, 3, 3)
|
||||||
|
|
||||||
|
// 6. KING SASCHA standing proud on the wagon
|
||||||
|
// Flowing Royal Red Velvet Cape
|
||||||
|
ctx.fillStyle = '#b91c1c'
|
||||||
|
ctx.fillRect(wx + 10, wy - 36 + bob, 24, 38)
|
||||||
|
|
||||||
|
// Royal Blue Tunic with Gold Trim
|
||||||
|
ctx.fillStyle = '#1d4ed8'
|
||||||
|
ctx.fillRect(wx + 15, wy - 32 + bob, 18, 24)
|
||||||
|
ctx.fillStyle = '#d4af37'
|
||||||
|
ctx.fillRect(wx + 22, wy - 32 + bob, 4, 24)
|
||||||
|
|
||||||
|
// Face & Beard
|
||||||
|
ctx.fillStyle = '#fed7aa'
|
||||||
|
ctx.fillRect(wx + 16, wy - 46 + bob, 16, 14)
|
||||||
|
// Short Blonde/Brown Hair (as requested)
|
||||||
|
ctx.fillStyle = '#b45309'
|
||||||
|
ctx.fillRect(wx + 15, wy - 50 + bob, 18, 5)
|
||||||
|
|
||||||
|
// Deep Black Royal Sunglasses
|
||||||
|
ctx.fillStyle = '#0f172a'
|
||||||
|
ctx.fillRect(wx + 18, wy - 43 + bob, 6, 4)
|
||||||
|
ctx.fillRect(wx + 25, wy - 43 + bob, 6, 4)
|
||||||
|
ctx.fillRect(wx + 23, wy - 42 + bob, 3, 2) // Bridge
|
||||||
|
|
||||||
|
// Golden Imperial Crown with Jewels
|
||||||
|
ctx.fillStyle = '#facc15'
|
||||||
|
ctx.fillRect(wx + 15, wy - 53 + bob, 18, 5)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(wx + 15, wy - 53 + bob); ctx.lineTo(wx + 15, wy - 58 + bob); ctx.lineTo(wx + 19, wy - 53 + bob)
|
||||||
|
ctx.lineTo(wx + 24, wy - 60 + bob); ctx.lineTo(wx + 29, wy - 53 + bob); ctx.lineTo(wx + 33, wy - 58 + bob)
|
||||||
|
ctx.lineTo(wx + 33, wy - 53 + bob)
|
||||||
|
ctx.fill()
|
||||||
|
// Crown jewels
|
||||||
|
ctx.fillStyle = '#dc2626'
|
||||||
|
ctx.fillRect(wx + 23, wy - 54 + bob, 2, 2)
|
||||||
|
|
||||||
|
// Raised Beer Stein in Right Hand (Prost!)
|
||||||
|
ctx.fillStyle = '#f59e0b' // Golden Beer
|
||||||
|
ctx.fillRect(wx + 35, wy - 44 + bob, 8, 12)
|
||||||
|
ctx.fillStyle = '#ffffff' // White froth/foam
|
||||||
|
ctx.fillRect(wx + 34, wy - 47 + bob, 10, 4)
|
||||||
|
ctx.fillStyle = '#cbd5e1' // Glass handle
|
||||||
|
ctx.fillRect(wx + 42, wy - 42 + bob, 3, 7)
|
||||||
|
|
||||||
|
// 7. Detailed Wooden Spoke Wheels with Golden Brass Caps
|
||||||
|
const wheelSpin = (distance * 0.08) % (Math.PI * 2)
|
||||||
|
;[-8, 68].forEach(wheelOffset => {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(wx + wheelOffset, wy + 24)
|
ctx.translate(wx + wheelOffset, wy + 26)
|
||||||
ctx.rotate(wheelSpin)
|
ctx.rotate(wheelSpin)
|
||||||
ctx.fillStyle = '#451a03'
|
|
||||||
|
// Outer dark tyre
|
||||||
|
ctx.fillStyle = '#1c1917'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(0, 0, 13, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Inner wooden rim
|
||||||
|
ctx.fillStyle = '#78350f'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(0, 0, 11, 0, Math.PI * 2)
|
ctx.arc(0, 0, 11, 0, Math.PI * 2)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
|
// Brass rim edge
|
||||||
ctx.strokeStyle = '#ffd700'
|
ctx.strokeStyle = '#ffd700'
|
||||||
ctx.lineWidth = 2.5
|
ctx.lineWidth = 1.5
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
// Spokes
|
|
||||||
|
// 8 Spokes
|
||||||
ctx.strokeStyle = '#b45309'
|
ctx.strokeStyle = '#b45309'
|
||||||
ctx.lineWidth = 1.5
|
ctx.lineWidth = 1.5
|
||||||
|
for (let s = 0; s < 4; s++) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(-11 * Math.cos(s * Math.PI / 4), -11 * Math.sin(s * Math.PI / 4))
|
||||||
|
ctx.lineTo(11 * Math.cos(s * Math.PI / 4), 11 * Math.sin(s * Math.PI / 4))
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center brass axle cap
|
||||||
|
ctx.fillStyle = '#facc15'
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(-11, 0); ctx.lineTo(11, 0)
|
ctx.arc(0, 0, 4, 0, Math.PI * 2)
|
||||||
ctx.moveTo(0, -11); ctx.lineTo(0, 11)
|
ctx.fill()
|
||||||
ctx.stroke()
|
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sascha standing on wagon with crown, sunglasses & beer mug
|
// Top Banner HUD
|
||||||
ctx.fillStyle = '#dc2626' // Cape
|
|
||||||
ctx.fillRect(wx + 12, wy - 30 + bob, 22, 34)
|
|
||||||
ctx.fillStyle = '#1d4ed8' // Tunic
|
|
||||||
ctx.fillRect(wx + 16, wy - 28 + bob, 16, 22)
|
|
||||||
ctx.fillStyle = '#fcd34d' // Face
|
|
||||||
ctx.fillRect(wx + 17, wy - 42 + bob, 14, 14)
|
|
||||||
// Sunglasses
|
|
||||||
ctx.fillStyle = '#0f172a'
|
|
||||||
ctx.fillRect(wx + 19, wy - 38 + bob, 5, 3)
|
|
||||||
ctx.fillRect(wx + 25, wy - 38 + bob, 5, 3)
|
|
||||||
// Crown
|
|
||||||
ctx.fillStyle = '#facc15'
|
|
||||||
ctx.fillRect(wx + 16, wy - 48 + bob, 16, 6)
|
|
||||||
|
|
||||||
// Giant Bass Speaker on the back of the wagon
|
|
||||||
ctx.fillStyle = '#18181b'
|
|
||||||
ctx.fillRect(wx - 16, wy - 34 + bob, 24, 34)
|
|
||||||
ctx.fillStyle = '#f43f5e'
|
|
||||||
ctx.beginPath()
|
|
||||||
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.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
|
|
||||||
ctx.fillStyle = '#ffd700'
|
ctx.fillStyle = '#ffd700'
|
||||||
ctx.font = 'bold 8.5px "Press Start 2P", monospace'
|
ctx.font = 'bold 8px "Press Start 2P", monospace'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.fillText('🎧 135 BPM PARTY EXPRESS 🎧', w / 2, 24)
|
ctx.fillText('AKT IV: DER 135-BPM-FEST-KONVOI', w / 2, 22)
|
||||||
|
|
||||||
// Progress Bar
|
// Progress Bar
|
||||||
const progress = Math.min(1, distance / maxDistance)
|
const progress = Math.min(1, distance / maxDistance)
|
||||||
const barMargin = 24
|
const barMargin = 20
|
||||||
const barWidth = w - barMargin * 2
|
const barWidth = w - barMargin * 2
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.6)'
|
ctx.fillStyle = 'rgba(0,0,0,0.7)'
|
||||||
ctx.fillRect(barMargin, 34, barWidth, 8)
|
ctx.fillRect(barMargin, 32, barWidth, 10)
|
||||||
|
ctx.strokeStyle = '#ffd700'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeRect(barMargin, 32, barWidth, 10)
|
||||||
|
|
||||||
|
// Progress fill
|
||||||
ctx.fillStyle = '#22c55e'
|
ctx.fillStyle = '#22c55e'
|
||||||
ctx.fillRect(barMargin, 34, barWidth * progress, 8)
|
ctx.fillRect(barMargin + 1, 33, (barWidth - 2) * progress, 8)
|
||||||
|
|
||||||
|
// Moving Keg Icon on the progress bar
|
||||||
|
const kegX = barMargin + (barWidth * progress)
|
||||||
|
ctx.font = '10px sans-serif'
|
||||||
|
ctx.fillText('🍺', kegX, 39)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
@@ -281,6 +424,38 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
<span class="express-hint">Tippe, wenn du an deinen Freunden vorbeifährst!</span>
|
<span class="express-hint">Tippe, wenn du an deinen Freunden vorbeifährst!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pre-Level Start Screen Modal -->
|
||||||
|
<div v-if="showExpressIntro" class="level-intro-modal">
|
||||||
|
<div class="intro-card">
|
||||||
|
<div class="intro-act-tag">👑 AKT IV: FAHRGESCHÄFT</div>
|
||||||
|
<h2 class="intro-card-title">DER 135-BPM-FEST-KONVOI</h2>
|
||||||
|
<p class="intro-card-story">
|
||||||
|
Der Hof-Bass wummert, der Bollerwagen ist poliert!<br />
|
||||||
|
König Sascha zieht mit der Reichs-Soundanlage über die Festmeile von Haumdaucher. Deine Freunde stehen durstig am Straßenrand!
|
||||||
|
</p>
|
||||||
|
<div class="mission-callout">
|
||||||
|
<span class="callout-icon">🎯</span>
|
||||||
|
<div class="callout-text">
|
||||||
|
<strong>KÖNIGLICHE MISSION:</strong><br />
|
||||||
|
Versorge Hansi, Viktor, Albin, Fabian, Flo Maier & das Gefolge mit kühlem Gerstensaft im Takt des Basses!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls-overview">
|
||||||
|
<div class="ctrl-item">
|
||||||
|
<span class="ctrl-badge">🍺 BIER SCHENKEN</span>
|
||||||
|
<span class="ctrl-label">Tippe den Button, sobald du an einem Freund bist</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-item">
|
||||||
|
<span class="ctrl-badge">🏆 TIMING</span>
|
||||||
|
<span class="ctrl-label">Jedes glückliche Gefolge bringt fette Highscore-Punkte!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="start-act-btn" @click="startExpress">
|
||||||
|
🚀 PARTY-EXPRESS STARTEN!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -320,7 +495,7 @@ onUnmounted(() => {
|
|||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 8.5px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||||
@@ -329,7 +504,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { transform: scale(1); }
|
0%, 100% { transform: scale(1); }
|
||||||
50% { transform: scale(1.05); }
|
50% { transform: scale(1.04); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.express-hint {
|
.express-hint {
|
||||||
@@ -338,4 +513,125 @@ onUnmounted(() => {
|
|||||||
color: #fef08a;
|
color: #fef08a;
|
||||||
text-shadow: 1px 1px 2px #000;
|
text-shadow: 1px 1px 2px #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pre-Level Start Screen Modal */
|
||||||
|
.level-intro-modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 50;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card {
|
||||||
|
background: #231209;
|
||||||
|
border: 3px double #d4af37;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
max-width: 320px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.95);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-act-tag {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6.5px;
|
||||||
|
color: #f59e0b;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-title {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 9.5px;
|
||||||
|
color: #ffd700;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-story {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fef08a;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-callout {
|
||||||
|
background: #451a03;
|
||||||
|
border-left: 3px solid #22c55e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-text {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #bbf7d0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
background: #170c06;
|
||||||
|
border: 1px solid #78350f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-badge {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6.5px;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-label {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-act-btn {
|
||||||
|
background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #ffd700;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-act-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import { sound } from '../audio/synthAudio'
|
|||||||
|
|
||||||
const store = useGameStore()
|
const store = useGameStore()
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const showLevelIntro = ref(true)
|
||||||
let animationFrameId: number | null = null
|
let animationFrameId: number | null = null
|
||||||
|
|
||||||
|
function startLevel() {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
showLevelIntro.value = false
|
||||||
|
}
|
||||||
|
|
||||||
// Game state
|
// Game state
|
||||||
interface Platform {
|
interface Platform {
|
||||||
x: number
|
x: number
|
||||||
@@ -428,7 +434,9 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gameLoop() {
|
function gameLoop() {
|
||||||
updatePhysics()
|
if (!showLevelIntro.value) {
|
||||||
|
updatePhysics()
|
||||||
|
}
|
||||||
if (canvasRef.value) {
|
if (canvasRef.value) {
|
||||||
const ctx = canvasRef.value.getContext('2d')
|
const ctx = canvasRef.value.getContext('2d')
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
@@ -481,10 +489,42 @@ onUnmounted(() => {
|
|||||||
<div class="platformer-hud-top">
|
<div class="platformer-hud-top">
|
||||||
<div class="hud-korken-badge">👑 {{ store.kronkorken }} KORKEN</div>
|
<div class="hud-korken-badge">👑 {{ store.kronkorken }} KORKEN</div>
|
||||||
<div class="level-overlay">
|
<div class="level-overlay">
|
||||||
<span class="level-title">AKTE II: DIE HOPFEN-PFADE</span>
|
<span class="level-title">AKT II: DIE HOPFEN-PFADE</span>
|
||||||
<span class="level-hint">Tippe oder drücke SPRUNG zum Hüpfen</span>
|
<span class="level-hint">Tippe oder drücke SPRUNG zum Hüpfen</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pre-Level Start Screen Modal -->
|
||||||
|
<div v-if="showLevelIntro" class="level-intro-modal">
|
||||||
|
<div class="intro-card">
|
||||||
|
<div class="intro-act-tag">👑 AKT II: JUMP & RUN</div>
|
||||||
|
<h2 class="intro-card-title">DIE HOPFEN-PFADE</h2>
|
||||||
|
<p class="intro-card-story">
|
||||||
|
Hört die Kunde, edler König Sascha!<br />
|
||||||
|
Deine Untertanen dürsten im ganzen Herzogtum nach frischem Gerstensaft. Doch ohne deine 40 heiligen Kronkorken bleiben die Fässer versiegelt!
|
||||||
|
</p>
|
||||||
|
<div class="mission-callout">
|
||||||
|
<span class="callout-icon">🎯</span>
|
||||||
|
<div class="callout-text">
|
||||||
|
<strong>KÖNIGLICHE MISSION:</strong><br />
|
||||||
|
Hüpfe über Ziegeldächer und Holzgerüste. Sammle mindestens <strong>5 Kronkorken</strong>, um das Tor zur Taverne zu entriegeln!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls-overview">
|
||||||
|
<div class="ctrl-item">
|
||||||
|
<span class="ctrl-badge">🕹️ STEUERKREUZ</span>
|
||||||
|
<span class="ctrl-label">Laufen (Links / Rechts)</span>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-item">
|
||||||
|
<span class="ctrl-badge">🅰️ A-TASTE / TIPPE</span>
|
||||||
|
<span class="ctrl-label">Sprung (auch Doppel-Sprung!)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="start-act-btn" @click="startLevel">
|
||||||
|
🚀 AUF DIE DÄCHER! (START)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -550,4 +590,124 @@ onUnmounted(() => {
|
|||||||
color: #fef08a;
|
color: #fef08a;
|
||||||
text-shadow: 1px 1px 0 #000;
|
text-shadow: 1px 1px 0 #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pre-Level Start Screen Modal */
|
||||||
|
.level-intro-modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 50;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card {
|
||||||
|
background: #231209;
|
||||||
|
border: 3px double #d4af37;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
max-width: 320px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.95);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-act-tag {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6.5px;
|
||||||
|
color: #f59e0b;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-title {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #ffd700;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-story {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fef08a;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-callout {
|
||||||
|
background: #451a03;
|
||||||
|
border-left: 3px solid #22c55e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-text {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #bbf7d0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
background: #170c06;
|
||||||
|
border: 1px solid #78350f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-badge {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6.5px;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-label {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-act-btn {
|
||||||
|
background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #ffd700;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-act-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
<!-- Balcony Visual Scene with Regal Portrait Frame -->
|
<!-- Balcony Visual Scene with Regal Portrait Frame -->
|
||||||
<div class="scene-visual">
|
<div class="scene-visual">
|
||||||
<div class="balcony-backdrop"></div>
|
<div class="balcony-backdrop"></div>
|
||||||
|
<div class="scene-tag">
|
||||||
|
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
|
||||||
|
</div>
|
||||||
<div class="speaker-frame">
|
<div class="speaker-frame">
|
||||||
<img
|
<img
|
||||||
:src="currentSpeaker().portrait"
|
:src="currentSpeaker().portrait"
|
||||||
@@ -43,9 +46,6 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
class="speaker-visual"
|
class="speaker-visual"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="scene-tag">
|
|
||||||
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Retro Dialogue Card -->
|
<!-- Retro Dialogue Card -->
|
||||||
@@ -88,13 +88,15 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
|
|
||||||
.scene-visual {
|
.scene-visual {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
min-height: 160px;
|
min-height: 175px;
|
||||||
max-height: 200px;
|
padding: 10px 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
background: radial-gradient(circle at 50% 40%, #4a2818 0%, #1a0f0a 100%);
|
background: radial-gradient(circle at 50% 40%, #4a2818 0%, #1a0f0a 100%);
|
||||||
border-bottom: 2px solid #d4af37;
|
border-bottom: 2px solid #d4af37;
|
||||||
}
|
}
|
||||||
@@ -109,10 +111,27 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-tag {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
border: 1px solid #ffd700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-title {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7px;
|
||||||
|
color: #ffd700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.speaker-frame {
|
.speaker-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 140px;
|
width: 116px;
|
||||||
height: 140px;
|
height: 116px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 3px double #ffd700;
|
border: 3px double #ffd700;
|
||||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.8), 0 0 10px rgba(212, 175, 55, 0.4);
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.8), 0 0 10px rgba(212, 175, 55, 0.4);
|
||||||
@@ -129,22 +148,6 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-tag {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
border: 1px solid #ffd700;
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scene-title {
|
|
||||||
font-family: 'Press Start 2P', monospace;
|
|
||||||
font-size: 7px;
|
|
||||||
color: #ffd700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-card {
|
.dialogue-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #26160e;
|
background: #26160e;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { sound } from '../audio/synthAudio'
|
|||||||
|
|
||||||
const store = useGameStore()
|
const store = useGameStore()
|
||||||
|
|
||||||
|
type TavernRoom = 'schankraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
|
||||||
|
const currentRoom = ref<TavernRoom>('schankraum')
|
||||||
|
|
||||||
interface ActiveDialog {
|
interface ActiveDialog {
|
||||||
characterId: string
|
characterId: string
|
||||||
name: string
|
name: string
|
||||||
@@ -23,6 +26,17 @@ const duelStep = ref(0)
|
|||||||
const duelWon = ref(false)
|
const duelWon = ref(false)
|
||||||
const zulumanMirrorActive = ref(false)
|
const zulumanMirrorActive = ref(false)
|
||||||
|
|
||||||
|
function switchRoom(room: TavernRoom) {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
currentRoom.value = room
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterSecretHinterzimmer() {
|
||||||
|
sound.playFanfare()
|
||||||
|
store.unlockEasterEgg('hinterzimmer')
|
||||||
|
currentRoom.value = 'hinterzimmer'
|
||||||
|
}
|
||||||
|
|
||||||
function openCharacterDialog(charId: string) {
|
function openCharacterDialog(charId: string) {
|
||||||
const char = CHARACTERS[charId]
|
const char = CHARACTERS[charId]
|
||||||
if (!char) return
|
if (!char) return
|
||||||
@@ -81,42 +95,16 @@ function openCharacterDialog(charId: string) {
|
|||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (charId === 'pue') {
|
|
||||||
activeDialog.value = {
|
|
||||||
characterId: charId,
|
|
||||||
name: char.name,
|
|
||||||
title: char.title,
|
|
||||||
portrait: char.portrait,
|
|
||||||
text: 'Majestät! Die magischen Kristall-Fackeln sind verkabelt und synchronisiert. Sobald der Drop bei 135 BPM einschlägt, brennt der Saal!',
|
|
||||||
actionLabel: 'Zünde den Stroboskop-Zauber!',
|
|
||||||
actionFn: () => {
|
|
||||||
store.addKronkorken(4)
|
|
||||||
activeDialog.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (charId === 'mane') {
|
|
||||||
activeDialog.value = {
|
|
||||||
characterId: charId,
|
|
||||||
name: char.name,
|
|
||||||
title: char.title,
|
|
||||||
portrait: char.portrait,
|
|
||||||
text: 'Servus beinand! Ich wandere grad gemütlich durch die Schenke. Wander-Tipp: Wenn du nachher auf den Festwagen steigst, halte dich an die Kronkorken-Spur!',
|
|
||||||
actionLabel: 'Danke Mane! Berg heil!',
|
|
||||||
actionFn: () => {
|
|
||||||
store.addKronkorken(2)
|
|
||||||
activeDialog.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (charId === 'mario') {
|
} else if (charId === 'mario') {
|
||||||
activeDialog.value = {
|
activeDialog.value = {
|
||||||
characterId: charId,
|
characterId: charId,
|
||||||
name: char.name,
|
name: char.name,
|
||||||
title: char.title,
|
title: char.title,
|
||||||
portrait: char.portrait,
|
portrait: char.portrait,
|
||||||
text: 'Hoppla die Waldfee! Schau wie die Bierkrüge und Hopfendolden durch die Luft wirbeln! Ein Hoch auf König Sascha!',
|
text: 'Hoppla die Waldfee! Schau wie die kreischenden Kettensägen und Hopfenkrüge durch die Luft wirbeln! Keine Sorge Majestät, die Ketten sind frisch geölt! Nimm diese 4 Meister-Korken!',
|
||||||
actionLabel: 'Sauber jongliert, Mario! (+3 Kronkorken)',
|
actionLabel: 'Sauber jongliert, Mario! (+4 Kronkorken)',
|
||||||
actionFn: () => {
|
actionFn: () => {
|
||||||
store.addKronkorken(3)
|
store.addKronkorken(4)
|
||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +122,72 @@ function openCharacterDialog(charId: string) {
|
|||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (charId === 'mane') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Servus beinand! Ich wandere grad über den Haumdaucher Höhenweg. Wander-Tipp: Wer rastet, der rostet – und wer nicht tanzt, kriegt keinen Schaum auf die Halbe! Hier, 4 Kronkorken für den Gipfelsturm!',
|
||||||
|
actionLabel: 'Danke Mane! Berg heil! (+4 Kronkorken)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(4)
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (charId === 'hansi') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Habe die Ehre Sascha! Schau dir dieses Panorama an. Ich halte hier die Stellung und habe dir 4 Kronkorken gesichert!',
|
||||||
|
actionLabel: 'Danke Hansi! (+4 Kronkorken)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(4)
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (charId === 'pue') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Majestät! Willkommen im innersten VIP-Kreis des Basses! Die magischen Kristalle vibrieren auf 135 BPM und die Lasershow brennt! Hier sind 5 Kronkorken für den Stroboskop-Zauber!',
|
||||||
|
actionLabel: 'Zünde den Kristall-Zauber, Pü! (+5 Kronkorken)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(5)
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (charId === 'flo_maier') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Habe die Ehre Sascha! Die Monitore wummern mit 135 BPM! Ich hab dir eine geheime Bass-Platte aufgelegt – hier sind 5 Meister-Kronkorken für den nächsten Drop!',
|
||||||
|
actionLabel: 'Dreh die Bässe auf, Flo! (+5 Kronkorken)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(5)
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (charId === 'viktor') {
|
||||||
|
activeDialog.value = {
|
||||||
|
characterId: charId,
|
||||||
|
name: char.name,
|
||||||
|
title: char.title,
|
||||||
|
portrait: char.portrait,
|
||||||
|
text: 'Servus König Sascha! Im Hinterzimmer fließt das Helle kälter als die Nacht! Hoch die Krüge auf 40 Jahre Regentschaft!',
|
||||||
|
actionLabel: 'Prost Viktor! (+3 Kronkorken & Helles)',
|
||||||
|
actionFn: () => {
|
||||||
|
store.addKronkorken(3)
|
||||||
|
store.addBeer()
|
||||||
|
activeDialog.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,57 +226,192 @@ function proceedToExpress() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="tavern-container">
|
<div class="tavern-container">
|
||||||
<div class="scene-viewport">
|
<div class="scene-viewport">
|
||||||
<img src="/assets/scenes/tavern.jpg" alt="Taverne Zum wummernden Ritter" class="tavern-bg" />
|
|
||||||
|
|
||||||
<!-- Interactive Hotspots on the visual scene with Avatar Thumbnails -->
|
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
|
||||||
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
<template v-if="currentRoom === 'schankraum'">
|
||||||
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
|
<img src="/assets/scenes/tavern.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||||
<span class="hotspot-badge">Verena 👑</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
|
<!-- Verena on the wooden gallery -->
|
||||||
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
|
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
||||||
<span class="hotspot-badge">Armend 🔨</span>
|
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
|
||||||
</button>
|
<span class="hotspot-badge">Verena 👑</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
|
<!-- Armend at the forge/anvil -->
|
||||||
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
|
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
|
||||||
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
|
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
|
||||||
</button>
|
<span class="hotspot-badge">Armend 🔨</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
|
<!-- Lutz at the fireplace -->
|
||||||
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
|
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
|
||||||
<span class="hotspot-badge">Amy 🎨</span>
|
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
|
||||||
</button>
|
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-mirror" @click="inspectMirror">
|
<!-- Amy at the tavern table -->
|
||||||
<span class="mirror-icon">🪞</span>
|
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
|
||||||
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
|
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
|
||||||
</button>
|
<span class="hotspot-badge">Amy 🎨</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-pue" @click="openCharacterDialog('pue')">
|
<!-- Zuluman Magic Mirror on the pillar -->
|
||||||
<img :src="CHARACTERS.pue.portrait" class="hotspot-thumb" alt="Pü" />
|
<button class="hotspot hotspot-mirror" @click="inspectMirror">
|
||||||
<span class="hotspot-badge">Pü (Licht) 💡</span>
|
<span class="mirror-icon">🪞</span>
|
||||||
</button>
|
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-mane" @click="openCharacterDialog('mane')">
|
<!-- Doorway to Marktplatz -->
|
||||||
<img :src="CHARACTERS.mane.portrait" class="hotspot-thumb" alt="Mane" />
|
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
|
||||||
<span class="hotspot-badge">Mane 🥾</span>
|
<span class="door-icon">🚪</span>
|
||||||
</button>
|
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
|
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer -->
|
||||||
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
|
<button class="hotspot secret-tap-hotspot" @click="enterSecretHinterzimmer" title="Ein sonderbarer goldener Zapfhahn...">
|
||||||
<span class="hotspot-badge">NAT 🐗</span>
|
<span class="secret-tap-icon pulse">🗝️</span>
|
||||||
</button>
|
<span class="secret-badge">Geheimer Zapfhahn</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
<!-- ================= 2. MARKTPLATZ ================= -->
|
||||||
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
|
<template v-else-if="currentRoom === 'marktplatz'">
|
||||||
<span class="hotspot-badge">Mario 🤹</span>
|
<img src="/assets/scenes/marktplatz.jpg" alt="Marktplatz Haumdaucher" class="tavern-bg" />
|
||||||
</button>
|
|
||||||
|
<!-- Gaukler Mario juggling chainsaws with animations & flying sparks -->
|
||||||
|
<div class="mario-container">
|
||||||
|
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
||||||
|
<div class="chainsaw-circus">
|
||||||
|
<span class="saw saw-left">🪚</span>
|
||||||
|
<span class="saw saw-right">🪚</span>
|
||||||
|
<span class="spark spark-1">⚡</span>
|
||||||
|
<span class="spark spark-2">✨</span>
|
||||||
|
</div>
|
||||||
|
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
|
||||||
|
<span class="hotspot-badge">Mario (Kettensägen!) 🤹</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NAT at the fountain -->
|
||||||
|
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
|
||||||
|
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
|
||||||
|
<span class="hotspot-badge">NAT (Josef) 🐗</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Door back to Gaststube -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-back-gaststube" @click="switchRoom('schankraum')">
|
||||||
|
<span class="door-icon">🚪</span>
|
||||||
|
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Trail to Hinterland -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-hinterland" @click="switchRoom('hinterland')">
|
||||||
|
<span class="door-icon">🌲</span>
|
||||||
|
<span class="hotspot-badge glow-door">Zum Hinterland ▶</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ================= 3. HINTERLAND ================= -->
|
||||||
|
<template v-else-if="currentRoom === 'hinterland'">
|
||||||
|
<img src="/assets/scenes/hinterland.jpg" alt="Hinterland Haumdaucher Alm" class="tavern-bg" />
|
||||||
|
|
||||||
|
<!-- Wanderin Mane (Ramona) hiking with animated stride -->
|
||||||
|
<button class="hotspot hotspot-mane hiking-anim" @click="openCharacterDialog('mane')">
|
||||||
|
<span class="hiking-stick">🦯</span>
|
||||||
|
<img :src="CHARACTERS.mane.portrait" class="hotspot-thumb" alt="Mane" />
|
||||||
|
<span class="hotspot-badge">Mane (Wanderin) 🥾</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Hansi enjoying the view on the bench -->
|
||||||
|
<button class="hotspot hotspot-hansi" @click="openCharacterDialog('hansi')">
|
||||||
|
<img :src="CHARACTERS.hansi.portrait" class="hotspot-thumb" alt="Hansi" />
|
||||||
|
<span class="hotspot-badge">Hansi 🧔</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Trail back to Marktplatz -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-back-marktplatz" @click="switchRoom('marktplatz')">
|
||||||
|
<span class="door-icon">🎪</span>
|
||||||
|
<span class="hotspot-badge glow-door">◀ Zum Marktplatz</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ================= 4. HINTERZIMMER (SECRET VIP RAVE) ================= -->
|
||||||
|
<template v-else-if="currentRoom === 'hinterzimmer'">
|
||||||
|
<img src="/assets/scenes/hinterzimmer.jpg" alt="Geheimes VIP Hinterzimmer" class="tavern-bg" />
|
||||||
|
<div class="strobe-overlay"></div>
|
||||||
|
|
||||||
|
<!-- Pü with pulsing crystal staff and disco lasers -->
|
||||||
|
<button class="hotspot hotspot-pue rave-pulse" @click="openCharacterDialog('pue')">
|
||||||
|
<span class="crystal-laser">💎✨</span>
|
||||||
|
<img :src="CHARACTERS.pue.portrait" class="hotspot-thumb" alt="Pü" />
|
||||||
|
<span class="hotspot-badge">Pü (Kristall-Zauber) 💡</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Flo Maier at mixing deck -->
|
||||||
|
<button class="hotspot hotspot-flo" @click="openCharacterDialog('flo_maier')">
|
||||||
|
<img :src="CHARACTERS.flo_maier.portrait" class="hotspot-thumb" alt="Flo Maier" />
|
||||||
|
<span class="hotspot-badge">DJ Flo Maier 🎧</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Viktor the vampire night-guard -->
|
||||||
|
<button class="hotspot hotspot-viktor" @click="openCharacterDialog('viktor')">
|
||||||
|
<img :src="CHARACTERS.viktor.portrait" class="hotspot-thumb" alt="Viktor" />
|
||||||
|
<span class="hotspot-badge">Viktor 🧛</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Door back to Gaststube -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-back-vip" @click="switchRoom('schankraum')">
|
||||||
|
<span class="door-icon">🚪</span>
|
||||||
|
<span class="hotspot-badge glow-door">◀ Zurück in die Gaststube</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Bottom Scene Navigation & Duel -->
|
<!-- Bottom Scene Navigation & Duel -->
|
||||||
<div class="scene-bottom-nav">
|
<div class="scene-bottom-nav">
|
||||||
<div class="tavern-bar-title">🍻 TAVERNE ZUM WUMMERNDEN RITTER 🍻</div>
|
<!-- Room Title Badge -->
|
||||||
|
<div class="tavern-bar-title">
|
||||||
|
{{
|
||||||
|
currentRoom === 'schankraum' ? '🍻 AKT III: GASTSTUBE ZUM WUMMERNDEN RITTER 🍻' :
|
||||||
|
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
|
||||||
|
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
|
||||||
|
'🪩 AKT III: GEHEIMES VIP-HINTERZIMMER 🪩'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Room Navigation Tabs -->
|
||||||
|
<div class="room-nav-tabs">
|
||||||
|
<button
|
||||||
|
class="room-tab"
|
||||||
|
:class="{ active: currentRoom === 'schankraum' }"
|
||||||
|
@click="switchRoom('schankraum')"
|
||||||
|
>
|
||||||
|
🍺 Gaststube
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="room-tab"
|
||||||
|
:class="{ active: currentRoom === 'marktplatz' }"
|
||||||
|
@click="switchRoom('marktplatz')"
|
||||||
|
>
|
||||||
|
🎪 Marktplatz
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="room-tab"
|
||||||
|
:class="{ active: currentRoom === 'hinterland' }"
|
||||||
|
@click="switchRoom('hinterland')"
|
||||||
|
>
|
||||||
|
🌲 Hinterland
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="store.unlockedEasterEggs.includes('hinterzimmer') || currentRoom === 'hinterzimmer'"
|
||||||
|
class="room-tab vip-tab"
|
||||||
|
:class="{ active: currentRoom === 'hinterzimmer' }"
|
||||||
|
@click="switchRoom('hinterzimmer')"
|
||||||
|
>
|
||||||
|
🪩 Hinterzimmer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Row (Duel & Next Level) -->
|
||||||
<div class="tavern-nav-buttons">
|
<div class="tavern-nav-buttons">
|
||||||
<button class="duel-trigger" @click="startGstanzlDuel">
|
<button class="duel-trigger" @click="startGstanzlDuel">
|
||||||
⚔️ Gstanzl-Fechten
|
⚔️ Gstanzl-Fechten
|
||||||
@@ -338,7 +527,7 @@ function proceedToExpress() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: rgba(26, 15, 10, 0.85);
|
background: rgba(26, 15, 10, 0.88);
|
||||||
border: 1.5px solid #ffd700;
|
border: 1.5px solid #ffd700;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 2px 6px 2px 2px;
|
padding: 2px 6px 2px 2px;
|
||||||
@@ -363,8 +552,8 @@ function proceedToExpress() {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mirror-icon {
|
.mirror-icon, .door-icon {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,15 +564,164 @@ function proceedToExpress() {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-verena { top: 15%; left: 80%; }
|
/* Door Hotspots */
|
||||||
|
.door-hotspot {
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
border: 1.5px solid #38bdf8;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-door {
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secret Tap Easter Egg */
|
||||||
|
.secret-tap-hotspot {
|
||||||
|
position: absolute;
|
||||||
|
top: 73%;
|
||||||
|
left: 88%;
|
||||||
|
background: rgba(234, 179, 8, 0.2);
|
||||||
|
border: 1.5px dashed #ffd700;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-tap-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-badge {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 5.5px;
|
||||||
|
color: #fef08a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 1. Schankraum Hotspots */
|
||||||
|
.hotspot-verena { top: 16%; left: 78%; }
|
||||||
.hotspot-armend { top: 62%; left: 24%; }
|
.hotspot-armend { top: 62%; left: 24%; }
|
||||||
.hotspot-lutz { top: 38%; left: 20%; }
|
.hotspot-lutz { top: 38%; left: 20%; }
|
||||||
|
.hotspot-amy { top: 52%; left: 80%; }
|
||||||
.hotspot-mirror { top: 32%; left: 63%; }
|
.hotspot-mirror { top: 32%; left: 63%; }
|
||||||
.hotspot-amy { top: 52%; left: 82%; }
|
.hotspot-door-marktplatz { top: 56%; left: 90%; }
|
||||||
.hotspot-pue { top: 20%; left: 35%; }
|
|
||||||
.hotspot-mane { top: 45%; left: 66%; }
|
/* 2. Marktplatz Hotspots */
|
||||||
.hotspot-nat { top: 63%; left: 85%; }
|
.mario-container {
|
||||||
.hotspot-mario { top: 36%; left: 90%; }
|
position: absolute;
|
||||||
|
top: 52%;
|
||||||
|
left: 34%;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotspot-mario {
|
||||||
|
position: relative;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chainsaw-circus {
|
||||||
|
position: absolute;
|
||||||
|
top: -24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saw {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 14px;
|
||||||
|
animation: spinSaw 0.3s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saw-left {
|
||||||
|
left: -4px;
|
||||||
|
animation-direction: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saw-right {
|
||||||
|
right: -4px;
|
||||||
|
animation-direction: reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spark {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 10px;
|
||||||
|
animation: sparkFlicker 0.2s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spark-1 { top: -6px; left: 10px; color: #f59e0b; }
|
||||||
|
.spark-2 { top: 4px; right: 10px; color: #ef4444; }
|
||||||
|
|
||||||
|
@keyframes spinSaw {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkFlicker {
|
||||||
|
0% { opacity: 0.2; transform: scale(0.8); }
|
||||||
|
100% { opacity: 1; transform: scale(1.3); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotspot-nat { top: 62%; left: 74%; }
|
||||||
|
.hotspot-door-back-gaststube { top: 72%; left: 16%; }
|
||||||
|
.hotspot-door-hinterland { top: 36%; left: 84%; }
|
||||||
|
|
||||||
|
/* 3. Hinterland Hotspots */
|
||||||
|
.hotspot-mane { top: 50%; left: 42%; }
|
||||||
|
.hiking-anim {
|
||||||
|
animation: hikeBob 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hikeBob {
|
||||||
|
0%, 100% { transform: translate(-50%, -50%) translateY(0); }
|
||||||
|
50% { transform: translate(-50%, -50%) translateY(-6px) rotate(-2deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiking-stick {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotspot-hansi { top: 66%; left: 78%; }
|
||||||
|
.hotspot-door-back-marktplatz { top: 74%; left: 20%; }
|
||||||
|
|
||||||
|
/* 4. Hinterzimmer Hotspots & Strobe */
|
||||||
|
.strobe-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
animation: discoFlash 0.6s infinite alternate;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes discoFlash {
|
||||||
|
0% { background: rgba(236, 72, 153, 0.25); }
|
||||||
|
33% { background: rgba(59, 130, 246, 0.25); }
|
||||||
|
66% { background: rgba(234, 179, 8, 0.25); }
|
||||||
|
100% { background: rgba(168, 85, 247, 0.25); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotspot-pue { top: 46%; left: 48%; }
|
||||||
|
.rave-pulse {
|
||||||
|
animation: raveGlow 1s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes raveGlow {
|
||||||
|
0% { box-shadow: 0 0 4px #eab308; }
|
||||||
|
100% { box-shadow: 0 0 16px #ec4899; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.crystal-laser {
|
||||||
|
font-size: 13px;
|
||||||
|
animation: pulseAnim 0.8s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotspot-flo { top: 40%; left: 22%; }
|
||||||
|
.hotspot-viktor { top: 62%; left: 78%; }
|
||||||
|
.hotspot-door-back-vip { top: 78%; left: 50%; }
|
||||||
|
|
||||||
.pulse {
|
.pulse {
|
||||||
animation: pulseAnim 1.5s infinite;
|
animation: pulseAnim 1.5s infinite;
|
||||||
@@ -394,12 +732,13 @@ function proceedToExpress() {
|
|||||||
50% { box-shadow: 0 0 12px #ec4899; }
|
50% { box-shadow: 0 0 12px #ec4899; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bottom Navigation */
|
||||||
.scene-bottom-nav {
|
.scene-bottom-nav {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 84px;
|
height: 94px;
|
||||||
background: linear-gradient(180deg, #451a03 0%, #26140b 100%);
|
background: linear-gradient(180deg, #451a03 0%, #26140b 100%);
|
||||||
border-top: 3px solid #b45309;
|
border-top: 3px solid #b45309;
|
||||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.95);
|
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.95);
|
||||||
@@ -407,20 +746,53 @@ function proceedToExpress() {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
padding: 6px 12px;
|
padding: 6px 10px;
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tavern-bar-title {
|
.tavern-bar-title {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 7px;
|
font-size: 6.5px;
|
||||||
color: #fef08a;
|
color: #fef08a;
|
||||||
text-shadow: 1px 1px 0 #000;
|
text-shadow: 1px 1px 0 #000;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.3px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.room-nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-tab {
|
||||||
|
background: #2b1810;
|
||||||
|
border: 1px solid #78350f;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-tab.active {
|
||||||
|
background: #b45309;
|
||||||
|
border-color: #ffd700;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 6px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-tab {
|
||||||
|
background: #3b0764;
|
||||||
|
border-color: #a855f7;
|
||||||
|
color: #f0abfc;
|
||||||
|
animation: pulseAnim 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.tavern-nav-buttons {
|
.tavern-nav-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -435,8 +807,8 @@ function proceedToExpress() {
|
|||||||
border: 1.5px solid #ffd700;
|
border: 1.5px solid #ffd700;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 7.5px;
|
font-size: 7px;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
||||||
}
|
}
|
||||||
@@ -447,8 +819,8 @@ function proceedToExpress() {
|
|||||||
border: 1.5px solid #86efac;
|
border: 1.5px solid #86efac;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 7.5px;
|
font-size: 7px;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,35 +84,53 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
|||||||
pue: {
|
pue: {
|
||||||
id: 'pue',
|
id: 'pue',
|
||||||
name: 'Pü (Jakob)',
|
name: 'Pü (Jakob)',
|
||||||
title: 'Der Lichtler des Hofes',
|
title: 'Lichtmeister & Kristall-Rave-Magier',
|
||||||
portrait: '/assets/characters/pue/portrait.jpg',
|
portrait: '/assets/characters/pue/portrait.jpg',
|
||||||
description: 'Früherer Club-Lichtmeister, bedient nun die magischen Stroboskop-Fackeln.',
|
description: 'Lässt im geheimen Hinterzimmer die Disco-Kristalle tanzen und erhellt die Entourage mit bunten Lasern.',
|
||||||
famousQuote: 'Majestät, wann zünden wir die Stroboskop-Fackeln zum Drop?',
|
famousQuote: 'Bass auf Anschlag, Kristalle auf Stroboskop! Willkommen im geheimen Hinterzimmer!',
|
||||||
themeColor: '#eab308'
|
themeColor: '#eab308'
|
||||||
},
|
},
|
||||||
mario: {
|
mario: {
|
||||||
id: 'mario',
|
id: 'mario',
|
||||||
name: 'Mario der Gaukler',
|
name: 'Mario',
|
||||||
title: 'Hof-Jongleur & Stimmungs-Garant',
|
title: 'Hof-Gaukler & Kettensägen-Virtuose',
|
||||||
portrait: '/assets/characters/mario/portrait.jpg',
|
portrait: '/assets/characters/mario/portrait.jpg',
|
||||||
description: 'Jongliert geschickt mit Hopfen, Hefe und Bierkrügen.',
|
description: 'Jongliert furchtlos auf dem Marktplatz mit kreischenden Kettensägen und Hopfenkrügen.',
|
||||||
famousQuote: 'Hoppla die Waldfee! Ein Krug für den König!',
|
famousQuote: 'Pass auf die Zinken auf! Kettensägen-Akrobatik für König Sascha!',
|
||||||
themeColor: '#14b8a6'
|
themeColor: '#14b8a6'
|
||||||
},
|
},
|
||||||
mane: {
|
mane: {
|
||||||
id: 'mane',
|
id: 'mane',
|
||||||
name: 'Mane (Ramona)',
|
name: 'Mane (Ramona)',
|
||||||
title: 'Die königliche Wanderin',
|
title: 'Königliche Alpen-Wanderin',
|
||||||
portrait: '/assets/characters/mane/portrait.jpg',
|
portrait: '/assets/characters/mane/portrait.jpg',
|
||||||
description: 'Wandert random mit Rucksack und Wanderstock quer durch alle Bildschirme.',
|
description: 'Zieht mit Wanderstock, Sonnenbrille und Flechtzopf durchs alpine Hinterland von Haumdaucher.',
|
||||||
famousQuote: 'Schee ists hier! Noch 3 Kilometer bis zum Gipfel-Bier!',
|
famousQuote: 'Schee ists hier! Noch 3 Kilometer über den Grat bis zum Gipfel-Bier!',
|
||||||
themeColor: '#84cc16'
|
themeColor: '#84cc16'
|
||||||
},
|
},
|
||||||
|
flo_maier: {
|
||||||
|
id: 'flo_maier',
|
||||||
|
name: 'Flo Maier',
|
||||||
|
title: 'Hof-DJ & Sound-Ingenieur',
|
||||||
|
portrait: '/assets/characters/flo_maier/portrait.jpg',
|
||||||
|
description: 'Mastermind an den Turntables und Behüter der heiligen 135-BPM-Frequenz.',
|
||||||
|
famousQuote: 'Dieser Track drückt mehr als die königliche Kutsche!',
|
||||||
|
themeColor: '#0284c7'
|
||||||
|
},
|
||||||
|
viktor: {
|
||||||
|
id: 'viktor',
|
||||||
|
name: 'Viktor',
|
||||||
|
title: 'Nachtwächter & Party-Ritter',
|
||||||
|
portrait: '/assets/characters/viktor/portrait.jpg',
|
||||||
|
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und sorgt für eiskaltes Helles.',
|
||||||
|
famousQuote: 'Die Nacht ist jung und der Gerstensaft fließt in Strömen!',
|
||||||
|
themeColor: '#b91c1c'
|
||||||
|
},
|
||||||
djs: {
|
djs: {
|
||||||
id: 'djs',
|
id: 'djs',
|
||||||
name: 'Die Hof-DJs',
|
name: 'Die Hof-DJs',
|
||||||
title: 'Minnesänger der 135 BPM',
|
title: 'Minnesänger der 135 BPM',
|
||||||
portrait: '/assets/characters/pue/portrait.jpg',
|
portrait: '/assets/characters/flo_maier/portrait.jpg',
|
||||||
description: 'Stehen an der königlichen Kanzel am doppelten Eichenholz-Plattenteller.',
|
description: 'Stehen an der königlichen Kanzel am doppelten Eichenholz-Plattenteller.',
|
||||||
famousQuote: 'Drop it like it\'s 1426! Mehr Bass auf die Burgzinnen!',
|
famousQuote: 'Drop it like it\'s 1426! Mehr Bass auf die Burgzinnen!',
|
||||||
themeColor: '#06b6d4'
|
themeColor: '#06b6d4'
|
||||||
@@ -121,8 +139,8 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
|||||||
id: 'gefolge',
|
id: 'gefolge',
|
||||||
name: 'Das Haumdaucher Gefolge',
|
name: 'Das Haumdaucher Gefolge',
|
||||||
title: 'Die feiernde Reichs-Crowd',
|
title: 'Die feiernde Reichs-Crowd',
|
||||||
portrait: '/assets/characters/hansi/portrait.jpg',
|
portrait: '/assets/characters/viktor/portrait.jpg',
|
||||||
description: 'Viktor der Vampir, der bärtige Guatemalteke, Fabian, Albin, Maier Flo, Markus & Nathalie.',
|
description: 'Viktor der Vampir-Ritter, DJ Flo Maier, Hansi, Fabian, Albin, der bärtige Guatemalteke, Markus & Nathalie.',
|
||||||
famousQuote: 'Ein Hoch auf König Sascha! Hoch soll er leben!',
|
famousQuote: 'Ein Hoch auf König Sascha! Hoch soll er leben!',
|
||||||
themeColor: '#e11d48'
|
themeColor: '#e11d48'
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 8.1 MiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 13 MiB |
|
After Width: | Height: | Size: 78 KiB |