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
@@ -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 {