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,150 @@
import { defineStore } from 'pinia'
import { sound } from '../audio/synthAudio'
export type GameAct = 'intro' | 'prologue' | 'platformer' | 'tavern' | 'party_express' | 'finale'
export interface GameState {
currentAct: GameAct
kronkorken: number
beers: number
guestsServed: number
gstanzlWon: boolean
partyMood: number
inventory: string[]
unlockedEasterEggs: string[]
startTime: number
elapsedSeconds: number
soundMuted: boolean
}
export const useGameStore = defineStore('game', {
state: (): GameState => ({
currentAct: 'intro',
kronkorken: 0,
beers: 0,
guestsServed: 0,
gstanzlWon: false,
partyMood: 20,
inventory: ['Königliche Sonnenbrille', 'Leergutsack'],
unlockedEasterEggs: [],
startTime: Date.now(),
elapsedSeconds: 0,
soundMuted: false,
}),
getters: {
isArcadeMode(state): boolean {
return state.currentAct === 'platformer' || state.currentAct === 'party_express'
},
totalScore(state): number {
const capPoints = state.kronkorken * 100
const beerPoints = state.beers * 250
const guestPoints = state.guestsServed * 300
const gstanzlBonus = state.gstanzlWon ? 1500 : 0
const eggBonus = state.unlockedEasterEggs.length * 500
const completionBonus = 2000
return capPoints + beerPoints + guestPoints + gstanzlBonus + eggBonus + completionBonus
},
scoreRank(): { rank: string; title: string; color: string } {
const score = this.totalScore
if (score >= 15000) {
return {
rank: 'S-RANG (LEGENDÄR)',
title: 'Göttlicher Imperator des 135-BPM-Schalldrucks',
color: '#ffd700'
}
} else if (score >= 12000) {
return {
rank: 'A-RANG (HERZOGLICH)',
title: 'Großfürst des Gerstensafts & Rave-Legende',
color: '#38bdf8'
}
} else if (score >= 9000) {
return {
rank: 'B-RANG (RITTERLICH)',
title: 'Ehren-Ritter der Hopfen-Manufaktur',
color: '#4ade80'
}
} else {
return {
rank: 'C-RANG (KNAPPE)',
title: 'Durstiger Party-Knappe von Haumdaucher',
color: '#fbbf24'
}
}
},
titleAward(state): string {
return this.scoreRank.title
}
},
actions: {
addKronkorken(amount: number = 1) {
this.kronkorken += amount
this.partyMood = Math.min(100, this.partyMood + 2 * amount)
sound.playCoin()
},
addBeer() {
this.beers++
this.partyMood = Math.min(100, this.partyMood + 10)
sound.playGulp()
},
serveGuest() {
this.guestsServed++
this.addBeer()
this.addKronkorken(3)
},
setGstanzlWon() {
this.gstanzlWon = true
this.addKronkorken(15)
},
addItem(item: string) {
if (!this.inventory.includes(item)) {
this.inventory.push(item)
}
},
unlockEasterEgg(egg: string) {
if (!this.unlockedEasterEggs.includes(egg)) {
this.unlockedEasterEggs.push(egg)
this.addKronkorken(5)
}
},
setAct(act: GameAct) {
this.currentAct = act
if (act === 'finale') {
sound.playFanfare()
}
},
startGame() {
this.currentAct = 'prologue'
sound.startBGM()
},
toggleMute() {
this.soundMuted = sound.toggleMute()
},
resetGame() {
this.currentAct = 'intro'
this.kronkorken = 0
this.beers = 0
this.guestsServed = 0
this.gstanzlWon = false
this.partyMood = 20
this.inventory = ['Königliche Sonnenbrille', 'Leergutsack']
this.unlockedEasterEggs = []
this.startTime = Date.now()
this.elapsedSeconds = 0
}
}
})