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