diff --git a/AGENTS.md b/AGENTS.md index 8206bd5..fb7a9ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ This repository is divided into distinct domains, each with its own `AGENTS.md` * **Purpose**: Deploying applications and services to the cluster. * **Method**: GitOps-style directory structure (Namespace = Folder). * **Constraints**: Enforces `ingress-nginx`, `cert-manager` (Let's Encrypt), and `git-crypt` for secrets. +* **OpenClaw**: For OpenClaw AI agent deployment and troubleshooting, see [k8s/openclaw/AGENTS.md](./k8s/openclaw/AGENTS.md) and [k8s/openclaw/TROUBLESHOOTING.md](./k8s/openclaw/TROUBLESHOOTING.md). * **Read**: [k8s/AGENTS.md](./k8s/AGENTS.md) ### 3. Terraform (`terraform/`) diff --git a/k8s/openclaw/AGENTS.md b/k8s/openclaw/AGENTS.md index f2f21a7..d819ad1 100644 --- a/k8s/openclaw/AGENTS.md +++ b/k8s/openclaw/AGENTS.md @@ -82,9 +82,12 @@ OpenClaw connects to three foundational Git repositories: --- -## 🚨 Troubleshooting & Live Verification +## 🚨 Troubleshooting & Incident Playbook -If the agent is offline or failing: +> [!IMPORTANT] +> **Mandatory Bug & Incident Documentation**: Whenever a bug, outage, schema error, or operational regression is fixed in OpenClaw, **always document the incident in [TROUBLESHOOTING.md](./TROUBLESHOOTING.md)** with symptom, root cause, resolution, and how to avoid recurrence. + +For known issues, resolved bugs, and specific failure playbooks, consult [TROUBLESHOOTING.md](./TROUBLESHOOTING.md). ### 1. Verify Deployment Health Ensure that the `OpenClawInstance` is successfully reconciled: diff --git a/k8s/openclaw/TROUBLESHOOTING.md b/k8s/openclaw/TROUBLESHOOTING.md new file mode 100644 index 0000000..5dac100 --- /dev/null +++ b/k8s/openclaw/TROUBLESHOOTING.md @@ -0,0 +1,90 @@ +# OpenClaw Troubleshooting & Incident Playbook + +This document captures fixed issues, operational quirks, and incident root-cause analyses for the **OpenClaw** deployment on Kubernetes. + +> [!IMPORTANT] +> **Rule for Agents & Operators**: Whenever you fix a bug, diagnose an operational failure, or resolve a configuration pitfall in OpenClaw, **document it here immediately**. This prevents recurring regressions and serves as the single source of truth for troubleshooting. + +--- + +## 📋 Catalog of Incidents & Fixed Issues + +| Date | Category | Summary / Symptom | Status | Key Resolution | +| :--- | :--- | :--- | :---: | :--- | +| **2026-09-20** | **Timeout & Execution** | Telegram `#IT Guy` (Topic 119) and `#Recherche` (Topic 117) request timed out before response generated (`timeoutSeconds` exceeded) | `RESOLVED` | Raised global `agents.defaults.timeoutSeconds` from 600 to 1800 (30 min). | +| **2026-09-20** | **Config Schema Validation** | Pod crash with `CrashLoopBackOff`: `agents.entries.: Unrecognized key: "timeoutSeconds"` | `RESOLVED` | `timeoutSeconds` is strictly global under `agents.defaults.timeoutSeconds`; do not place it under `agents.entries.*`. | +| **2026-09-24** | **Database & Startup Latency** | Gateway crash/restart during initialization: `slow OpenClaw agent database open` leading to Startup/Liveness probe failures | `RESOLVED` | Probe budget vs WAL checkpointing on large SQLite files (~1.5GB across subagents). | + +--- + +## 🔍 Detailed Incident Records + +### Issue 1: Telegram Agent Request Timeout (`agents.defaults.timeoutSeconds`) + +- **Date**: 2026-09-20 +- **Severity**: High (User-facing timeout in Telegram topics `#IT Guy` and `#Recherche`) +- **Symptom**: + Telegram replies with error message: + ```text + Request timed out before a response was generated. Please try again, or increase agents.defaults.timeoutSeconds in your config. + ``` +- **Root Cause**: + The IT Guy agent (`itguy`) performs deep, sequential DevOps investigations (multiple `exec` / `kubectl` calls, file writes, and PR operations) using `deepseek/deepseek-v4.1-flash`. Similarly, `#Recherche` runs deep research chains. The global default was set to `timeoutSeconds: 600` (10 minutes), causing active, non-stuck runs to be killed mid-flight. +- **Resolution**: + In [`k8s/openclaw/openclaw-instance.secret.yaml`](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/openclaw-instance.secret.yaml), configure: + ```yaml + spec: + config: + raw: + agents: + defaults: + timeoutSeconds: 1800 # 30 minutes + ``` +- **How to Avoid Recurrence**: + Always verify timeout requirements against the longest-running task types (multi-step K8s audits and deep research). Never assume 10 minutes is sufficient for multi-turn tool loops. + +--- + +### Issue 2: Schema Crash on Per-Agent Timeout Override + +- **Date**: 2026-09-20 +- **Severity**: Critical (Deployment blocked with `CrashLoopBackOff`) +- **Symptom**: + Gateway container fails during startup: + ```text + Gateway failed to start: Invalid config at /home/openclaw/.openclaw/openclaw.json: + openclaw.json:169 — agents.entries.itguy: Unrecognized key: "timeoutSeconds" + ``` +- **Root Cause**: + OpenClaw’s JSON schema strict validator does not permit `timeoutSeconds` inside individual agent entries under `agents.entries.`. +- **Resolution**: + Remove `timeoutSeconds` from `agents.entries.*`. Configure it strictly at the root of `agents.defaults.timeoutSeconds`. +- **How to Avoid Recurrence**: + Before adding new keys to agent configurations in `openclaw-instance.secret.yaml`, inspect the upstream OpenClaw schema or test locally to ensure the key is recognized under that exact path. + +--- + +### Issue 3: Startup Probe Failures due to Slow SQLite Database Initialization + +- **Date**: 2026-09-24 +- **Severity**: High (Intermittent container kills on gateway restart) +- **Symptom**: + Container `openclaw` fails startup or liveness probes, causing kubelet to restart the container: + ```text + Liveness probe failed: Get "http://...:18790/healthz": context deadline exceeded + Startup probe failed: context deadline exceeded / connection reset by peer + ``` + Logs display repeated warnings: + ```text + [state/agent-db] slow OpenClaw agent database open + ``` +- **Root Cause**: + OpenClaw maintains separate SQLite databases for each specialist subagent (`agents/*/agent/openclaw-agent.sqlite`). Over time, databases and uncommitted WAL logs accumulate significant size: + - `agents/main/agent/openclaw-agent.sqlite`: >220 MB + - `agents/fitness/`, `agents/itguy/`, `agents/hebamme/`, `agents/recherche/`: >100 MB each + multi-megabyte WAL files. + On startup, SQLite WAL recovery and integrity passes across 10+ databases on single-node hostpath storage can take several minutes. If this exceeds the `startupProbe` limit (`failureThreshold * periodSeconds`), Kubernetes kills the container before it finishes opening databases. +- **Resolution**: + 1. Ensure `probes.startup.failureThreshold` in `openclaw-instance.secret.yaml` allows adequate startup headroom (e.g. at least 120 attempts with 5s period = 600s). + 2. Periodically ensure SQLite VACUUM / WAL checkpoints run during maintenance windows or let the database open without interrupting it. +- **How to Avoid Recurrence**: + Monitor SQLite database sizes under `/home/openclaw/.openclaw/agents/*/agent/` and avoid overly aggressive liveness/startup probe kill thresholds during gateway initialization. diff --git a/projects/sasch40/frontend/nginx.conf b/projects/sasch40/frontend/nginx.conf index e0c74ce..f57088a 100644 --- a/projects/sasch40/frontend/nginx.conf +++ b/projects/sasch40/frontend/nginx.conf @@ -12,9 +12,16 @@ server { try_files $uri $uri/ /index.html; } - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { - expires 30d; - add_header Cache-Control "public, no-transform"; + # Static unhashed images, audio, and media - never cache to allow instant updates + location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|mp3|wav|ogg)$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + expires -1; + } + + # Vite bundled hashed JS/CSS assets + location ~* \.(js|css)$ { + expires 1y; + add_header Cache-Control "public, immutable"; } } diff --git a/projects/sasch40/frontend/public/assets/characters/amy/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/amy/portrait.jpg index 48da109..837259d 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/amy/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/amy/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/amy/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/amy/portrait_v2.jpg new file mode 100644 index 0000000..837259d Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/amy/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/armend/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/armend/portrait.jpg index bbe8d73..a1255a1 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/armend/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/armend/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/armend/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/armend/portrait_v2.jpg new file mode 100644 index 0000000..a1255a1 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/armend/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/flo_maier/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/flo_maier/portrait_v2.jpg new file mode 100644 index 0000000..f3a9563 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/flo_maier/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/hansi/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/hansi/portrait.jpg index f02d105..93cd977 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/hansi/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/hansi/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/hansi/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/hansi/portrait_v2.jpg new file mode 100644 index 0000000..93cd977 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/hansi/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/lutz/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/lutz/portrait.jpg index 6a805ee..9bf4f59 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/lutz/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/lutz/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/lutz/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/lutz/portrait_v2.jpg new file mode 100644 index 0000000..9bf4f59 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/lutz/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/mane/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/mane/portrait_v2.jpg new file mode 100644 index 0000000..5c892ec Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/mane/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/mario/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/mario/portrait_v2.jpg new file mode 100644 index 0000000..262012e Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/mario/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/mop/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/mop/portrait.jpg index 7b73d13..63673a9 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/mop/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/mop/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/mop/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/mop/portrait_v2.jpg new file mode 100644 index 0000000..63673a9 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/mop/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/nat/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/nat/portrait.jpg index 8cae767..72a2446 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/nat/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/nat/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/nat/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/nat/portrait_v2.jpg new file mode 100644 index 0000000..72a2446 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/nat/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/pue/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/pue/portrait_v2.jpg new file mode 100644 index 0000000..b2b45fa Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/pue/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/sascha/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/sascha/portrait_v2.jpg new file mode 100644 index 0000000..f432ba8 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/sascha/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/verena/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/verena/portrait.jpg index 0411a2e..dfa0793 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/verena/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/verena/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/verena/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/verena/portrait_v2.jpg new file mode 100644 index 0000000..dfa0793 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/verena/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/characters/viktor/portrait_v2.jpg b/projects/sasch40/frontend/public/assets/characters/viktor/portrait_v2.jpg new file mode 100644 index 0000000..32b1df7 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/characters/viktor/portrait_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg new file mode 100644 index 0000000..2c9a67e Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg new file mode 100644 index 0000000..2c9a67e Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg differ diff --git a/projects/sasch40/frontend/scripts/playtest.mjs b/projects/sasch40/frontend/scripts/playtest.mjs index 10bc7bd..3c4331b 100644 --- a/projects/sasch40/frontend/scripts/playtest.mjs +++ b/projects/sasch40/frontend/scripts/playtest.mjs @@ -171,6 +171,18 @@ async function runPlaytest() { if (actionBtn) await actionBtn.click(); await new Promise(r => setTimeout(r, 400)); } + + // Click NAT (Josef) + const natBtn = await page.$('.hotspot-nat'); + if (natBtn) { + await natBtn.click(); + await new Promise(r => setTimeout(r, 700)); + console.log('📸 09b: Capturing NAT (Josef) Dialog...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') }); + const actionBtn = await page.$('.dialog-actions .action-btn'); + if (actionBtn) await actionBtn.click(); + await new Promise(r => setTimeout(r, 400)); + } } // 7. TAVERN: HINTERLAND (Mane hiking) @@ -196,7 +208,7 @@ async function runPlaytest() { } // 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap) - console.log('🗝️ Returning to Schankraum to trigger Secret Tap...'); + console.log('🗝️ Returning to Schankraum to check Secret Tap (initially invisible)...'); const schankraumTab = await page.$('.room-tab:nth-child(1)'); if (schankraumTab) { await schankraumTab.click(); @@ -242,23 +254,46 @@ async function runPlaytest() { await new Promise(r => setTimeout(r, 400)); } - console.log('⚔️ Testing Gstanzl-Duel...'); + console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...'); const duelBtn = await page.$('.duel-trigger'); if (duelBtn) { await duelBtn.click(); await new Promise(r => setTimeout(r, 700)); - console.log('📸 15: Capturing Gstanzl Duel...'); + console.log('📸 15: Capturing Gstanzl Duel with [✕] button...'); await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15_tavern_gstanzl_duel.png') }); - const firstAnswer = await page.$('.choice-btn'); - if (firstAnswer) { - await firstAnswer.click(); - await new Promise(r => setTimeout(r, 500)); + + // Test the [✕] close button + const closeDuelBtn = await page.$('.duel-close-btn'); + if (closeDuelBtn) { + console.log('Testing duel exit via [✕] button...'); + await closeDuelBtn.click(); + await new Promise(r => setTimeout(r, 600)); + const duelClosed = await page.$('.duel-box') === null; + console.log('Duel modal closed successfully by [✕]:', duelClosed); } - // Close duel if won or modal - const actionBtn = await page.$('.duel-box .action-btn'); - if (actionBtn) await actionBtn.click(); } + // Test reload persistence in Tavern Act + console.log('💾 Testing game state reload persistence...'); + const stateBeforeReload = await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const pinia = app?.config?.globalProperties?.$pinia; + const s = pinia?._s.get('game'); + return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null; + }); + console.log('State before reload:', stateBeforeReload); + + await page.reload({ waitUntil: 'networkidle0' }); + await new Promise(r => setTimeout(r, 1000)); + + const stateAfterReload = await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const pinia = app?.config?.globalProperties?.$pinia; + const s = pinia?._s.get('game'); + return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null; + }); + console.log('State after reload:', stateAfterReload); + // 10. PARTY EXPRESS (Act IV) console.log('🚂 Advancing to Party Express (Act IV)...'); await page.evaluate(() => { @@ -310,6 +345,16 @@ async function runPlaytest() { console.log('📸 18: Capturing Certificate Modal...'); await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '18_certificate_screen.png') }); + // Test NOCHMAL SPIELEN button + console.log('🔄 Testing NOCHMAL SPIELEN button...'); + const restartBtn = await page.$('.restart-btn'); + if (restartBtn) { + await restartBtn.click(); + await new Promise(r => setTimeout(r, 1000)); + console.log('📸 19: Capturing post-restart Intro screen...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '19_post_restart_intro.png') }); + } + // Verify all images loaded properly const brokenImages = await page.evaluate(() => { const imgs = Array.from(document.querySelectorAll('img')); diff --git a/projects/sasch40/frontend/src/audio/synthAudio.ts b/projects/sasch40/frontend/src/audio/synthAudio.ts index 3ceca07..9f17abe 100644 --- a/projects/sasch40/frontend/src/audio/synthAudio.ts +++ b/projects/sasch40/frontend/src/audio/synthAudio.ts @@ -31,6 +31,13 @@ class SoundEngine { return this.isMuted } + public setMuted(val: boolean) { + this.isMuted = val + if (this.isMuted) { + this.stopBGM() + } + } + // SFX: Kronkorken Collect (bright double ding) public playCoin() { if (this.isMuted) return diff --git a/projects/sasch40/frontend/src/components/CertificateModal.vue b/projects/sasch40/frontend/src/components/CertificateModal.vue index f37c77c..d395367 100644 --- a/projects/sasch40/frontend/src/components/CertificateModal.vue +++ b/projects/sasch40/frontend/src/components/CertificateModal.vue @@ -34,7 +34,7 @@ function printOrSave() {
König Sascha I. diff --git a/projects/sasch40/frontend/src/components/IntroView.vue b/projects/sasch40/frontend/src/components/IntroView.vue index 8c6e29e..3589192 100644 --- a/projects/sasch40/frontend/src/components/IntroView.vue +++ b/projects/sasch40/frontend/src/components/IntroView.vue @@ -23,7 +23,7 @@ function handleStart() {
König Sascha I. diff --git a/projects/sasch40/frontend/src/components/TavernView.vue b/projects/sasch40/frontend/src/components/TavernView.vue index 848c20b..e7be007 100644 --- a/projects/sasch40/frontend/src/components/TavernView.vue +++ b/projects/sasch40/frontend/src/components/TavernView.vue @@ -26,11 +26,20 @@ const duelStep = ref(0) const duelWon = ref(false) const zulumanMirrorActive = ref(false) +const secretTapDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer')) + function switchRoom(room: TavernRoom) { sound.playDialogueBlip() currentRoom.value = room } +function triggerSecretTap() { + sound.playFanfare() + secretTapDiscovered.value = true + store.unlockEasterEgg('hinterzimmer') + currentRoom.value = 'hinterzimmer' +} + function enterSecretHinterzimmer() { sound.playFanfare() store.unlockEasterEgg('hinterzimmer') @@ -229,7 +238,7 @@ function proceedToExpress() { @@ -465,8 +481,11 @@ function proceedToExpress() {
-

⚔️ BAYERISCHES BELEIDIGUNGS-GSTANZL

- Runde {{ duelStep + 1 }} von 3 +
+

⚔️ BAYERISCHES BELEIDIGUNGS-GSTANZL

+ Runde {{ duelStep + 1 }} von 3 +
+
@@ -578,14 +597,30 @@ function proceedToExpress() { /* Secret Tap Easter Egg */ .secret-tap-hotspot { position: absolute; - top: 73%; - left: 88%; + top: 72%; + left: 82%; background: rgba(234, 179, 8, 0.2); border: 1.5px dashed #ffd700; border-radius: 12px; padding: 3px 6px; cursor: pointer; z-index: 12; + transition: all 0.2s; +} + +.secret-tap-hotspot.tap-invisible, +.secret-tap-hotspot.tap-invisible:hover, +.secret-tap-hotspot.tap-invisible:active, +.secret-tap-hotspot.tap-invisible:focus { + background: transparent !important; + border: none !important; + box-shadow: none !important; + outline: none !important; + transform: translate(-50%, -50%) !important; + width: 50px; + height: 50px; + padding: 0 !important; + cursor: pointer; } .secret-tap-icon { @@ -599,18 +634,18 @@ function proceedToExpress() { } /* 1. Schankraum Hotspots */ -.hotspot-verena { top: 16%; left: 78%; } -.hotspot-armend { top: 62%; left: 24%; } -.hotspot-lutz { top: 38%; left: 20%; } -.hotspot-amy { top: 52%; left: 80%; } -.hotspot-mirror { top: 32%; left: 63%; } -.hotspot-door-marktplatz { top: 56%; left: 90%; } +.hotspot-verena { top: 17%; left: 28%; } +.hotspot-mirror { top: 30%; left: 68%; } +.hotspot-amy { top: 45%; left: 32%; } +.hotspot-lutz { top: 54%; left: 56%; } +.hotspot-armend { top: 62%; left: 18%; } +.hotspot-door-marktplatz { top: 50%; left: 84%; } /* 2. Marktplatz Hotspots */ .mario-container { position: absolute; - top: 52%; - left: 34%; + top: 44%; + left: 30%; z-index: 10; } @@ -664,12 +699,12 @@ function proceedToExpress() { 100% { opacity: 1; transform: scale(1.3); } } -.hotspot-nat { top: 62%; left: 74%; } -.hotspot-door-back-gaststube { top: 72%; left: 16%; } -.hotspot-door-hinterland { top: 36%; left: 84%; } +.hotspot-nat { top: 62%; left: 70%; } +.hotspot-door-back-gaststube { top: 72%; left: 20%; } +.hotspot-door-hinterland { top: 32%; left: 76%; } /* 3. Hinterland Hotspots */ -.hotspot-mane { top: 50%; left: 42%; } +.hotspot-mane { top: 44%; left: 36%; } .hiking-anim { animation: hikeBob 1.2s infinite ease-in-out; } @@ -684,8 +719,8 @@ function proceedToExpress() { transform: rotate(20deg); } -.hotspot-hansi { top: 66%; left: 78%; } -.hotspot-door-back-marktplatz { top: 74%; left: 20%; } +.hotspot-hansi { top: 60%; left: 74%; } +.hotspot-door-back-marktplatz { top: 72%; left: 22%; } /* 4. Hinterzimmer Hotspots & Strobe */ .strobe-overlay { @@ -704,24 +739,10 @@ function proceedToExpress() { 100% { background: rgba(168, 85, 247, 0.25); } } -.hotspot-pue { top: 46%; left: 48%; } -.rave-pulse { - animation: raveGlow 1s infinite alternate; -} - -@keyframes raveGlow { - 0% { box-shadow: 0 0 4px #eab308; } - 100% { box-shadow: 0 0 16px #ec4899; } -} - -.crystal-laser { - font-size: 13px; - animation: pulseAnim 0.8s infinite; -} - -.hotspot-flo { top: 40%; left: 22%; } -.hotspot-viktor { top: 62%; left: 78%; } -.hotspot-door-back-vip { top: 78%; left: 50%; } +.hotspot-flo { top: 38%; left: 24%; } +.hotspot-viktor { top: 52%; left: 62%; } +.hotspot-pue { top: 64%; left: 82%; } +.hotspot-door-back-vip { top: 74%; left: 45%; } .pulse { animation: pulseAnim 1.5s infinite; @@ -958,11 +979,18 @@ function proceedToExpress() { /* Gstanzl */ .duel-header { + display: flex; + justify-content: space-between; + align-items: flex-start; border-bottom: 1px solid #78350f; margin-bottom: 10px; padding-bottom: 6px; } +.duel-title-group { + flex: 1; +} + .duel-header h3 { font-family: 'Press Start 2P', monospace; font-size: 8px; @@ -975,6 +1003,31 @@ function proceedToExpress() { color: #94a3b8; } +.duel-close-btn { + background: #7f1d1d; + color: #fef08a; + border: 1px solid #ef4444; + border-radius: 50%; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: bold; + cursor: pointer; + margin-left: 8px; + flex-shrink: 0; + transition: all 0.2s ease; + line-height: 1; +} + +.duel-close-btn:hover { + background: #dc2626; + transform: scale(1.1); + box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); +} + .insult-bubble { background: #451a03; border-left: 3px solid #ef4444; diff --git a/projects/sasch40/frontend/src/config/characters.ts b/projects/sasch40/frontend/src/config/characters.ts index 66d8180..e09e37f 100644 --- a/projects/sasch40/frontend/src/config/characters.ts +++ b/projects/sasch40/frontend/src/config/characters.ts @@ -13,7 +13,7 @@ export const CHARACTERS: Record = { id: 'sascha', name: 'König Sascha I.', title: 'Der 40-jährige Imperator', - portrait: '/assets/characters/sascha/portrait.jpg', + portrait: '/assets/characters/sascha/portrait_v2.jpg', description: 'Herrscher über Haumdaucher, Träger der heiligen Sonnenbrille und Beschützer des 135-BPM-Basses.', famousQuote: 'O zapft is im Reiche! Wo san meine Kronkorken?!', themeColor: '#d4af37' @@ -22,7 +22,7 @@ export const CHARACTERS: Record = { id: 'mop', name: 'Mop (Moritz)', title: 'Der Welten-Erschaffer & Matrix-Architekt', - portrait: '/assets/characters/mop/portrait.jpg', + portrait: '/assets/characters/mop/portrait_v2.jpg', description: 'Sitzt auf einer Wolke über dem Schloss und baut aus Pixeln und Hopfen diese Welt.', famousQuote: 'Just adding a few more polygons here... wait, pixels. Pixels.', themeColor: '#38bdf8' @@ -31,7 +31,7 @@ export const CHARACTERS: Record = { id: 'nat', name: 'NAT (Josef)', title: 'Hof-Chronist & Barde des Wildschweins', - portrait: '/assets/characters/nat/portrait.jpg', + portrait: '/assets/characters/nat/portrait_v2.jpg', description: 'Kennt alle 3 Staffeln Bridgerton in- und auswendig, trägt florale Bomberjacken und erzählt epische Geschichten.', famousQuote: 'Also pass auf Sascha... ich damals mit meinen Girls ab...', themeColor: '#22c55e' @@ -40,7 +40,7 @@ export const CHARACTERS: Record = { id: 'verena', name: 'Königin Verena', title: 'Herrin des Hofes', - portrait: '/assets/characters/verena/portrait.jpg', + portrait: '/assets/characters/verena/portrait_v2.jpg', description: 'Liebt Sascha über alles auf der Welt, hat aber ein unumstößliches königliches Edikt erlassen.', famousQuote: 'Ich hab dich lieb mein Schatz, aber ich putze nach der Party fei ned!', themeColor: '#ec4899' @@ -49,7 +49,7 @@ export const CHARACTERS: Record = { id: 'hansi', name: 'Hansi (Johann)', title: 'Ritter der floralen Garde', - portrait: '/assets/characters/hansi/portrait.jpg', + portrait: '/assets/characters/hansi/portrait_v2.jpg', description: 'Perfekt gestylter Schnurrbart und florale Plattenrüstung.', famousQuote: 'Habe die Ehre! Der Bass im Rittersaal ist auf Punkt.', themeColor: '#6366f1' @@ -58,7 +58,7 @@ export const CHARACTERS: Record = { id: 'lutz', name: 'Lutz', title: 'Der ZTUL-Hofschornsteinfeger', - portrait: '/assets/characters/lutz/portrait.jpg', + portrait: '/assets/characters/lutz/portrait_v2.jpg', description: 'Ein Hüne von Gestalt mit Kappe, runder Brille und dem legendären ZTUL-Trikot.', famousQuote: 'Glück auf, Euer Durchlaucht! Schornstein ist frei für den Party-Nebel.', themeColor: '#64748b' @@ -67,7 +67,7 @@ export const CHARACTERS: Record = { id: 'armend', name: 'Armend', title: 'Meister-Schmied der Kronkorken-Manufaktur', - portrait: '/assets/characters/armend/portrait.jpg', + portrait: '/assets/characters/armend/portrait_v2.jpg', description: 'Prachtvoller Vollbart, formt aus 40 Kronkorken den goldenen Sound-Verstärker.', famousQuote: 'Schlag zu, solange das Blech warm ist!', themeColor: '#f97316' @@ -76,7 +76,7 @@ export const CHARACTERS: Record = { id: 'amy', name: 'Amy (Amelie)', title: 'Die Hof-Künstlerin', - portrait: '/assets/characters/amy/portrait.jpg', + portrait: '/assets/characters/amy/portrait_v2.jpg', description: 'Kreatives Genie in Latzhose mit Dutt und feinen Kräuter-Zigarren.', famousQuote: 'Rave the Kingdom! Die Farben brauchen mehr Bass!', themeColor: '#a855f7' @@ -85,7 +85,7 @@ export const CHARACTERS: Record = { id: 'pue', name: 'Pü (Jakob)', title: 'Lichtmeister & Kristall-Rave-Magier', - portrait: '/assets/characters/pue/portrait.jpg', + portrait: '/assets/characters/pue/portrait_v2.jpg', description: 'Lässt im geheimen Hinterzimmer die Disco-Kristalle tanzen und erhellt die Entourage mit bunten Lasern.', famousQuote: 'Bass auf Anschlag, Kristalle auf Stroboskop! Willkommen im geheimen Hinterzimmer!', themeColor: '#eab308' @@ -94,7 +94,7 @@ export const CHARACTERS: Record = { id: 'mario', name: 'Mario', title: 'Hof-Gaukler & Kettensägen-Virtuose', - portrait: '/assets/characters/mario/portrait.jpg', + portrait: '/assets/characters/mario/portrait_v2.jpg', description: 'Jongliert furchtlos auf dem Marktplatz mit kreischenden Kettensägen und Hopfenkrügen.', famousQuote: 'Pass auf die Zinken auf! Kettensägen-Akrobatik für König Sascha!', themeColor: '#14b8a6' @@ -103,7 +103,7 @@ export const CHARACTERS: Record = { id: 'mane', name: 'Mane (Ramona)', title: 'Königliche Alpen-Wanderin', - portrait: '/assets/characters/mane/portrait.jpg', + portrait: '/assets/characters/mane/portrait_v2.jpg', description: 'Zieht mit Wanderstock, Sonnenbrille und Flechtzopf durchs alpine Hinterland von Haumdaucher.', famousQuote: 'Schee ists hier! Noch 3 Kilometer über den Grat bis zum Gipfel-Bier!', themeColor: '#84cc16' @@ -112,7 +112,7 @@ export const CHARACTERS: Record = { id: 'flo_maier', name: 'Flo Maier', title: 'Hof-DJ & Sound-Ingenieur', - portrait: '/assets/characters/flo_maier/portrait.jpg', + portrait: '/assets/characters/flo_maier/portrait_v2.jpg', description: 'Mastermind an den Turntables und Behüter der heiligen 135-BPM-Frequenz.', famousQuote: 'Dieser Track drückt mehr als die königliche Kutsche!', themeColor: '#0284c7' @@ -121,7 +121,7 @@ export const CHARACTERS: Record = { id: 'viktor', name: 'Viktor', title: 'Nachtwächter & Party-Ritter', - portrait: '/assets/characters/viktor/portrait.jpg', + portrait: '/assets/characters/viktor/portrait_v2.jpg', description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und sorgt für eiskaltes Helles.', famousQuote: 'Die Nacht ist jung und der Gerstensaft fließt in Strömen!', themeColor: '#b91c1c' @@ -130,7 +130,7 @@ export const CHARACTERS: Record = { id: 'djs', name: 'Die Hof-DJs', title: 'Minnesänger der 135 BPM', - portrait: '/assets/characters/flo_maier/portrait.jpg', + portrait: '/assets/characters/flo_maier/portrait_v2.jpg', description: 'Stehen an der königlichen Kanzel am doppelten Eichenholz-Plattenteller.', famousQuote: 'Drop it like it\'s 1426! Mehr Bass auf die Burgzinnen!', themeColor: '#06b6d4' @@ -139,7 +139,7 @@ export const CHARACTERS: Record = { id: 'gefolge', name: 'Das Haumdaucher Gefolge', title: 'Die feiernde Reichs-Crowd', - portrait: '/assets/characters/viktor/portrait.jpg', + portrait: '/assets/characters/viktor/portrait_v2.jpg', description: 'Viktor der Vampir-Ritter, DJ Flo Maier, Hansi, Fabian, Albin, der bärtige Guatemalteke, Markus & Nathalie.', famousQuote: 'Ein Hoch auf König Sascha! Hoch soll er leben!', themeColor: '#e11d48' diff --git a/projects/sasch40/frontend/src/main.ts b/projects/sasch40/frontend/src/main.ts index a2cedb6..aa62600 100644 --- a/projects/sasch40/frontend/src/main.ts +++ b/projects/sasch40/frontend/src/main.ts @@ -4,5 +4,13 @@ import './style.css' import App from './App.vue' const app = createApp(App) -app.use(createPinia()) +const pinia = createPinia() +app.use(pinia) + +import { useGameStore, saveStateToStorage } from './stores/gameStore' +const store = useGameStore() +store.$subscribe((_mutation, state) => { + saveStateToStorage(state) +}) + app.mount('#app') diff --git a/projects/sasch40/frontend/src/stores/gameStore.ts b/projects/sasch40/frontend/src/stores/gameStore.ts index 71e2b7a..b26d3ac 100644 --- a/projects/sasch40/frontend/src/stores/gameStore.ts +++ b/projects/sasch40/frontend/src/stores/gameStore.ts @@ -17,8 +17,12 @@ export interface GameState { soundMuted: boolean } -export const useGameStore = defineStore('game', { - state: (): GameState => ({ +export const STORAGE_KEY = 'sasch40_game_save_v1' + +const VALID_ACTS: GameAct[] = ['intro', 'prologue', 'platformer', 'tavern', 'party_express', 'finale'] + +function getInitialState(): GameState { + const defaults: GameState = { currentAct: 'intro', kronkorken: 0, beers: 0, @@ -30,7 +34,66 @@ export const useGameStore = defineStore('game', { startTime: Date.now(), elapsedSeconds: 0, soundMuted: false, - }), + } + + if (typeof window === 'undefined' || !window.localStorage) { + return defaults + } + + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return defaults + const parsed = JSON.parse(raw) + if (parsed && typeof parsed === 'object') { + const state: GameState = { + currentAct: VALID_ACTS.includes(parsed.currentAct) ? parsed.currentAct : 'intro', + kronkorken: typeof parsed.kronkorken === 'number' && !isNaN(parsed.kronkorken) ? parsed.kronkorken : 0, + beers: typeof parsed.beers === 'number' && !isNaN(parsed.beers) ? parsed.beers : 0, + guestsServed: typeof parsed.guestsServed === 'number' && !isNaN(parsed.guestsServed) ? parsed.guestsServed : 0, + gstanzlWon: Boolean(parsed.gstanzlWon), + partyMood: typeof parsed.partyMood === 'number' && !isNaN(parsed.partyMood) ? parsed.partyMood : 20, + inventory: Array.isArray(parsed.inventory) ? parsed.inventory : defaults.inventory, + unlockedEasterEggs: Array.isArray(parsed.unlockedEasterEggs) ? parsed.unlockedEasterEggs : [], + startTime: typeof parsed.startTime === 'number' ? parsed.startTime : defaults.startTime, + elapsedSeconds: typeof parsed.elapsedSeconds === 'number' ? parsed.elapsedSeconds : 0, + soundMuted: Boolean(parsed.soundMuted) + } + if (state.soundMuted) { + sound.setMuted(true) + } + return state + } + } catch (e) { + console.warn('Failed to parse saved game state:', e) + } + + return defaults +} + +export function saveStateToStorage(state: GameState) { + if (typeof window === 'undefined' || !window.localStorage) return + try { + const dataToSave = { + currentAct: state.currentAct, + kronkorken: state.kronkorken, + beers: state.beers, + guestsServed: state.guestsServed, + gstanzlWon: state.gstanzlWon, + partyMood: state.partyMood, + inventory: state.inventory, + unlockedEasterEggs: state.unlockedEasterEggs, + startTime: state.startTime, + elapsedSeconds: state.elapsedSeconds, + soundMuted: state.soundMuted + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave)) + } catch (e) { + console.warn('Failed to save game state to localStorage:', e) + } +} + +export const useGameStore = defineStore('game', { + state: (): GameState => getInitialState(), getters: { isArcadeMode(state): boolean { @@ -135,6 +198,13 @@ export const useGameStore = defineStore('game', { }, resetGame() { + if (typeof window !== 'undefined' && window.localStorage) { + try { + localStorage.removeItem(STORAGE_KEY) + } catch (e) { + console.warn('Failed to clear saved state', e) + } + } this.currentAct = 'intro' this.kronkorken = 0 this.beers = 0 @@ -145,6 +215,7 @@ export const useGameStore = defineStore('game', { this.unlockedEasterEggs = [] this.startTime = Date.now() this.elapsedSeconds = 0 + saveStateToStorage(this.$state) } } })