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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user