feat(sasch40): multi-room tavern, authentic character portraits, pre-level start screens & bollerwagen redesign

This commit is contained in:
Moritz Graf
2026-09-24 10:02:57 +02:00
parent 4488749d6f
commit 7a7038e2c2
26 changed files with 1243 additions and 334 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 KiB

After

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 KiB

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 679 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 972 KiB

+156 -99
View File
@@ -9,7 +9,7 @@ if (!fs.existsSync(SCREENSHOTS_DIR)) {
}
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 errors = [];
@@ -60,165 +60,206 @@ async function runPlaytest() {
try {
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
console.log('📸 Capturing Intro Screen...');
// 1. INTRO / PREQUEL SCREEN
console.log('📸 01: Capturing Intro Screen...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '01_intro_screen.png') });
// Inspect layout and controls presence in Intro
const introControlsVisible = await page.evaluate(() => {
const dpad = document.querySelector('.dpad');
const intro = document.querySelector('.intro-screen');
const chassis = document.querySelector('.gameboy-chassis');
return {
hasDpad: !!dpad,
dpadOffsetHeight: dpad ? (dpad.offsetHeight > 0) : false,
hasIntro: !!intro,
layoutClass: chassis ? chassis.className : ''
};
});
console.log('Intro Layout Check:', introControlsVisible);
// Click Start Button
console.log('🖱️ Clicking Start Button...');
await page.click('.start-btn');
await new Promise(r => setTimeout(r, 1000));
// 2. PROLOGUE SCREEN
console.log('📸 Capturing Prologue Screen...');
// 2. PROLOGUE SCREEN (Balcony Scene)
console.log('📸 02: Capturing Prologue Screen (Verifying banner/portrait alignment)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
// Advance Prologue Dialogues
console.log('💬 Advancing Prologue Dialogues (clicking choice buttons)...');
// Click choice 1 on 'start'
await page.waitForSelector('.choice-btn', { timeout: 5000 });
const firstChoice = await page.$('.choice-btn');
if (firstChoice) {
await firstChoice.click();
await new Promise(r => setTimeout(r, 800));
console.log('📸 Capturing Mop Appears Prologue Dialogue...');
console.log('📸 02b: Capturing Prologue Mop Dialog...');
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');
if (mopChoice) {
await mopChoice.click();
await new Promise(r => setTimeout(r, 1200));
await new Promise(r => setTimeout(r, 1000));
}
// 3. PLATFORMER SCREEN
console.log('📸 Capturing Platformer (Jump & Run)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_screen.png') });
// 3. PLATFORMER PRE-LEVEL START SCREEN (Act II)
console.log('📸 03: Capturing Platformer Start Screen Modal...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_start_modal.png') });
const platformerControls = await page.evaluate(() => {
const dpad = document.querySelector('.dpad');
const btnA = document.querySelector('.btn-a');
const canvas = document.querySelector('canvas');
return {
hasDpad: !!dpad,
hasBtnA: !!btnA,
hasCanvas: !!canvas,
canvasW: canvas?.width,
canvasH: canvas?.height
};
});
console.log('Platformer Controls Check:', platformerControls);
// Click Start Act Button on the modal
const startPlatformerBtn = await page.$('.start-act-btn');
if (startPlatformerBtn) {
await startPlatformerBtn.click();
await new Promise(r => setTimeout(r, 800));
}
// Test Jump & Move
console.log('🎮 Testing Jump & Move Controls in Platformer...');
// 4. PLATFORMER GAMEPLAY
console.log('📸 04: Capturing Platformer Gameplay...');
const btnA = await page.$('.btn-a');
if (btnA) {
await btnA.click();
await new Promise(r => setTimeout(r, 400));
await btnA.click();
await new Promise(r => setTimeout(r, 400));
}
const dpadRight = await page.$('.dpad-right');
if (dpadRight) {
await dpadRight.hover();
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03b_platformer_jump.png') });
await new Promise(r => setTimeout(r, 300));
}
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_platformer_gameplay.png') });
// Directly set act to 'tavern' to test Tavern Point & Click
console.log('🍺 Advancing to Tavern Act...');
// 5. TAVERN: SCHANKRAUM (Gaststube)
console.log('🍺 Advancing to Tavern Act (Schankraum)...');
await page.evaluate(() => {
const chassis = document.querySelector('.gameboy-chassis');
// access pinia store from window if exposed, or trigger level finish
// We can click the level finish or evaluate store change
window.dispatchEvent(new CustomEvent('test-set-act', { detail: 'tavern' }));
});
// Check if act switched, or use page.evaluate to trigger store change
await page.evaluate(() => {
// Find Pinia root if possible
const app = document.querySelector('#app')?.__vue_app__;
if (app && app.config?.globalProperties?.$pinia) {
const pinia = app.config.globalProperties.$pinia;
// find game store
for (const [id, store] of pinia._s.entries()) {
if (id === 'game') {
store.setAct('tavern');
}
if (id === 'game') store.setAct('tavern');
}
}
});
await new Promise(r => setTimeout(r, 1000));
console.log('📸 Capturing Tavern Screen...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_tavern_screen.png') });
console.log('📸 05: Capturing Tavern Gaststube (Schankraum)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_schankraum.png') });
// Test Tavern Hotspots
console.log('👥 Testing Verena Hotspot...');
// Test Verena Hotspot
console.log('👥 Testing Verena Dialog...');
const verenaBtn = await page.$('.hotspot-verena');
if (verenaBtn) {
await verenaBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_verena_dialog.png') });
await new Promise(r => setTimeout(r, 700));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_verena_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
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');
if (armendBtn) {
await armendBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_armend_dialog.png') });
await new Promise(r => setTimeout(r, 700));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_armend_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
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');
if (mirrorBtn) {
await mirrorBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_zuluman_mirror.png') });
await new Promise(r => setTimeout(r, 700));
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');
if (closeBtn) await closeBtn.click();
await new Promise(r => setTimeout(r, 500));
await new Promise(r => setTimeout(r, 400));
}
console.log('⚔️ Testing Gstanzl-Duel...');
const duelBtn = await page.$('.duel-trigger');
if (duelBtn) {
await duelBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_gstanzl_duel.png') });
// Answer Gstanzl question
const correctOption = await page.$('.option-btn');
if (correctOption) {
await correctOption.click();
await new Promise(r => setTimeout(r, 600));
await new Promise(r => setTimeout(r, 700));
console.log('📸 15: Capturing Gstanzl Duel...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15_tavern_gstanzl_duel.png') });
const firstAnswer = await page.$('.choice-btn');
if (firstAnswer) {
await firstAnswer.click();
await new Promise(r => setTimeout(r, 500));
}
// 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)...');
await page.evaluate(() => {
const app = document.querySelector('#app')?.__vue_app__;
@@ -230,10 +271,26 @@ async function runPlaytest() {
}
});
await new Promise(r => setTimeout(r, 1000));
console.log('📸 Capturing Party Express...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_party_express.png') });
console.log('📸 16: Capturing Party Express Start Screen Modal...');
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)...');
await page.evaluate(() => {
const app = document.querySelector('#app')?.__vue_app__;
@@ -241,7 +298,7 @@ async function runPlaytest() {
const pinia = app.config.globalProperties.$pinia;
for (const [id, store] of pinia._s.entries()) {
if (id === 'game') {
store.addKronkorken(25);
store.addKronkorken(35);
store.serveGuest();
store.serveGuest();
store.setAct('finale');
@@ -250,10 +307,10 @@ async function runPlaytest() {
}
});
await new Promise(r => setTimeout(r, 1500));
console.log('📸 Capturing Certificate Modal...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_certificate_screen.png') });
console.log('📸 18: Capturing Certificate Modal...');
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 imgs = Array.from(document.querySelectorAll('img'));
return imgs
@@ -265,7 +322,7 @@ async function runPlaytest() {
console.log('Total Console Logs:', logs.length);
console.log('Console Errors:', errors);
console.log('Failed Requests:', failedRequests);
console.log('Broken Images on Certificate Page:', brokenImages);
console.log('Broken Images:', brokenImages);
const report = {
timestamp: new Date().toISOString(),
@@ -43,9 +43,9 @@ function toggleInventory() {
<span></span><span></span><span></span>
</div>
<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">
{{ 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>
</div>
<button class="sound-toggle" @click="toggleAudio">
@@ -204,20 +204,23 @@ function toggleInventory() {
.chassis-brand {
display: flex;
align-items: center;
gap: 6px;
gap: 4px;
overflow: hidden;
}
.brand-text {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
font-size: 6.8px;
color: #ffd700;
letter-spacing: 0.5px;
letter-spacing: 0.2px;
white-space: nowrap;
}
.brand-act {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
font-size: 6px;
color: #f59e0b;
white-space: nowrap;
}
.speaker-grill-left {
@@ -5,13 +5,19 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const showExpressIntro = ref(true)
let animId: number | null = null
let wagonX = 60
let wagonY = 330
let speed = 3.5
let wagonX = 70
let wagonY = 325
let speed = 3.2
let distance = 0
const maxDistance = 2000
const maxDistance = 2100
function startExpress() {
sound.playDialogueBlip()
showExpressIntro.value = false
}
interface Guest {
id: string
@@ -24,18 +30,18 @@ interface Guest {
let guests: Guest[] = [
{ id: 'hansi', x: 300, name: 'Hansi 🧔', served: false, avatarColor: '#6366f1' },
{ id: 'viktor', x: 550, name: 'Viktor (Vampir) 🧛', served: false, avatarColor: '#881337' },
{ id: 'albin', x: 800, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
{ id: 'fabian', x: 1050, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
{ id: 'maierflo', x: 1300, name: 'Maier Flo (Ton) 🎧', served: false, avatarColor: '#7c3aed' },
{ id: 'guate', x: 1550, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
{ id: 'markus_nat', x: 1800, name: 'Markus & Nathalie 🥂', served: false, avatarColor: '#db2777' }
{ id: 'albin', x: 820, name: 'Albin (Fitness) 🏋️', served: false, avatarColor: '#0284c7' },
{ id: 'fabian', x: 1100, name: 'Fabian (Brauer) 🍺', served: false, avatarColor: '#d97706' },
{ id: 'maierflo', x: 1380, name: 'Maier Flo (DJ) 🎧', served: false, avatarColor: '#7c3aed' },
{ id: 'guate', x: 1650, name: 'Guatemalteke 🌋', served: false, avatarColor: '#059669' },
{ id: 'markus_nat', x: 1920, name: 'Markus & Nathalie 🥂', served: false, avatarColor: '#db2777' }
]
function throwBeer() {
sound.playGulp()
// Check if any guest is near
// Check if any guest is near the wagon
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
store.serveGuest()
}
@@ -43,6 +49,8 @@ function throwBeer() {
}
function updateGame() {
if (showExpressIntro.value) return // Don't advance while reading intro!
distance += speed
if (distance >= maxDistance) {
@@ -54,43 +62,68 @@ function updateGame() {
function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
ctx.clearRect(0, 0, w, h)
// Sunset Party Sky
// Sunset Party Sky Gradient
const grad = ctx.createLinearGradient(0, 0, 0, h)
grad.addColorStop(0, '#31103f')
grad.addColorStop(0.5, '#7a2048')
grad.addColorStop(0.8, '#c2410c')
grad.addColorStop(0, '#1e082b')
grad.addColorStop(0.4, '#4a154b')
grad.addColorStop(0.7, '#831843')
grad.addColorStop(0.9, '#c2410c')
grad.addColorStop(1, '#f59e0b')
ctx.fillStyle = grad
ctx.fillRect(0, 0, w, h)
// Distant party silhouettes with laser beams
// Sweeping Festival Lasers & Party Beams
const now = Date.now() * 0.003
ctx.strokeStyle = `hsl(${(now * 60) % 360}, 100%, 75%)`
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(w / 2, 100)
ctx.lineTo(w * ((Math.sin(now) + 1) / 2), 350)
ctx.moveTo(w / 2, 100)
ctx.lineTo(w * ((Math.cos(now) + 1) / 2), 350)
ctx.stroke()
// Cobblestone road
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.save()
for (let b = 0; b < 3; b++) {
const angleOffset = b * (Math.PI / 3)
const targetX = w * 0.5 + Math.sin(now + angleOffset) * (w * 0.45)
ctx.strokeStyle = `hsla(${((now * 50) + b * 90) % 360}, 100%, 75%, 0.45)`
ctx.lineWidth = 3
ctx.beginPath()
ctx.moveTo(w * 0.5, 90)
ctx.lineTo(targetX, 350)
ctx.stroke()
}
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
guests.forEach(g => {
const screenX = g.x - distance
if (screenX > -60 && screenX < w + 60) {
// Custom sprite details per guest
if (screenX > -70 && screenX < w + 70) {
// 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') {
// Vampire cape
// Red velvet vampire cape
ctx.fillStyle = '#4c0519'
ctx.fillRect(screenX - 4, 305, 28, 42)
ctx.fillRect(screenX - 5, 305, 30, 44)
}
// Guest body
@@ -107,12 +140,11 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
if (g.id === 'hansi') {
// Moustache
ctx.fillStyle = '#451a03'
ctx.fillRect(screenX + 6, 296, 8, 2.5)
ctx.fillRect(screenX + 6, 297, 8, 2.5)
} else if (g.id === 'viktor') {
// Fangs
ctx.fillStyle = '#ffffff'
ctx.fillRect(screenX + 8, 297, 2, 3)
ctx.fillRect(screenX + 12, 297, 2, 3)
// Sunglasses & fangs
ctx.fillStyle = '#0f172a'
ctx.fillRect(screenX + 6, 293, 8, 3)
} else if (g.id === 'albin') {
// Gym Headband
ctx.fillStyle = '#38bdf8'
@@ -159,98 +191,209 @@ function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
ctx.fillText('🍻 PROST!', screenX + 10, 264)
} else {
ctx.fillStyle = '#fbbf24'
ctx.font = '9px "Press Start 2P", monospace'
ctx.font = '8px "Press Start 2P", monospace'
ctx.fillText('DURST!', screenX + 10, 264)
}
}
})
// Draw Royal Party Wagon (Bollerwagen)
// ================= REDESIGNED BOLLERWAGEN (RETRO PIXEL ART) =================
const wx = wagonX
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
ctx.fillStyle = '#78350f'
ctx.fillRect(wx - 20, wy + bob, 95, 24)
// 1. Shadow underneath the wagon
ctx.fillStyle = 'rgba(0,0,0,0.5)'
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.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
const wheelSpin = (distance * 0.1) % (Math.PI * 2)
;[-6, 60].forEach(wheelOffset => {
// 4. Giant 135-BPM Bass Subwoofer on the rear
ctx.fillStyle = '#09090b'
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.translate(wx + wheelOffset, wy + 24)
ctx.translate(wx + wheelOffset, wy + 26)
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.arc(0, 0, 11, 0, Math.PI * 2)
ctx.fill()
// Brass rim edge
ctx.strokeStyle = '#ffd700'
ctx.lineWidth = 2.5
ctx.lineWidth = 1.5
ctx.stroke()
// Spokes
// 8 Spokes
ctx.strokeStyle = '#b45309'
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.moveTo(-11, 0); ctx.lineTo(11, 0)
ctx.moveTo(0, -11); ctx.lineTo(0, 11)
ctx.stroke()
ctx.arc(0, 0, 4, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
})
// Sascha standing on wagon with crown, sunglasses & beer mug
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
// Top Banner HUD
ctx.fillStyle = '#ffd700'
ctx.font = 'bold 8.5px "Press Start 2P", monospace'
ctx.font = 'bold 8px "Press Start 2P", monospace'
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
const progress = Math.min(1, distance / maxDistance)
const barMargin = 24
const barMargin = 20
const barWidth = w - barMargin * 2
ctx.fillStyle = 'rgba(0,0,0,0.6)'
ctx.fillRect(barMargin, 34, barWidth, 8)
ctx.fillStyle = 'rgba(0,0,0,0.7)'
ctx.fillRect(barMargin, 32, barWidth, 10)
ctx.strokeStyle = '#ffd700'
ctx.lineWidth = 1
ctx.strokeRect(barMargin, 32, barWidth, 10)
// Progress fill
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() {
@@ -281,6 +424,38 @@ onUnmounted(() => {
</button>
<span class="express-hint">Tippe, wenn du an deinen Freunden vorbeifährst!</span>
</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>
</template>
@@ -320,7 +495,7 @@ onUnmounted(() => {
border: 2px solid #fff;
border-radius: 8px;
font-family: 'Press Start 2P', monospace;
font-size: 9px;
font-size: 8.5px;
padding: 10px 14px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
@@ -329,7 +504,7 @@ onUnmounted(() => {
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
50% { transform: scale(1.04); }
}
.express-hint {
@@ -338,4 +513,125 @@ onUnmounted(() => {
color: #fef08a;
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>
@@ -5,8 +5,14 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const showLevelIntro = ref(true)
let animationFrameId: number | null = null
function startLevel() {
sound.playDialogueBlip()
showLevelIntro.value = false
}
// Game state
interface Platform {
x: number
@@ -428,7 +434,9 @@ function drawGame(ctx: CanvasRenderingContext2D, width: number, height: number)
}
function gameLoop() {
updatePhysics()
if (!showLevelIntro.value) {
updatePhysics()
}
if (canvasRef.value) {
const ctx = canvasRef.value.getContext('2d')
if (ctx) {
@@ -481,10 +489,42 @@ onUnmounted(() => {
<div class="platformer-hud-top">
<div class="hud-korken-badge">👑 {{ store.kronkorken }} KORKEN</div>
<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>
</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>
</template>
@@ -550,4 +590,124 @@ onUnmounted(() => {
color: #fef08a;
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>
@@ -36,6 +36,9 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
<!-- Balcony Visual Scene with Regal Portrait Frame -->
<div class="scene-visual">
<div class="balcony-backdrop"></div>
<div class="scene-tag">
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
</div>
<div class="speaker-frame">
<img
:src="currentSpeaker().portrait"
@@ -43,9 +46,6 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
class="speaker-visual"
/>
</div>
<div class="scene-tag">
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
</div>
</div>
<!-- Retro Dialogue Card -->
@@ -88,13 +88,15 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
.scene-visual {
position: relative;
flex: 1;
min-height: 160px;
max-height: 200px;
flex: 0 0 auto;
min-height: 175px;
padding: 10px 8px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
background: radial-gradient(circle at 50% 40%, #4a2818 0%, #1a0f0a 100%);
border-bottom: 2px solid #d4af37;
}
@@ -109,10 +111,27 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
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 {
position: relative;
width: 140px;
height: 140px;
width: 116px;
height: 116px;
border-radius: 12px;
border: 3px double #ffd700;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.8), 0 0 10px rgba(212, 175, 55, 0.4);
@@ -129,22 +148,6 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
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 {
flex: 1;
background: #26160e;
@@ -7,6 +7,9 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
type TavernRoom = 'schankraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
const currentRoom = ref<TavernRoom>('schankraum')
interface ActiveDialog {
characterId: string
name: string
@@ -23,6 +26,17 @@ const duelStep = ref(0)
const duelWon = 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) {
const char = CHARACTERS[charId]
if (!char) return
@@ -81,42 +95,16 @@ function openCharacterDialog(charId: string) {
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') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Hoppla die Waldfee! Schau wie die Bierkrüge und Hopfendolden durch die Luft wirbeln! Ein Hoch auf König Sascha!',
actionLabel: 'Sauber jongliert, Mario! (+3 Kronkorken)',
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! (+4 Kronkorken)',
actionFn: () => {
store.addKronkorken(3)
store.addKronkorken(4)
activeDialog.value = null
}
}
@@ -134,6 +122,72 @@ function openCharacterDialog(charId: string) {
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>
<div class="tavern-container">
<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 -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
<span class="hotspot-badge">Verena 👑</span>
</button>
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
<template v-if="currentRoom === 'schankraum'">
<img src="/assets/scenes/tavern.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
<span class="hotspot-badge">Armend 🔨</span>
</button>
<!-- Verena on the wooden gallery -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
<span class="hotspot-badge">Verena 👑</span>
</button>
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
</button>
<!-- Armend at the forge/anvil -->
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
<span class="hotspot-badge">Armend 🔨</span>
</button>
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
<span class="hotspot-badge">Amy 🎨</span>
</button>
<!-- Lutz at the fireplace -->
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
</button>
<button class="hotspot hotspot-mirror" @click="inspectMirror">
<span class="mirror-icon">🪞</span>
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
</button>
<!-- Amy at the tavern table -->
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
<span class="hotspot-badge">Amy 🎨</span>
</button>
<button class="hotspot hotspot-pue" @click="openCharacterDialog('pue')">
<img :src="CHARACTERS.pue.portrait" class="hotspot-thumb" alt="Pü" />
<span class="hotspot-badge">Pü (Licht) 💡</span>
</button>
<!-- Zuluman Magic Mirror on the pillar -->
<button class="hotspot hotspot-mirror" @click="inspectMirror">
<span class="mirror-icon">🪞</span>
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
</button>
<button class="hotspot hotspot-mane" @click="openCharacterDialog('mane')">
<img :src="CHARACTERS.mane.portrait" class="hotspot-thumb" alt="Mane" />
<span class="hotspot-badge">Mane 🥾</span>
</button>
<!-- Doorway to Marktplatz -->
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
<span class="door-icon">🚪</span>
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
</button>
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
<span class="hotspot-badge">NAT 🐗</span>
</button>
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer -->
<button class="hotspot secret-tap-hotspot" @click="enterSecretHinterzimmer" title="Ein sonderbarer goldener Zapfhahn...">
<span class="secret-tap-icon pulse">🗝️</span>
<span class="secret-badge">Geheimer Zapfhahn</span>
</button>
</template>
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
<span class="hotspot-badge">Mario 🤹</span>
</button>
<!-- ================= 2. MARKTPLATZ ================= -->
<template v-else-if="currentRoom === 'marktplatz'">
<img src="/assets/scenes/marktplatz.jpg" alt="Marktplatz Haumdaucher" class="tavern-bg" />
<!-- 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 -->
<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">
<button class="duel-trigger" @click="startGstanzlDuel">
⚔️ Gstanzl-Fechten
@@ -338,7 +527,7 @@ function proceedToExpress() {
display: flex;
align-items: center;
gap: 4px;
background: rgba(26, 15, 10, 0.85);
background: rgba(26, 15, 10, 0.88);
border: 1.5px solid #ffd700;
border-radius: 16px;
padding: 2px 6px 2px 2px;
@@ -363,8 +552,8 @@ function proceedToExpress() {
object-fit: cover;
}
.mirror-icon {
font-size: 16px;
.mirror-icon, .door-icon {
font-size: 15px;
padding: 0 2px;
}
@@ -375,15 +564,164 @@ function proceedToExpress() {
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-lutz { top: 38%; left: 20%; }
.hotspot-amy { top: 52%; left: 80%; }
.hotspot-mirror { top: 32%; left: 63%; }
.hotspot-amy { top: 52%; left: 82%; }
.hotspot-pue { top: 20%; left: 35%; }
.hotspot-mane { top: 45%; left: 66%; }
.hotspot-nat { top: 63%; left: 85%; }
.hotspot-mario { top: 36%; left: 90%; }
.hotspot-door-marktplatz { top: 56%; left: 90%; }
/* 2. Marktplatz Hotspots */
.mario-container {
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 {
animation: pulseAnim 1.5s infinite;
@@ -394,12 +732,13 @@ function proceedToExpress() {
50% { box-shadow: 0 0 12px #ec4899; }
}
/* Bottom Navigation */
.scene-bottom-nav {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 84px;
height: 94px;
background: linear-gradient(180deg, #451a03 0%, #26140b 100%);
border-top: 3px solid #b45309;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.95);
@@ -407,20 +746,53 @@ function proceedToExpress() {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 12px;
gap: 5px;
padding: 6px 10px;
z-index: 15;
}
.tavern-bar-title {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
font-size: 6.5px;
color: #fef08a;
text-shadow: 1px 1px 0 #000;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
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 {
display: flex;
width: 100%;
@@ -435,8 +807,8 @@ function proceedToExpress() {
border: 1.5px solid #ffd700;
border-radius: 6px;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 8px 10px;
font-size: 7px;
padding: 6px 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
@@ -447,8 +819,8 @@ function proceedToExpress() {
border: 1.5px solid #86efac;
border-radius: 6px;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 8px 10px;
font-size: 7px;
padding: 6px 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
@@ -84,35 +84,53 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
pue: {
id: 'pue',
name: 'Pü (Jakob)',
title: 'Der Lichtler des Hofes',
title: 'Lichtmeister & Kristall-Rave-Magier',
portrait: '/assets/characters/pue/portrait.jpg',
description: 'Früherer Club-Lichtmeister, bedient nun die magischen Stroboskop-Fackeln.',
famousQuote: 'Majestät, wann zünden wir die Stroboskop-Fackeln zum Drop?',
description: 'Lässt im geheimen Hinterzimmer die Disco-Kristalle tanzen und erhellt die Entourage mit bunten Lasern.',
famousQuote: 'Bass auf Anschlag, Kristalle auf Stroboskop! Willkommen im geheimen Hinterzimmer!',
themeColor: '#eab308'
},
mario: {
id: 'mario',
name: 'Mario der Gaukler',
title: 'Hof-Jongleur & Stimmungs-Garant',
name: 'Mario',
title: 'Hof-Gaukler & Kettensägen-Virtuose',
portrait: '/assets/characters/mario/portrait.jpg',
description: 'Jongliert geschickt mit Hopfen, Hefe und Bierkrügen.',
famousQuote: 'Hoppla die Waldfee! Ein Krug für den König!',
description: 'Jongliert furchtlos auf dem Marktplatz mit kreischenden Kettensägen und Hopfenkrügen.',
famousQuote: 'Pass auf die Zinken auf! Kettensägen-Akrobatik für König Sascha!',
themeColor: '#14b8a6'
},
mane: {
id: 'mane',
name: 'Mane (Ramona)',
title: 'Die königliche Wanderin',
title: 'Königliche Alpen-Wanderin',
portrait: '/assets/characters/mane/portrait.jpg',
description: 'Wandert random mit Rucksack und Wanderstock quer durch alle Bildschirme.',
famousQuote: 'Schee ists hier! Noch 3 Kilometer bis zum Gipfel-Bier!',
description: 'Zieht mit Wanderstock, Sonnenbrille und Flechtzopf durchs alpine Hinterland von Haumdaucher.',
famousQuote: 'Schee ists hier! Noch 3 Kilometer über den Grat bis zum Gipfel-Bier!',
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: {
id: 'djs',
name: 'Die Hof-DJs',
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.',
famousQuote: 'Drop it like it\'s 1426! Mehr Bass auf die Burgzinnen!',
themeColor: '#06b6d4'
@@ -121,8 +139,8 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
id: 'gefolge',
name: 'Das Haumdaucher Gefolge',
title: 'Die feiernde Reichs-Crowd',
portrait: '/assets/characters/hansi/portrait.jpg',
description: 'Viktor der Vampir, der bärtige Guatemalteke, Fabian, Albin, Maier Flo, Markus & Nathalie.',
portrait: '/assets/characters/viktor/portrait.jpg',
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!',
themeColor: '#e11d48'
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB