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,202 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useGameStore } from '../stores/gameStore'
import { PROLOGUE_DIALOGUES, type DialogueNode } from '../config/dialogues'
import { CHARACTERS } from '../config/characters'
import { sound } from '../audio/synthAudio'
const store = useGameStore()
const currentNodeId = ref('start')
function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0]) {
sound.playDialogueBlip()
if (choice.action === 'addKronkorken' && choice.reward) {
store.addKronkorken(choice.reward)
}
if (choice.action === 'unlockBridgerton') {
store.unlockEasterEgg('bridgerton')
}
if (choice.action === 'startPlatformer') {
store.setAct('platformer')
return
}
if (choice.nextId && PROLOGUE_DIALOGUES[choice.nextId]) {
currentNodeId.value = choice.nextId
}
}
const currentNode = () => PROLOGUE_DIALOGUES[currentNodeId.value]
const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['sascha']
</script>
<template>
<div class="prologue-container">
<!-- Balcony Visual Scene -->
<div class="scene-visual">
<img
:src="currentSpeaker().portrait"
:alt="currentSpeaker().name"
class="speaker-visual"
/>
<div class="scene-tag">
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
</div>
</div>
<!-- Retro Dialogue Card -->
<div class="dialogue-card">
<div class="dialogue-header">
<div class="speaker-name-badge">
<span class="speaker-name">{{ currentSpeaker().name }}</span>
<span class="speaker-title">{{ currentSpeaker().title }}</span>
</div>
</div>
<div class="dialogue-body">
<p class="dialogue-text">„{{ currentNode().text }}“</p>
</div>
<div class="dialogue-choices">
<button
v-for="(choice, idx) in currentNode().choices"
:key="idx"
class="choice-btn"
@click="selectChoice(choice)"
>
<span class="choice-arrow">▶</span>
<span class="choice-text">„{{ choice.text }}“</span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.prologue-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: #1a0f0a;
overflow: hidden;
}
.scene-visual {
position: relative;
flex: 1;
min-height: 140px;
max-height: 200px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background: #2b1810;
border-bottom: 2px solid #d4af37;
}
.speaker-visual {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
image-rendering: crisp-edges;
}
.scene-tag {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0, 0, 0, 0.75);
border: 1px solid #ffd700;
padding: 3px 8px;
border-radius: 4px;
}
.scene-title {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
color: #ffd700;
}
.dialogue-card {
flex: 1;
background: #26160e;
padding: 12px;
display: flex;
flex-direction: column;
border-top: 1px solid #78350f;
overflow-y: auto;
}
.dialogue-header {
margin-bottom: 8px;
}
.speaker-name-badge {
display: flex;
flex-direction: column;
}
.speaker-name {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: #facc15;
}
.speaker-title {
font-size: 11px;
color: #cbd5e1;
font-family: 'MedievalSharp', serif;
}
.dialogue-body {
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.4);
padding: 8px 10px;
border-left: 3px solid #d4af37;
border-radius: 4px;
}
.dialogue-text {
margin: 0;
font-family: 'MedievalSharp', serif;
font-size: 14px;
line-height: 1.35;
color: #fef3c7;
}
.dialogue-choices {
display: flex;
flex-direction: column;
gap: 6px;
}
.choice-btn {
display: flex;
align-items: center;
gap: 8px;
background: #3b2014;
border: 1px solid #b45309;
border-radius: 6px;
padding: 8px 10px;
text-align: left;
color: #fef08a;
font-family: 'MedievalSharp', serif;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.choice-btn:hover, .choice-btn:active {
background: #78350f;
border-color: #ffd700;
transform: translateX(3px);
}
.choice-arrow {
color: #f59e0b;
font-size: 10px;
}
</style>