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:
Moritz Graf
2026-09-23 21:56:34 +02:00
parent d11a84a0d3
commit 5852479c21
53 changed files with 5544 additions and 0 deletions
@@ -0,0 +1,434 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useGameStore } from '../stores/gameStore'
import confetti from 'canvas-confetti'
const store = useGameStore()
const certificateRef = ref<HTMLDivElement | null>(null)
onMounted(() => {
// Fire celebratory party confetti
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
})
})
function restart() {
store.resetGame()
}
function printOrSave() {
window.print()
}
</script>
<template>
<div class="certificate-container">
<div ref="certificateRef" class="certificate-paper">
<div class="royal-seal">👑 40 👑</div>
<h1 class="cert-title">KÖNIGLICHE THRON-URKUNDE</h1>
<h2 class="cert-subtitle">Gratulation zu 40 Jahren Regentschaft!</h2>
<div class="king-portrait-wrapper">
<img
src="/assets/characters/sascha/portrait.jpg"
alt="König Sascha I."
class="king-portrait"
/>
</div>
<!-- Party Competition Highscore Banner -->
<div class="score-banner" :style="{ borderColor: store.scoreRank.color }">
<div class="score-rank-title" :style="{ color: store.scoreRank.color }">
★ {{ store.scoreRank.rank }} ★
</div>
<div class="score-number">
{{ store.totalScore.toLocaleString('de-DE') }} PUNKTE
</div>
<div class="score-subtitle">
„{{ store.scoreRank.title }}“
</div>
</div>
<div class="cert-body">
<p class="decree-text">
Hiermit bekundet das gesamte Herzogtum Haumdaucher, dass
</p>
<h3 class="honoree-name">KÖNIG SASCHA I.</h3>
<p class="decree-details">
die 40 Kronkorken des Reiches glorreich vereint und den Bass auf unsterbliche 135 BPM gehoben hat!
</p>
</div>
<!-- Stats Grid -->
<div class="cert-stats">
<div class="stat-card">
<span class="stat-val">👑 {{ store.kronkorken }}</span>
<span class="stat-lbl">KRONKORKEN</span>
</div>
<div class="stat-card">
<span class="stat-val">🍻 {{ store.beers }}</span>
<span class="stat-lbl">KRÜGE HELLES</span>
</div>
<div class="stat-card">
<span class="stat-val">👥 {{ store.guestsServed }}</span>
<span class="stat-lbl">GEFOLGE VERSORGT</span>
</div>
<div class="stat-card">
<span class="stat-val">🔊 135</span>
<span class="stat-lbl">TECHNO BPM</span>
</div>
</div>
<!-- Point Breakdown Table -->
<div class="breakdown-card">
<div class="breakdown-header">📊 SCORE-AUFSCHLÜSSELUNG</div>
<div class="breakdown-row">
<span>Kronkorken ({{ store.kronkorken }}x)</span>
<span class="breakdown-pts">+{{ store.kronkorken * 100 }}</span>
</div>
<div class="breakdown-row">
<span>Helles gezapft ({{ store.beers }}x)</span>
<span class="breakdown-pts">+{{ store.beers * 250 }}</span>
</div>
<div class="breakdown-row">
<span>Gefolge glücklich ({{ store.guestsServed }}x)</span>
<span class="breakdown-pts">+{{ store.guestsServed * 300 }}</span>
</div>
<div class="breakdown-row">
<span>Gstanzl-Battle Sieg</span>
<span class="breakdown-pts">{{ store.gstanzlWon ? '+1500' : '0' }}</span>
</div>
<div class="breakdown-row">
<span>Geheimnisse ({{ store.unlockedEasterEggs.length }}x)</span>
<span class="breakdown-pts">+{{ store.unlockedEasterEggs.length * 500 }}</span>
</div>
<div class="breakdown-row total-row">
<span>GESAMT-ERGEBNIS</span>
<span class="total-pts">{{ store.totalScore.toLocaleString('de-DE') }}</span>
</div>
<div class="compare-callout">
📱 Zeig deinen Score an der Bar & vergleiche mit anderen Gästen!
</div>
</div>
<!-- Royal Postscript from Verena -->
<div class="queen-ps">
<span class="queen-signature">Königin Verena bemerkt:</span>
<p>„Ich hab dich unendlich lieb mein Schatz... aber geputzt wird nach der Party fei trotzdem ned!“ 🧹</p>
</div>
<!-- Easter Eggs Unlocked -->
<div v-if="store.unlockedEasterEggs.length" class="eggs-list">
<span>Gefundene Geheimnisse:</span>
<div class="egg-tags">
<span v-for="egg in store.unlockedEasterEggs" :key="egg" class="egg-badge">
🌟 {{ egg.toUpperCase() }}
</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="cert-actions">
<button class="save-btn" @click="printOrSave">
📸 URKUNDE DRUCKEN / SCREENSHOT
</button>
<button class="restart-btn" @click="restart">
🔄 NOCHMAL SPIELEN
</button>
</div>
</div>
</template>
<style scoped>
.certificate-container {
width: 100%;
height: 100%;
background: #1a0f0a;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.certificate-paper {
background: #fdf6e2;
border: 6px double #854d0e;
border-radius: 12px;
padding: 16px 14px;
width: 100%;
max-width: 330px;
text-align: center;
color: #451a03;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7);
position: relative;
}
.royal-seal {
font-size: 16px;
font-weight: bold;
color: #b45309;
margin-bottom: 4px;
}
.cert-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: #78350f;
margin: 0 0 6px 0;
}
.cert-subtitle {
font-family: 'MedievalSharp', serif;
font-size: 17px;
font-weight: bold;
color: #b91c1c;
margin: 0 0 10px 0;
}
.king-portrait-wrapper {
margin: 0 auto 10px auto;
width: 90px;
height: 90px;
border: 3px solid #d4af37;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.king-portrait {
width: 100%;
height: 100%;
object-fit: cover;
}
.decree-text {
font-family: 'MedievalSharp', serif;
font-size: 13px;
margin: 4px 0;
}
.honoree-name {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
color: #991b1b;
margin: 6px 0 2px 0;
}
.honoree-title {
font-family: 'MedievalSharp', serif;
font-size: 14px;
font-style: italic;
font-weight: bold;
color: #b45309;
margin: 0 0 6px 0;
}
.decree-details {
font-family: 'MedievalSharp', serif;
font-size: 13px;
line-height: 1.3;
margin: 0 0 10px 0;
}
.cert-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 12px;
}
.stat-card {
background: rgba(217, 119, 6, 0.15);
border: 1px solid #d97706;
border-radius: 6px;
padding: 4px;
display: flex;
flex-direction: column;
}
.stat-val {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: #92400e;
}
.stat-lbl {
font-size: 9px;
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);
}
.score-rank-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
margin-bottom: 4px;
text-shadow: 0 0 8px currentColor;
}
.score-number {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
color: #fff;
letter-spacing: 1px;
margin: 4px 0;
}
.score-subtitle {
font-family: 'MedievalSharp', serif;
font-size: 13px;
font-style: italic;
color: #fef08a;
}
.breakdown-card {
background: rgba(254, 243, 199, 0.7);
border: 1px solid #d97706;
border-radius: 6px;
padding: 8px;
margin-bottom: 10px;
font-family: 'MedievalSharp', serif;
font-size: 12px;
text-align: left;
}
.breakdown-header {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: #78350f;
margin-bottom: 6px;
text-align: center;
border-bottom: 1px dashed #b45309;
padding-bottom: 4px;
}
.breakdown-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
color: #451a03;
}
.breakdown-pts {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: #15803d;
font-weight: bold;
}
.total-row {
border-top: 1px solid #b45309;
margin-top: 4px;
padding-top: 4px;
font-weight: bold;
}
.total-pts {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: #b45309;
}
.compare-callout {
margin-top: 6px;
padding: 4px;
background: #fef08a;
border: 1px solid #eab308;
border-radius: 4px;
font-size: 10px;
color: #713f12;
text-align: center;
font-weight: bold;
}
.queen-ps {
background: #fef3c7;
border-left: 3px solid #ec4899;
padding: 6px 8px;
border-radius: 4px;
margin-bottom: 8px;
font-family: 'MedievalSharp', serif;
text-align: left;
}
.queen-signature {
font-size: 11px;
font-weight: bold;
color: #be185d;
}
.queen-ps p {
margin: 2px 0 0 0;
font-size: 12px;
color: #831843;
}
.eggs-list {
font-size: 11px;
color: #78350f;
margin-top: 6px;
}
.egg-tags {
display: flex;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
margin-top: 4px;
}
.egg-badge {
background: #f59e0b;
color: #fff;
font-size: 8px;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Press Start 2P', monospace;
}
.cert-actions {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 330px;
}
.save-btn {
background: #15803d;
color: #fff;
border: 2px solid #86efac;
border-radius: 8px;
padding: 10px;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
.restart-btn {
background: #78350f;
color: #fef08a;
border: 1px solid #b45309;
border-radius: 8px;
padding: 8px;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
cursor: pointer;
}
</style>