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,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>
|
||||
Reference in New Issue
Block a user