feat(sasch40): add retro birthday game subproject and k8s deployment
- Add Vue 3 + Vite + TypeScript retro game: 'Saschas Kronkorken Kreuzzug: 40 Jahre Bass & Gerstensaft' - Add dynamic GameBoy layout: controls visible only during arcade modes, fullscreen during dialogue and tavern - Add QR code welcome & prequel screen with lore, feature preview, and audio hints - Add competitive party highscore and ranking breakdown on royal certificate - Add dedicated individual 1:1 character portraits for Sascha, Verena, NAT, Hansi, Mop, Lutz, Armend, Amy, Mane, Pü, and Mario - Add Dockerfile, Makefile, and K8s deployment at sasch40.haumdaucher.de - Update openclaw timeout configuration
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { sound } from '../audio/synthAudio'
|
||||
|
||||
const store = useGameStore()
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
let animId: number | null = null
|
||||
|
||||
let wagonX = 60
|
||||
let wagonY = 220
|
||||
let speed = 3.5
|
||||
let distance = 0
|
||||
const maxDistance = 2000
|
||||
|
||||
interface Guest {
|
||||
x: number
|
||||
name: string
|
||||
served: boolean
|
||||
avatarColor: string
|
||||
}
|
||||
|
||||
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' }
|
||||
]
|
||||
|
||||
function throwBeer() {
|
||||
sound.playGulp()
|
||||
// Check if any guest is near
|
||||
guests.forEach(g => {
|
||||
if (!g.served && Math.abs(distance + wagonX - g.x) < 80) {
|
||||
g.served = true
|
||||
store.addBeer()
|
||||
store.addKronkorken(3)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateGame() {
|
||||
distance += speed
|
||||
|
||||
if (distance >= maxDistance) {
|
||||
store.setAct('finale')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function draw(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
|
||||
// Sunset Party Sky
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
||||
grad.addColorStop(0, '#4a154b')
|
||||
grad.addColorStop(0.5, '#993355')
|
||||
grad.addColorStop(1, '#ffaa44')
|
||||
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.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.stroke()
|
||||
|
||||
// Cobblestone road
|
||||
ctx.fillStyle = '#292524'
|
||||
ctx.fillRect(0, 240, w, 80)
|
||||
ctx.fillStyle = '#44403c'
|
||||
for (let r = 0; r < w; r += 30) {
|
||||
ctx.fillRect((r - (distance % 30)), 240, 26, 4)
|
||||
}
|
||||
|
||||
// Draw Guests along the road
|
||||
guests.forEach(g => {
|
||||
const screenX = g.x - distance
|
||||
if (screenX > -50 && screenX < w + 50) {
|
||||
// Guest body
|
||||
ctx.fillStyle = g.avatarColor
|
||||
ctx.fillRect(screenX, 195, 20, 45)
|
||||
// Head
|
||||
ctx.fillStyle = '#fed7aa'
|
||||
ctx.beginPath()
|
||||
ctx.arc(screenX + 10, 185, 10, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// Name & icon tag
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.font = 'bold 9px MedievalSharp, serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(g.name, screenX + 10, 168)
|
||||
|
||||
if (g.served) {
|
||||
ctx.fillStyle = '#22c55e'
|
||||
ctx.font = 'bold 12px sans-serif'
|
||||
ctx.fillText('🍻 PROST!', screenX + 10, 154)
|
||||
} else {
|
||||
ctx.fillStyle = '#fbbf24'
|
||||
ctx.font = '10px monospace'
|
||||
ctx.fillText('DURST!', screenX + 10, 154)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Draw Royal Party Wagon (Bollerwagen)
|
||||
const wx = wagonX
|
||||
const wy = wagonY
|
||||
const bob = Math.sin(Date.now() * 0.015) * 2
|
||||
|
||||
// Wagon wooden bed
|
||||
ctx.fillStyle = '#78350f'
|
||||
ctx.fillRect(wx - 20, wy + bob, 90, 24)
|
||||
ctx.fillStyle = '#b45309'
|
||||
ctx.fillRect(wx - 18, wy + bob + 3, 86, 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()
|
||||
|
||||
// Sascha standing on wagon with crown, sunglasses & beer mug
|
||||
ctx.fillStyle = '#dc2626' // Cape
|
||||
ctx.fillRect(wx + 10, wy - 30 + bob, 22, 34)
|
||||
ctx.fillStyle = '#1d4ed8' // Tunic
|
||||
ctx.fillRect(wx + 14, wy - 28 + bob, 16, 22)
|
||||
ctx.fillStyle = '#fcd34d' // Face
|
||||
ctx.fillRect(wx + 15, 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)
|
||||
// Crown
|
||||
ctx.fillStyle = '#facc15'
|
||||
ctx.fillRect(wx + 14, 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)
|
||||
ctx.fill()
|
||||
|
||||
// Hof-DJs Banner
|
||||
ctx.fillStyle = '#ffd700'
|
||||
ctx.font = 'bold 9px "Press Start 2P", monospace'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText('135 BPM EXPRESS', 10, 25)
|
||||
|
||||
// Progress Bar
|
||||
const progress = Math.min(1, distance / maxDistance)
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.5)'
|
||||
ctx.fillRect(10, 35, w - 20, 8)
|
||||
ctx.fillStyle = '#22c55e'
|
||||
ctx.fillRect(10, 35, (w - 20) * progress, 8)
|
||||
}
|
||||
|
||||
function loop() {
|
||||
updateGame()
|
||||
if (canvasRef.value) {
|
||||
const ctx = canvasRef.value.getContext('2d')
|
||||
if (ctx) draw(ctx, canvasRef.value.width, canvasRef.value.height)
|
||||
}
|
||||
animId = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
animId = requestAnimationFrame(loop)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animId) cancelAnimationFrame(animId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="express-container">
|
||||
<canvas ref="canvasRef" width="360" height="320" class="game-canvas"></canvas>
|
||||
|
||||
<div class="express-controls">
|
||||
<button class="throw-beer-btn" @click="throwBeer">
|
||||
🍺 BIER AN DIE GÄSTE SCHENKEN!
|
||||
</button>
|
||||
<span class="express-hint">Tippe, wenn du an deinen Freunden vorbeifährst!</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.express-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.express-controls {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.throw-beer-btn {
|
||||
background: #eab308;
|
||||
color: #1a0f0a;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 8px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 9px;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.express-hint {
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 13px;
|
||||
color: #fef08a;
|
||||
text-shadow: 1px 1px 2px #000;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user