feat(sasch40): polish retro game layout, clean tavern scene, and improve mobile HUD

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