feat: Add audio assets, UI escape room polish, requirements, and INSTALL.md

This commit is contained in:
Maxi
2026-10-29 08:51:59 +01:00
parent c07c0e10b9
commit 3d30d0812a
17 changed files with 1367 additions and 180 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
venv/
__pycache__/ __pycache__/
*.pyc *.pyc
venv/ .env
+82
View File
@@ -0,0 +1,82 @@
# Magisches Hogwarts Escape Adventure (Bootle)
Interaktives Hogwarts Escape Room & Potion Brewing Adventure (FastAPI).
## Systemanforderungen
- Debian 12 / 13 (oder kompatibles Linux)
- Python 3.13+ mit `python3-venv`
- Nginx (Reverse Proxy mit SSL)
## 1. Installation
```bash
# Repository klonen
cd /home/maxi/Projekt
git clone https://gitea.sp13l.de/kobold-ai/bootle.git bootle.waiswuaschd.de
cd bootle.waiswuaschd.de
# Python Virtualenv erstellen und Abh?ngigkeiten installieren
python3 -m venv venv
./venv/bin/pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
```
## 2. Umgebungsvariablen (.env)
Erstelle im Projektverzeichnis eine `.env` Datei:
```ini
PORT=8002
SECRET_KEY=hogwarts_magic_bootle_potion_key_2026_escape
```
## 3. Systemd Service (/etc/systemd/system/bootle.service)
```ini
[Unit]
Description=Magisches Hogwarts Escape Adventure (FastAPI)
After=network.target
[Service]
Type=simple
User=maxi
Group=maxi
WorkingDirectory=/home/maxi/Projekt/bootle.waiswuaschd.de
Environment="PATH=/home/maxi/Projekt/bootle.waiswuaschd.de/venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/maxi/Projekt/bootle.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8002 --workers 1
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
Aktivieren und starten:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now bootle
```
## 4. Nginx Reverse Proxy
```nginx
server {
listen 80;
server_name bootle.dev.waiswuaschd.de;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name bootle.dev.waiswuaschd.de;
# SSL Zertifikate hier konfigurieren (z. B. via certbot)
location /static/ {
alias /home/maxi/Projekt/bootle.waiswuaschd.de/app/static/;
add_header Cache-Control "no-cache, must-revalidate";
}
location / {
proxy_pass http://127.0.0.1:8002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Binary file not shown.
+4 -4
View File
@@ -58,11 +58,11 @@ ALL_ITEMS = {
}, },
"duel_badge": { "duel_badge": {
"id": "duel_badge", "id": "duel_badge",
"name": "Duellanten-Abzeichen", "name": "Duellanten-Abzeichen & Snapes Notiz",
"icon": "⚡", "icon": "⚡",
"desc": "Ein goldenes Abzeichen für meisterhafte Zauberstab-Gesten im Duellier-Club von Hogwarts.", "desc": "Ein goldenes Abzeichen mit Snapes handschriftlicher Rezeptur-Notiz für Felix Felicis auf der Rückseite.",
"action": "spark_wand", "action": "read_recipe",
"action_label": "Funkenregen sprühen ⚡" "action_label": "Snapes Rezeptur lesen 📜"
}, },
"felix_felicis": { "felix_felicis": {
"id": "felix_felicis", "id": "felix_felicis",
BIN
View File
Binary file not shown.
+453
View File
@@ -2233,3 +2233,456 @@ body {
} }
} }
/* ========================================================
SCREEN SHAKE & OBSTACLES ANIMATION
======================================================== */
@keyframes screenShake {
0% { transform: translate(0, 0) rotate(0deg); }
15% { transform: translate(-7px, 5px) rotate(-1.5deg); }
30% { transform: translate(6px, -6px) rotate(1.5deg); }
45% { transform: translate(-5px, -4px) rotate(-1deg); }
60% { transform: translate(4px, 4px) rotate(1deg); }
75% { transform: translate(-2px, 2px) rotate(-0.5deg); }
100% { transform: translate(0, 0) rotate(0deg); }
}
.screen-shake {
animation: screenShake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
/* ========================================================
ENHANCED CAULDRON: INGREDIENT DOSAGE & STIR METER (/brew)
======================================================== */
.dose-grid {
display: flex;
gap: 0.75rem;
width: 100%;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.dose-card {
background: rgba(25, 20, 35, 0.95);
border: 2px solid rgba(229, 185, 72, 0.4);
border-radius: 14px;
padding: 0.75rem 0.6rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
min-width: 95px;
flex: 1;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
transition: border-color 0.2s, transform 0.15s;
user-select: none;
-webkit-user-select: none;
}
.dose-card:hover {
border-color: var(--gold);
}
.dose-controls {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.2rem;
}
.dose-btn {
background: rgba(229, 185, 72, 0.18);
border: 1px solid var(--gold);
color: var(--gold);
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.15rem;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
touch-action: manipulation;
transition: all 0.15s;
}
.dose-btn:active {
background: var(--gold);
color: #120d08;
transform: scale(0.9);
}
.dose-val {
font-size: 1.35rem;
font-weight: bold;
color: #ffffff;
min-width: 22px;
text-align: center;
text-shadow: 0 0 6px var(--gold);
}
/* Two-Way Stirring & Gauge */
.stir-gauge-wrapper {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.65rem;
background: rgba(20, 16, 28, 0.92);
border: 2px solid var(--gold);
border-radius: 14px;
padding: 1rem 1.2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
}
.stir-track {
width: 100%;
height: 24px;
background: rgba(10, 8, 16, 0.95);
border: 2px solid #555;
border-radius: 12px;
position: relative;
overflow: hidden;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
}
.stir-target-zone {
position: absolute;
left: 40%;
width: 35%;
top: 0;
bottom: 0;
background: rgba(46, 204, 113, 0.35);
border-left: 2px dashed #2ecc71;
border-right: 2px dashed #2ecc71;
z-index: 1;
}
.stir-indicator-bar {
height: 100%;
width: 10%;
background: linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%);
border-radius: 10px;
transition: width 0.08s ease-out;
position: relative;
z-index: 2;
}
.stir-buttons-row {
display: flex;
gap: 0.75rem;
width: 100%;
max-width: 360px;
justify-content: center;
}
.stir-action-btn {
flex: 1;
padding: 0.85rem 0.6rem;
font-size: 1.05rem;
font-weight: bold;
font-family: inherit;
border-radius: 12px;
border: 2px solid rgba(229, 185, 72, 0.5);
background: linear-gradient(135deg, rgba(38, 28, 54, 0.95), rgba(18, 14, 26, 0.95));
color: var(--text-light);
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
transition: all 0.15s;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.stir-action-btn:active {
transform: scale(0.95);
background: var(--gold);
color: #120d08;
}
.stir-action-btn.active-target {
border-color: #2ecc71;
box-shadow: 0 0 16px rgba(46, 204, 113, 0.6);
color: #ffd700;
animation: targetPulse 1.2s infinite alternate;
}
@keyframes targetPulse {
0% { transform: scale(1); }
100% { transform: scale(1.03); }
}
/* ========================================================
SNAPE REPRIMAND MODAL (SARCASTIC QUOTES ON ERROR)
======================================================== */
.snape-reprimand-modal {
position: fixed;
inset: 0;
background: rgba(6, 4, 10, 0.94);
backdrop-filter: blur(8px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
opacity: 0;
pointer-events: none;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.snape-reprimand-modal.show {
opacity: 1;
pointer-events: auto;
visibility: visible;
}
.snape-card {
background: linear-gradient(135deg, #1d1526, #0e0a14);
border: 3px solid #8e44ad;
box-shadow: 0 0 45px rgba(142, 68, 173, 0.5), inset 0 0 30px rgba(0, 0, 0, 0.85);
border-radius: 18px;
max-width: 480px;
width: 100%;
padding: 1.8rem 1.4rem;
text-align: center;
transform: scale(0.9);
transition: transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
.snape-reprimand-modal.show .snape-card {
transform: scale(1);
}
.snape-avatar {
font-size: 3.8rem;
filter: drop-shadow(0 0 16px rgba(142, 68, 173, 0.9));
margin-bottom: 0.4rem;
}
.snape-quote-box {
background: rgba(0, 0, 0, 0.55);
border-left: 3px solid #8e44ad;
padding: 0.9rem 1.1rem;
margin: 1.1rem 0;
text-align: left;
font-family: 'Caveat', cursive;
font-size: 1.45rem;
color: #e8d8be;
line-height: 1.5;
border-radius: 0 10px 10px 0;
}
/* =========================================
HOGWARTS ENVELOPE & WAX SEAL INTRO (Bootle Escape)
========================================= */
.escape-letter-intro {
max-width: 600px;
margin: 0 auto 1.5rem;
}
.antique-envelope-scene {
position: relative;
width: min(460px, 92vw);
height: min(265px, 58vw);
margin: 20px auto 25px;
perspective: 1200px;
perspective-origin: center top;
transition: margin-top 0.8s cubic-bezier(0.25, 1, 0.5, 1);
}
.antique-envelope-scene.envelope-opened {
margin-top: 155px;
margin-bottom: 25px;
}
.envelope-base {
position: relative;
width: 100%;
height: 100%;
border-radius: 6px;
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.75), 0 4px 14px rgba(0, 0, 0, 0.5);
}
.envelope-interior {
position: absolute;
inset: 0;
border-radius: 6px;
background-color: #2b1d12;
background-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.65) 100%);
border: 2px solid #7a5c36;
z-index: 1;
}
.letter-sheet {
position: absolute;
top: 15px;
left: 4%;
width: 92%;
height: 390px;
max-height: 72vh;
background-color: #fcf8ee;
background-image: radial-gradient(ellipse at 50% 25%, rgba(255, 255, 255, 0.6) 0%, rgba(210, 180, 130, 0.25) 100%);
border-radius: 4px;
border: 1.5px solid #8e6c43;
padding: 16px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
z-index: 2;
visibility: hidden;
opacity: 0;
transform: translateY(50px) scale(0.92);
pointer-events: none;
transition: transform 0.9s cubic-bezier(0.2, 0.85, 0.25, 1) 0.35s, opacity 0.6s ease 0.35s, visibility 0.6s ease 0.35s;
overflow-y: auto;
text-align: center;
outline: 1px dashed rgba(130, 95, 55, 0.45);
outline-offset: -5px;
}
.envelope-back-body {
position: absolute;
inset: 0;
border-radius: 6px;
background-color: #ebd8ae;
background-image: radial-gradient(ellipse at 50% 60%, rgba(255, 255, 255, 0.22) 0%, rgba(130, 95, 45, 0.22) 100%);
border: 1.5px solid #8e6c43;
z-index: 3;
overflow: hidden;
}
.envelope-closing-flap.envelope-v-flap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #dfcd9f;
background-image: radial-gradient(ellipse at 50% 18%, rgba(255, 255, 255, 0.35) 0%, rgba(125, 90, 40, 0.28) 100%);
clip-path: polygon(0 0, 100% 0, 50% 56%);
transform-origin: top center;
transform-style: preserve-3d;
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 6;
border-top: 2px solid #7c5830;
filter: drop-shadow(0 7px 14px rgba(35, 18, 5, 0.65));
}
.envelope-opened .envelope-closing-flap.envelope-v-flap {
transform: rotateX(180deg);
z-index: 1;
}
.v-flap-sketch-line {
position: absolute;
inset: 4px;
clip-path: polygon(2% 2%, 98% 2%, 50% 53.5%);
border-top: 1.5px dashed rgba(110, 75, 35, 0.45);
pointer-events: none;
}
.sketched-wax-seal {
position: absolute;
top: 56%;
left: 50%;
transform: translate(-50%, -50%) rotate(-3deg);
width: 72px;
height: 72px;
border-radius: 50%;
cursor: pointer;
z-index: 7;
transition: transform 0.3s ease, opacity 0.4s ease;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.75);
}
.sketched-wax-seal:hover {
transform: translate(-50%, -50%) rotate(0deg) scale(1.08);
}
.seal-crest-img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
display: block;
border: 3px solid #4a0d0d;
}
.envelope-opened .sketched-wax-seal {
opacity: 0;
pointer-events: none;
transform: translate(-50%, -50%) scale(0.3);
}
.envelope-opened .letter-sheet {
visibility: visible;
opacity: 1;
pointer-events: auto;
transform: translateY(-140px) scale(1.02);
z-index: 25;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.85), 0 0 35px rgba(201, 168, 106, 0.4);
}
.letter-crest-box {
width: 46px;
height: 46px;
margin: 0 auto 4px;
border-radius: 50%;
overflow: hidden;
border: 1px solid rgba(139, 105, 60, 0.3);
}
.letter-crest-sketch {
width: 100%;
height: 100%;
object-fit: cover;
filter: sepia(0.65) contrast(1.15) brightness(0.92);
}
.letter-school-name {
font-family: 'MedievalSharp', Georgia, serif;
font-size: 1.05rem;
color: #2c1a0c;
text-transform: uppercase;
margin-bottom: 2px;
}
.letter-headmaster {
font-family: 'Caveat', cursive;
font-size: 0.92rem;
color: #4a331c;
margin-bottom: 6px;
}
.letter-ornament-line {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
color: #8c6838;
margin: 4px auto 8px;
max-width: 240px;
}
.ornament-rule {
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, #8c6838, transparent);
}
.letter-body-handwritten {
font-family: 'Marck Script', 'Caveat', cursive;
color: #0f3d24;
text-align: left;
line-height: 1.48;
}
.letter-salutation {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 6px;
}
.letter-paragraph {
font-size: 1.05rem;
margin-bottom: 8px;
text-indent: 1em;
}
.letter-signature-block {
margin-top: 8px;
text-align: right;
font-family: 'Marck Script', 'Caveat', cursive;
color: #0f3d24;
}
.letter-signature-ink {
font-size: 1.45rem;
font-weight: 700;
}
.letter-sign-title {
font-size: 0.85rem;
color: #4a6352;
}
+15 -1
View File
@@ -144,9 +144,18 @@ window.soundFX = new SoundFX();
function updateAudioButtonUI() { function updateAudioButtonUI() {
const btn = document.getElementById('audioToggleBtn'); const btn = document.getElementById('audioToggleBtn');
const bgm = document.getElementById('bgm');
if (!btn) return; if (!btn) return;
const isMuted = window.soundFX.isMuted(); const isMuted = window.soundFX.isMuted();
btn.innerHTML = isMuted ? '🔇 <span class="audio-lbl">Ton: Aus</span>' : '🔊 <span class="audio-lbl">Ton: An</span>'; btn.innerHTML = isMuted ? '🔇 <span class="audio-lbl">Musik & Ton: Aus</span>' : '🔊 <span class="audio-lbl">Musik & Ton: An</span>';
if (bgm) {
bgm.volume = 0.45;
if (isMuted && !bgm.paused) {
bgm.pause();
} else if (!isMuted && bgm.paused) {
bgm.play().catch(() => {});
}
}
} }
function toggleSound() { function toggleSound() {
@@ -158,5 +167,10 @@ document.addEventListener('DOMContentLoaded', () => {
updateAudioButtonUI(); updateAudioButtonUI();
document.body.addEventListener('click', () => { document.body.addEventListener('click', () => {
window.soundFX.init(); window.soundFX.init();
const bgm = document.getElementById('bgm');
if (bgm && !window.soundFX.isMuted() && bgm.paused) {
bgm.volume = 0.45;
bgm.play().catch(() => {});
}
}, { once: true }); }, { once: true });
}); });
+267 -94
View File
@@ -1,15 +1,17 @@
// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine - SVG Overhaul) // Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine: Riddles, Two-Way Stirring & Snape's Wrath)
(function() { (function() {
let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done let currentStep = 1; // 1: Dosage, 2: Stirring, 3: Heat, 4: Done
const ingredients = {
// Step 1: Ingredient Doses
const doses = {
dragon: 0, dragon: 0,
mandrake: 0, mandrake: 0,
star: 0 star: 0
}; };
const targetIngredients = { const targetDoses = {
dragon: 2, dragon: 3, // Fluffy's 3 heads
mandrake: 1, mandrake: 2, // Harry's companions Ron & Hermione
star: 1 star: 1 // The Alchemist's 1 ring
}; };
// DOM Elements // DOM Elements
@@ -17,7 +19,9 @@
const stepInstruction = document.getElementById('stepInstruction'); const stepInstruction = document.getElementById('stepInstruction');
const stepSubtext = document.getElementById('stepSubtext'); const stepSubtext = document.getElementById('stepSubtext');
const step1Wrapper = document.getElementById('step1Wrapper'); const step1Wrapper = document.getElementById('step1Wrapper');
const totalIngredientsCount = document.getElementById('totalIngredientsCount'); const doseValDragon = document.getElementById('doseValDragon');
const doseValMandrake = document.getElementById('doseValMandrake');
const doseValStar = document.getElementById('doseValStar');
// SVG Elements // SVG Elements
const gradStop1 = document.getElementById('gradStop1'); const gradStop1 = document.getElementById('gradStop1');
@@ -27,121 +31,275 @@
const stirSpoon = document.getElementById('stirSpoon'); const stirSpoon = document.getElementById('stirSpoon');
const dropItemAnim = document.getElementById('dropItemAnim'); const dropItemAnim = document.getElementById('dropItemAnim');
// Controls // Step 2: Stirring Controls
const stirPanel = document.getElementById('stirPanel'); const stirPanel = document.getElementById('stirPanel');
const stirCountSpan = document.getElementById('stirCount'); const stirPhaseLabel = document.getElementById('stirPhaseLabel');
const stirBtn = document.getElementById('stirBtn'); const stirIndicatorBar = document.getElementById('stirIndicatorBar');
const stirTimerVal = document.getElementById('stirTimerVal');
const btnStirLeft = document.getElementById('btnStirLeft');
const btnStirRight = document.getElementById('btnStirRight');
// Step 3: Heat Controls
const heatPanel = document.getElementById('heatPanel'); const heatPanel = document.getElementById('heatPanel');
const tempIndicator = document.getElementById('tempIndicator'); const tempIndicator = document.getElementById('tempIndicator');
const heatTimerSpan = document.getElementById('heatTimer'); const heatTimerSpan = document.getElementById('heatTimer');
const bellowsBtn = document.getElementById('bellowsBtn');
// Success & Modals
const successPanel = document.getElementById('successPanel'); const successPanel = document.getElementById('successPanel');
const snapeModal = document.getElementById('snapeReprimandModal');
const snapeText = document.getElementById('snapeReprimandText');
const riddleModal = document.getElementById('recipeRiddleModal');
// Step 1: Add Ingredient // Snape Insult Repository
window.addIngredient = function(type) { const snapeQuotes = [
if (currentStep !== 1) return; "»Kläglich. Sogar ein Bergtroll mit verbundenen Augen hätte die Mengenangaben treffender ergründet.«",
if (ingredients[type] >= targetIngredients[type]) return; "»Offenbar mangelt es Ihnen nicht nur an magischem Talent, sondern auch an elementarem Leseverständnis.«",
"»Ein weiterer solcher Fehltritt, und dieser Kessel fliegt Ihnen um die Ohren wie Longbottoms letzte Missetat.«",
"»Haben Sie Ihre Brille verlegt, oder ist das schlichte Ignoranz gegenüber den feinsten Künsten des Zaubertrankbrauens?«",
"»3 Drachenblut, 2 Alraunen, 1 Sternenstaub – ist das für Ihr Denkvermögen tatsächlich zu hoch gegriffen?«"
];
ingredients[type]++; function getRandomSnapeQuote() {
if (window.soundFX) window.soundFX.playBottleClink(); return snapeQuotes[Math.floor(Math.random() * snapeQuotes.length)];
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(30); }
// Update UI Badge // Modal helpers
if (type === 'dragon') { window.openRecipeRiddleModal = function() {
document.getElementById('countDragon').innerText = `${ingredients.dragon} / ${targetIngredients.dragon}`; if (riddleModal) riddleModal.classList.add('show');
if (ingredients.dragon >= targetIngredients.dragon) document.getElementById('ingDragon').classList.add('done'); if (window.soundFX) window.soundFX.playStoneSlide();
} else if (type === 'mandrake') {
document.getElementById('countMandrake').innerText = `${ingredients.mandrake} / ${targetIngredients.mandrake}`;
if (ingredients.mandrake >= targetIngredients.mandrake) document.getElementById('ingMandrake').classList.add('done');
} else if (type === 'star') {
document.getElementById('countStar').innerText = `${ingredients.star} / ${targetIngredients.star}`;
if (ingredients.star >= targetIngredients.star) document.getElementById('ingStar').classList.add('done');
}
const total = ingredients.dragon + ingredients.mandrake + ingredients.star;
if (totalIngredientsCount) totalIngredientsCount.innerText = total;
// Visual drop animation in SVG
if (dropItemAnim) {
const icons = { dragon: '🐉', mandrake: '🌿', star: '✨' };
dropItemAnim.textContent = icons[type];
dropItemAnim.style.display = 'block';
dropItemAnim.style.animation = 'ingredientDropAnim 0.5s ease-out forwards';
setTimeout(() => {
dropItemAnim.style.display = 'none';
}, 550);
}
// Ripple pulse on liquid
if (liquidSurface) {
liquidSurface.style.transform = 'scale(1.06)';
setTimeout(() => liquidSurface.style.transform = 'scale(1)', 200);
}
// Check if all ingredients added
if (total === 4) {
setTimeout(startStep2, 600);
}
}; };
// Step 2: Stirring window.closeRecipeRiddleModal = function() {
let completedStirs = 0; if (riddleModal) riddleModal.classList.remove('show');
if (window.soundFX) window.soundFX.playStoneSlide();
};
window.triggerSnapeInsult = function(customQuote) {
if (snapeText) snapeText.innerText = customQuote || getRandomSnapeQuote();
if (snapeModal) snapeModal.classList.add('show');
// Turn cauldron murky black with foul smoke
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#2d2d2d');
gradStop2.setAttribute('stop-color', '#1a1a1a');
gradStop3.setAttribute('stop-color', '#0d0d0d');
}
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([60, 40, 100]);
};
window.closeSnapeModal = function() {
if (snapeModal) snapeModal.classList.remove('show');
// Reset liquid back to clean starting emerald green
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#2ecc71');
gradStop2.setAttribute('stop-color', '#16a085');
gradStop3.setAttribute('stop-color', '#0b3b30');
}
if (window.soundFX) window.soundFX.playStoneSlide();
};
// Step 1: Ingredient Dosage Adjustments
window.adjustDose = function(type, delta) {
if (currentStep !== 1) return;
doses[type] = Math.max(0, Math.min(9, doses[type] + delta));
if (type === 'dragon' && doseValDragon) doseValDragon.innerText = doses.dragon;
if (type === 'mandrake' && doseValMandrake) doseValMandrake.innerText = doses.mandrake;
if (type === 'star' && doseValStar) doseValStar.innerText = doses.star;
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(25);
};
window.verifyAndMixIngredients = function() {
if (currentStep !== 1) return;
// Check if correct: 3 Dragon, 2 Mandrake, 1 Star
const isCorrect = (doses.dragon === targetDoses.dragon &&
doses.mandrake === targetDoses.mandrake &&
doses.star === targetDoses.star);
if (!isCorrect) {
// Wrong dosage! Trigger Snape's rebuke!
window.triggerSnapeInsult();
return;
}
// Correct! Play dropping animations and proceed to Step 2
if (dropItemAnim) {
dropItemAnim.textContent = '🐉🌿✨';
dropItemAnim.style.display = 'block';
dropItemAnim.style.animation = 'ingredientDropAnim 0.7s ease-out forwards';
setTimeout(() => { dropItemAnim.style.display = 'none'; }, 750);
}
if (liquidSurface) {
liquidSurface.style.transform = 'scale(1.08)';
setTimeout(() => liquidSurface.style.transform = 'scale(1)', 300);
}
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 80]);
setTimeout(startStep2, 700);
};
// Step 2: Two-Way Stirring with Tempo Bar
let stirPhase = 'left'; // 'left' then 'right'
let phaseTimer = 0.0;
let stirTempo = 25; // 0 to 100%
let stirLoopInterval = null;
let wildTempoTime = 0.0;
function startStep2() { function startStep2() {
currentStep = 2; currentStep = 2;
stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; stirPhase = 'left';
stepInstruction.innerText = "Rühre den Kessel 3-mal um!"; phaseTimer = 0.0;
stepSubtext.innerText = "Tippe auf den großen Rühr-Button, um die Essenzen zu vermengen."; stirTempo = 25;
wildTempoTime = 0.0;
stepBadge.innerText = "Schritt 2: Rhythmisches Rühren";
stepInstruction.innerText = "Rühre den Sud gleichmäßig im Takt!";
stepSubtext.innerText = "Laut Rezept: Zuerst 3 Sekunden LINKS (↺), danach 3 Sekunden RECHTS (↻)!";
if (step1Wrapper) step1Wrapper.style.display = 'none'; if (step1Wrapper) step1Wrapper.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'flex'; if (stirPanel) stirPanel.style.display = 'flex';
// Show wooden spoon in cauldron
if (stirSpoon) stirSpoon.style.display = 'block'; if (stirSpoon) stirSpoon.style.display = 'block';
// Shift liquid color to mystical purple // Set liquid to rich amethyst purple
if (gradStop1 && gradStop2 && gradStop3) { if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#9b59b6'); gradStop1.setAttribute('stop-color', '#9b59b6');
gradStop2.setAttribute('stop-color', '#8e44ad'); gradStop2.setAttribute('stop-color', '#8e44ad');
gradStop3.setAttribute('stop-color', '#341f47'); gradStop3.setAttribute('stop-color', '#341f47');
} }
updateStirUI();
if (window.soundFX) window.soundFX.playStoneSlide(); if (window.soundFX) window.soundFX.playStoneSlide();
// Start tempo decay loop
if (stirLoopInterval) clearInterval(stirLoopInterval);
stirLoopInterval = setInterval(updateStirLoop, 50);
} }
window.doStir = function() { function updateStirUI() {
if (stirPhaseLabel) {
if (stirPhase === 'left') {
stirPhaseLabel.innerText = "↺ LINKS (Phase 1)";
stirPhaseLabel.style.color = "#4cc9f0";
if (btnStirLeft) btnStirLeft.classList.add('active-target');
if (btnStirRight) btnStirRight.classList.remove('active-target');
} else {
stirPhaseLabel.innerText = "↻ RECHTS (Phase 2)";
stirPhaseLabel.style.color = "#ffd700";
if (btnStirLeft) btnStirLeft.classList.remove('active-target');
if (btnStirRight) btnStirRight.classList.add('active-target');
}
}
if (stirTimerVal) stirTimerVal.innerText = phaseTimer.toFixed(1);
}
window.stirTap = function(direction) {
if (currentStep !== 2) return; if (currentStep !== 2) return;
completedStirs++;
if (stirCountSpan) stirCountSpan.innerText = completedStirs;
if (stirBtn) stirBtn.innerHTML = `🥄 Kessel umrühren! (<span>${completedStirs}</span> / 3)`;
if (window.soundFX) window.soundFX.playBottleClink(); // Check if wrong direction tapped!
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(35); if (direction !== stirPhase) {
const wrongDirQuote = (stirPhase === 'left') ?
// Animate stirring spoon "»Linksherum, sagte ich! Verwechseln Sie die Drehrichtungen wie Mr. Finnigan?«" :
if (stirSpoon) { "»Nun rechtsherum! Hören Sie überhaupt zu oder träumen Sie von Schokofröschen?«";
stirSpoon.classList.remove('stirring-active'); window.triggerSnapeInsult(wrongDirQuote);
void stirSpoon.offsetWidth; // re-flow phaseTimer = 0.0;
stirSpoon.classList.add('stirring-active'); stirTempo = 25;
updateStirUI();
return;
} }
if (completedStirs >= 3) { // Correct direction tap: give a rhythmic boost
setTimeout(() => { stirTempo = Math.min(100, stirTempo + 13);
if (stirSpoon) stirSpoon.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'none'; if (window.soundFX) window.soundFX.playBottleClink();
startStep3(); if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20);
}, 600);
// Animate spoon
if (stirSpoon) {
stirSpoon.classList.remove('stirring-active');
void stirSpoon.offsetWidth;
stirSpoon.classList.add('stirring-active');
} }
}; };
// Step 3: Heat Control function updateStirLoop() {
let currentTemp = 35; // percent (0-100) if (currentStep !== 2) return;
// Continuous tempo decay
stirTempo = Math.max(0, stirTempo - 1.1);
if (stirIndicatorBar) stirIndicatorBar.style.width = stirTempo + '%';
// Check if in green zone (40% to 75%)
if (stirTempo >= 40 && stirTempo <= 75) {
phaseTimer += 0.05;
wildTempoTime = 0.0;
if (stirIndicatorBar) stirIndicatorBar.style.background = '#2ecc71';
if (stirTimerVal) stirTimerVal.innerText = phaseTimer.toFixed(1);
} else if (stirTempo > 82) {
// Too frantic!
if (stirIndicatorBar) stirIndicatorBar.style.background = '#e74c3c';
wildTempoTime += 0.05;
if (wildTempoTime > 1.2) {
wildTempoTime = 0.0;
stirTempo = 20;
phaseTimer = 0.0;
window.triggerSnapeInsult("»Mäßigen Sie Ihr ungestümes Tempo! Sie schlagen Schaum statt zu rühren! Das ist kein Pfannkuchenteig!«");
updateStirUI();
return;
}
} else {
// Too slow (blue/cold)
if (stirIndicatorBar) stirIndicatorBar.style.background = 'linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%)';
wildTempoTime = 0.0;
}
// Phase completion checks (3.0 seconds needed each)
if (phaseTimer >= 3.0) {
if (stirPhase === 'left') {
// Switch to Phase 2: Right
stirPhase = 'right';
phaseTimer = 0.0;
stirTempo = 30;
updateStirUI();
if (window.soundFX) window.soundFX.playKeyChime();
if (window.showFloatingToast) {
window.showFloatingToast("✨ Hervorragend! Jetzt 3 Sekunden gleichmäßig RECHTS (↻) rühren!");
}
} else if (stirPhase === 'right') {
// Step 2 Complete!
clearInterval(stirLoopInterval);
stirLoopInterval = null;
if (stirSpoon) stirSpoon.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'none';
startStep3();
}
}
}
// Step 3: Heat Control (Blasebalg) with Overheat Insults
let currentTemp = 35;
let heatTimer = 0.0; let heatTimer = 0.0;
let heatInterval = null; let heatInterval = null;
let overheatSeconds = 0.0;
function startStep3() { function startStep3() {
currentStep = 3; currentStep = 3;
heatTimer = 0.0;
currentTemp = 35;
overheatSeconds = 0.0;
stepBadge.innerText = "Schritt 3: Hitze balancieren"; stepBadge.innerText = "Schritt 3: Hitze balancieren";
stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Flammen zu nähren!"; stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Flammen zu nähren!";
stepSubtext.innerText = "Halte die Anzeige für 3 Sekunden im grünen Idealbereich (45% - 85%)."; stepSubtext.innerText = "Halte die Anzeige für 3 Sekunden im grünen Idealbereich (45% - 85%). Nicht überhitzen!";
if (heatPanel) heatPanel.classList.add('active'); if (heatPanel) heatPanel.classList.add('active');
// Shift liquid color to fiery orange // Shift liquid color to fiery orange
@@ -152,7 +310,7 @@
} }
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (heatInterval) clearInterval(heatInterval);
heatInterval = setInterval(updateHeatLoop, 50); heatInterval = setInterval(updateHeatLoop, 50);
} }
@@ -162,21 +320,35 @@
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20);
const bellows = document.getElementById('bellowsBtn'); if (bellowsBtn) {
if (bellows) { bellowsBtn.style.transform = 'scale(0.92)';
bellows.style.transform = 'scale(0.92)'; setTimeout(() => bellowsBtn.style.transform = 'scale(1)', 100);
setTimeout(() => bellows.style.transform = 'scale(1)', 100);
} }
}; };
function updateHeatLoop() { function updateHeatLoop() {
if (currentStep !== 3) return; if (currentStep !== 3) return;
// Gentle cooling down // Cooling decay
currentTemp = Math.max(0, currentTemp - 0.45); currentTemp = Math.max(0, currentTemp - 0.45);
if (tempIndicator) tempIndicator.style.width = currentTemp + '%'; if (tempIndicator) tempIndicator.style.width = currentTemp + '%';
// Check ideal zone (45% to 85%) - forgiving on mobile! // Check overheat condition
if (currentTemp > 92) {
overheatSeconds += 0.05;
if (overheatSeconds > 1.0) {
overheatSeconds = 0.0;
currentTemp = 35;
heatTimer = 0.0;
if (heatTimerSpan) heatTimerSpan.innerText = "0.0";
window.triggerSnapeInsult("»Kesselüberhitzung! Sie kochen die kostbaren Essenzen zu Schlacke! Ein wenig mehr Feingefühl mit dem Blasebalg!«");
return;
}
} else {
overheatSeconds = 0.0;
}
// Check ideal zone (45% to 85%)
if (currentTemp >= 45 && currentTemp <= 85) { if (currentTemp >= 45 && currentTemp <= 85) {
heatTimer += 0.05; heatTimer += 0.05;
if (heatTimerSpan) heatTimerSpan.innerText = heatTimer.toFixed(1); if (heatTimerSpan) heatTimerSpan.innerText = heatTimer.toFixed(1);
@@ -187,6 +359,7 @@
if (heatTimer >= 3.0) { if (heatTimer >= 3.0) {
clearInterval(heatInterval); clearInterval(heatInterval);
heatInterval = null;
finishPotionBrew(); finishPotionBrew();
} }
} }
@@ -200,11 +373,11 @@
stepBadge.style.color = "#2ecc71"; stepBadge.style.color = "#2ecc71";
stepInstruction.innerText = "Felix Felicis strahlt in reinem Gold!"; stepInstruction.innerText = "Felix Felicis strahlt in reinem Gold!";
stepSubtext.innerText = "Dein Zaubertrank ist meisterhaft gelungen."; stepSubtext.innerText = "Dein Zaubertrank ist meisterhaft gelungen. Selbst Snape nickt widerwillig!";
if (heatPanel) heatPanel.classList.remove('active'); if (heatPanel) heatPanel.classList.remove('active');
// Turn liquid into sparkling golden elixir // Turn liquid into radiant shimmering gold
if (gradStop1 && gradStop2 && gradStop3) { if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#fff6a5'); gradStop1.setAttribute('stop-color', '#fff6a5');
gradStop2.setAttribute('stop-color', '#ffd700'); gradStop2.setAttribute('stop-color', '#ffd700');
@@ -239,9 +412,9 @@
window.showChamberVictory({ window.showChamberVictory({
chamberBadge: "Kammer III: Die Brauküche", chamberBadge: "Kammer III: Die Brauküche",
title: "⚗️ Zaubertrank Gemeistert!", title: "⚗️ Zaubertrank Gemeistert!",
statsHtml: `<span>🧪 Elixier: <strong>Felix Felicis</strong></span><span>🌡️ Hitze: <strong>Perfekt gemeistert</strong></span>`, statsHtml: `<span>🧪 Elixier: <strong>Felix Felicis</strong></span><span>🥄 Rühren: <strong>2 Phasen perfekt</strong></span><span>🌡️ Hitze: <strong>Meisterlich</strong></span>`,
item: newItem, item: newItem,
storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren <strong>Felix Felicis</strong> ab. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige <strong>Runen-Drehschloss der vier Gründer</strong>!", storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren <strong>Felix Felicis</strong> ab. Selbst Severus Snape quittiert dein Werk mit einem knappen, anerkennenden Nicken. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige <strong>Runen-Drehschloss der vier Gründer</strong>!",
nextUrl: "/cryptex", nextUrl: "/cryptex",
nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐" nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐"
}); });
+235 -6
View File
@@ -9,7 +9,11 @@
const timerVal = document.getElementById('timerVal'); const timerVal = document.getElementById('timerVal');
const ringsVal = document.getElementById('ringsVal'); const ringsVal = document.getElementById('ringsVal');
const comboVal = document.getElementById('comboVal'); const comboVal = document.getElementById('comboVal');
const livesVal = document.getElementById('livesVal');
const statusMsg = document.getElementById('statusMsg'); const statusMsg = document.getElementById('statusMsg');
const canvasWrapper = document.getElementById('canvasWrapper');
const broomCrashOverlay = document.getElementById('broomCrashOverlay');
const restartFlightBtn = document.getElementById('restartFlightBtn');
const modalBackdrop = document.getElementById('modalBackdrop'); const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText'); const modalText = document.getElementById('modalText');
@@ -32,15 +36,28 @@
let score = 0; let score = 0;
let ringsCollected = 0; let ringsCollected = 0;
let combo = 0; let combo = 0;
let lives = 3;
function updateLivesDisplay() {
if (!livesVal) return;
livesVal.innerText = '💚'.repeat(Math.max(0, lives)) + '🖤'.repeat(Math.max(0, 3 - lives));
}
window.startFlight = function() { window.startFlight = function() {
const overlay = document.getElementById('broomStartOverlay'); const startOverlay = document.getElementById('broomStartOverlay');
if (overlay) { if (startOverlay) {
overlay.classList.add('hidden'); startOverlay.classList.add('hidden');
overlay.style.display = 'none'; startOverlay.style.display = 'none';
}
if (broomCrashOverlay) {
broomCrashOverlay.classList.add('hidden');
broomCrashOverlay.style.display = 'none';
} }
rings.length = 0; rings.length = 0;
obstacles.length = 0;
particles.length = 0;
rings.push({ x: width + 60, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); rings.push({ x: width + 60, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 });
rings.push({ x: width + 290, y: height * 0.65, radius: 25, speed: 3.6, golden: true, collected: false, scale: 1, alpha: 1, pulse: 0 }); rings.push({ x: width + 290, y: height * 0.65, radius: 25, speed: 3.6, golden: true, collected: false, scale: 1, alpha: 1, pulse: 0 });
rings.push({ x: width + 530, y: height * 0.3, radius: 25, speed: 3.7, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); rings.push({ x: width + 530, y: height * 0.3, radius: 25, speed: 3.7, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 });
@@ -48,19 +65,24 @@
score = 0; score = 0;
ringsCollected = 0; ringsCollected = 0;
combo = 0; combo = 0;
lives = 3;
player.invulnerableTimer = 0;
updateLivesDisplay();
if (scoreVal) scoreVal.innerText = score; if (scoreVal) scoreVal.innerText = score;
if (ringsVal) ringsVal.innerText = `0/${TOTAL_RINGS_NEEDED}`; if (ringsVal) ringsVal.innerText = `0/${TOTAL_RINGS_NEEDED}`;
if (comboVal) comboVal.innerText = ''; if (comboVal) comboVal.innerText = '';
startTime = Date.now(); startTime = Date.now();
lastRingSpawn = Date.now(); lastRingSpawn = Date.now();
lastObstacleSpawn = Date.now() + 1200;
gameRunning = true; gameRunning = true;
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(40); if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(40);
}; };
// Explicitly bind click and pointerdown to start button for instant mobile activation // Explicitly bind click and pointerdown to start buttons for instant mobile activation
const startFlightBtn = document.getElementById('startFlightBtn'); const startFlightBtn = document.getElementById('startFlightBtn');
if (startFlightBtn) { if (startFlightBtn) {
const onStartTrigger = (e) => { const onStartTrigger = (e) => {
@@ -74,6 +96,18 @@
startFlightBtn.addEventListener('pointerdown', onStartTrigger); startFlightBtn.addEventListener('pointerdown', onStartTrigger);
} }
if (restartFlightBtn) {
const onRestartTrigger = (e) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
window.startFlight();
};
restartFlightBtn.addEventListener('click', onRestartTrigger);
restartFlightBtn.addEventListener('pointerdown', onRestartTrigger);
}
// Player (Broomstick) // Player (Broomstick)
const player = { const player = {
x: 90, x: 90,
@@ -85,6 +119,8 @@
speed: 6.5, speed: 6.5,
boost: 1.0, boost: 1.0,
angle: 0, angle: 0,
invulnerableTimer: 0,
hitRadius: 18,
trail: [] trail: []
}; };
@@ -109,6 +145,39 @@
}); });
} }
// Dangerous Flying Obstacles (Klatscher 💣 & Felsen 🪨)
const obstacles = [];
let lastObstacleSpawn = Date.now();
function spawnObstacle() {
const obsY = Math.random() * (height - 120) + 60;
const isKlatscher = Math.random() < 0.65;
if (isKlatscher) {
obstacles.push({
type: 'klatscher',
x: width + 50,
y: obsY,
baseY: obsY,
radius: 17,
speed: Math.random() * 1.6 + 4.0,
wobblePhase: Math.random() * Math.PI * 2,
wobbleAmp: Math.random() * 32 + 20,
wobbleFreq: 0.005,
angle: 0
});
} else {
obstacles.push({
type: 'rock',
x: width + 50,
y: obsY,
radius: Math.random() * 5 + 20,
speed: Math.random() * 1.2 + 3.0,
angle: 0,
rotSpeed: (Math.random() - 0.5) * 0.03
});
}
}
// Sparkles & Confetti particles // Sparkles & Confetti particles
const particles = []; const particles = [];
function spawnSparkles(x, y, color) { function spawnSparkles(x, y, color) {
@@ -264,6 +333,17 @@
lastRingSpawn = Date.now(); lastRingSpawn = Date.now();
} }
// Spawn obstacles continuously
if (Date.now() - lastObstacleSpawn > 2200 && obstacles.length < 4) {
spawnObstacle();
lastObstacleSpawn = Date.now();
}
// Countdown invulnerability timer
if (player.invulnerableTimer > 0) {
player.invulnerableTimer--;
}
// Steer player // Steer player
let ax = 0, ay = 0; let ax = 0, ay = 0;
if (keysDown['arrowup'] || keysDown['w']) ay -= 1; if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
@@ -299,6 +379,71 @@
if (player.trail.length > 20) player.trail.shift(); if (player.trail.length > 20) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; }); player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; });
// Update Obstacles (Klatscher & Schwebefelsen)
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.x -= obs.speed * (player.boost > 1.2 ? 1.35 : 1.0);
if (obs.type === 'klatscher') {
obs.y = obs.baseY + Math.sin(Date.now() * obs.wobbleFreq + obs.wobblePhase) * obs.wobbleAmp;
obs.angle += 0.12;
} else {
obs.angle += obs.rotSpeed;
}
// Collision detection with player
const dist = Math.hypot(player.x - obs.x, player.y - obs.y);
if (dist < player.hitRadius + obs.radius) {
if (player.invulnerableTimer <= 0) {
lives--;
player.invulnerableTimer = 90; // ~1.5s invulnerability
score = Math.max(0, score - 100);
combo = 0;
if (scoreVal) scoreVal.innerText = score;
if (comboVal) comboVal.innerText = '';
updateLivesDisplay();
// Screen shake animation
if (canvasWrapper) {
canvasWrapper.classList.remove('screen-shake');
void canvasWrapper.offsetWidth;
canvasWrapper.classList.add('screen-shake');
setTimeout(() => canvasWrapper.classList.remove('screen-shake'), 450);
}
// Sparks & explosion particles
spawnSparkles(obs.x, obs.y, obs.type === 'klatscher' ? '#e74c3c' : '#bdc3c7');
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([80, 50, 80]);
if (window.showFloatingToast) {
window.showFloatingToast(obs.type === 'klatscher' ?
"💥 <strong>Klatscher-Treffer!</strong> Besen-Zustand beschädigt! (-100 Pkt)" :
"🪨 <strong>Felsen gerammt!</strong> Besen-Stabilität verloren! (-100 Pkt)");
}
obstacles.splice(i, 1);
// Check Game Over
if (lives <= 0) {
gameRunning = false;
if (broomCrashOverlay) {
broomCrashOverlay.classList.remove('hidden');
broomCrashOverlay.style.display = 'flex';
}
if (window.soundFX) window.soundFX.playWhoosh();
return;
}
continue;
}
}
// Remove off-screen obstacles
if (obs.x < -60) {
obstacles.splice(i, 1);
}
}
// Update rings // Update rings
for (let i = rings.length - 1; i >= 0; i--) { for (let i = rings.length - 1; i >= 0; i--) {
const r = rings[i]; const r = rings[i];
@@ -445,6 +590,85 @@
ctx.restore(); ctx.restore();
}); });
// Render Obstacles (Klatscher 💣 & Schwebesteine 🪨)
obstacles.forEach(obs => {
ctx.save();
ctx.translate(obs.x, obs.y);
ctx.rotate(obs.angle);
if (obs.type === 'klatscher') {
// Sinister pulsing red danger glow
ctx.shadowColor = '#e74c3c';
ctx.shadowBlur = 14 + Math.sin(Date.now() * 0.01) * 6;
// Dark iron sphere
const ironGrad = ctx.createRadialGradient(-3, -3, 2, 0, 0, obs.radius);
ironGrad.addColorStop(0, '#555555');
ironGrad.addColorStop(0.6, '#222222');
ironGrad.addColorStop(1, '#0a0a0a');
ctx.fillStyle = ironGrad;
ctx.beginPath();
ctx.arc(0, 0, obs.radius, 0, Math.PI * 2);
ctx.fill();
// Spiked / ribbed metal bands
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(0, 0, obs.radius - 2, 0, Math.PI * 2);
ctx.stroke();
// Glowing core / demonic red eye
ctx.fillStyle = '#ff3333';
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.fill();
// Fiery sparks trail
if (Math.random() < 0.3) {
particles.push({
x: obs.x + (Math.random() * 6 - 3),
y: obs.y + (Math.random() * 6 - 3),
vx: -(Math.random() * 2 + 1.5),
vy: (Math.random() - 0.5) * 1.5,
color: '#e74c3c',
size: Math.random() * 3 + 1,
alpha: 0.7
});
}
} else {
// Craggy Floating Castle Stone
ctx.shadowColor = '#000000';
ctx.shadowBlur = 8;
ctx.fillStyle = '#453f3a';
ctx.beginPath();
const r = obs.radius;
ctx.moveTo(r, 0);
ctx.lineTo(r * 0.7, r * 0.7);
ctx.lineTo(0, r * 0.95);
ctx.lineTo(-r * 0.75, r * 0.6);
ctx.lineTo(-r, 0);
ctx.lineTo(-r * 0.7, -r * 0.75);
ctx.lineTo(0, -r);
ctx.lineTo(r * 0.65, -r * 0.6);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#73685e';
ctx.lineWidth = 2;
ctx.stroke();
// Crater texture
ctx.fillStyle = '#26221f';
ctx.beginPath();
ctx.arc(-3, -2, r * 0.32, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
});
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// Render Sparkles // Render Sparkles
@@ -457,7 +681,12 @@
}); });
ctx.globalAlpha = 1.0; ctx.globalAlpha = 1.0;
// Player (Broom & Wizard) // Player (Broom & Wizard) - blink when invulnerable
if (player.invulnerableTimer > 0 && Math.floor(Date.now() / 90) % 2 === 0) {
// Invisible blinking frame during invulnerability
return;
}
ctx.save(); ctx.save();
ctx.translate(player.x, player.y); ctx.translate(player.x, player.y);
ctx.rotate(player.angle); ctx.rotate(player.angle);
+27
View File
@@ -159,6 +159,33 @@
} else if (action === 'spark_wand') { } else if (action === 'spark_wand') {
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
showFloatingToast("⚡ Magische Funken sprühen aus deiner Zauberstabspitze!"); showFloatingToast("⚡ Magische Funken sprühen aus deiner Zauberstabspitze!");
} else if (action === 'read_recipe') {
window.closePlayerDrawer();
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
const modalIcon = document.getElementById('modalIcon');
if (modalBackdrop) {
if (modalIcon) modalIcon.innerText = "📜";
if (modalTitle) modalTitle.innerText = "Snapes Felix-Felicis-Rezeptur";
if (modalText) {
modalText.innerHTML = `
<div style="font-family: 'Caveat', cursive; font-size: 1.35rem; line-height: 1.7; text-align: left; max-height: 52vh; overflow-y: auto; padding: 0.5rem 1rem;">
<em>»Felix Felicis ist kein Trank für Scharlatane. Wer die Maße missachtet, verdirbt den Kessel augenblicklich:«</em><br><br>
<strong>🐉 Drachenblut:</strong> <em>"So viele Tropfen wie Köpfe des schlafenden Fluffy (3 Tropfen)"</em><br>
<strong>🌿 Alraunenblatt:</strong> <em>"Harrys treue Gefährten am Gryffindor-Tisch (Ron & Hermine = 2 Blätter)"</em><br>
<strong>✨ Sternenstaub:</strong> <em>"Der eine Siegelring des Alchemisten (1 Prise)"</em><br><br>
<strong>🥄 Rühr-Anweisung:</strong> <em>"Zuerst 3 Sekunden gleichmäßig linksherum rühren (↺), danach 3 Sekunden rechtsherum (↻)! Ein Hektiker schlägt Schaum, ein Zauderer lässt den Sud erstarren!"</em>
</div>
`;
}
if (modalBtn) {
modalBtn.innerText = "Rezeptur einprägen ➔";
modalBtn.onclick = () => modalBackdrop.classList.remove('show');
}
modalBackdrop.classList.add('show');
}
} else if (action === 'lucky_aura') { } else if (action === 'lucky_aura') {
if (window.soundFX) window.soundFX.playMagicSuccess(); if (window.soundFX) window.soundFX.playMagicSuccess();
showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!"); showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!");
+47 -20
View File
@@ -1,8 +1,8 @@
// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First) // Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First - Frenzy & Challenge Overhaul)
(function() { (function() {
let score = 0; let score = 0;
const targetScore = 10; const targetScore = 12;
let timeLeft = 35; let timeLeft = 30;
let gameRunning = false; let gameRunning = false;
let timerInterval = null; let timerInterval = null;
let spawnTimeout = null; let spawnTimeout = null;
@@ -21,10 +21,10 @@
if (gameRunning) return; if (gameRunning) return;
gameRunning = true; gameRunning = true;
score = 0; score = 0;
timeLeft = 35; timeLeft = 30;
scoreEl.innerText = score; if (scoreEl) scoreEl.innerText = score;
timerEl.innerText = timeLeft; if (timerEl) timerEl.innerText = timeLeft;
if (startOverlay) startOverlay.style.display = 'none'; if (startOverlay) startOverlay.style.display = 'none';
if (successPanel) successPanel.style.display = 'none'; if (successPanel) successPanel.style.display = 'none';
@@ -34,7 +34,7 @@
// Start countdown // Start countdown
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
timeLeft--; timeLeft--;
timerEl.innerText = timeLeft; if (timerEl) timerEl.innerText = timeLeft;
if (timeLeft <= 0) { if (timeLeft <= 0) {
endGame(false); endGame(false);
@@ -46,9 +46,24 @@
function scheduleNextSpawn() { function scheduleNextSpawn() {
if (!gameRunning) return; if (!gameRunning) return;
const delay = Math.random() * 350 + 400; // 400ms - 750ms
// Dynamic spawn delay based on progress
let minDelay = 380, maxDelay = 650;
if (score >= 8) {
minDelay = 220;
maxDelay = 420;
} else if (score >= 4) {
minDelay = 300;
maxDelay = 520;
}
const delay = Math.random() * (maxDelay - minDelay) + minDelay;
spawnTimeout = setTimeout(() => { spawnTimeout = setTimeout(() => {
spawnTarget(); spawnTarget();
// Chance of double spawn when frenzy begins (score >= 4)
if (score >= 4 && Math.random() < 0.40) {
setTimeout(spawnTarget, 80);
}
scheduleNextSpawn(); scheduleNextSpawn();
}, delay); }, delay);
} }
@@ -67,13 +82,23 @@
const targetEl = document.getElementById(`target${holeIdx}`); const targetEl = document.getElementById(`target${holeIdx}`);
if (!targetEl) return; if (!targetEl) return;
// 12% chance Neville, 88% chance Pixie // 16% chance Neville, 84% chance Pixie
const isNeville = Math.random() < 0.12; const isNeville = Math.random() < 0.16;
targetEl.className = 'pixie-target active'; targetEl.className = 'pixie-target active';
targetEl.innerText = isNeville ? '👦' : '🧚'; targetEl.innerText = isNeville ? '👦' : '🧚';
if (isNeville) targetEl.classList.add('neville'); if (isNeville) targetEl.classList.add('neville');
const stayTime = Math.random() * 450 + 900; // 900ms - 1350ms (forgiving for mobile) // Dynamic stay duration: faster as player scores higher
let stayTime = 800;
if (isNeville) {
stayTime = 950; // Neville stays a bit longer as a decoy
} else if (score >= 8) {
stayTime = Math.random() * 220 + 420; // 420ms - 640ms (Lightning fast frenzy!)
} else if (score >= 4) {
stayTime = Math.random() * 250 + 520; // 520ms - 770ms
} else {
stayTime = Math.random() * 280 + 660; // 660ms - 940ms
}
const hideTimer = setTimeout(() => { const hideTimer = setTimeout(() => {
hideTarget(holeIdx); hideTarget(holeIdx);
@@ -113,16 +138,18 @@
const targetEl = document.getElementById(`target${idx}`); const targetEl = document.getElementById(`target${idx}`);
if (targetData.isNeville) { if (targetData.isNeville) {
// Clicked Neville -> Penalty! // Clicked Neville -> Penalty (-2 Pkt and -1s)!
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 40]); if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([60, 60]);
score = Math.max(0, score - 2); score = Math.max(0, score - 2);
scoreEl.innerText = score; timeLeft = Math.max(1, timeLeft - 1);
if (scoreEl) scoreEl.innerText = score;
if (timerEl) timerEl.innerText = timeLeft;
targetEl.style.transform = 'scale(1.25) rotate(15deg)'; targetEl.style.transform = 'scale(1.25) rotate(15deg)';
if (window.showFloatingToast) { if (window.showFloatingToast) {
window.showFloatingToast("🤕 <strong>Neville:</strong> »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)"); window.showFloatingToast("🤕 <strong>Neville:</strong> »Aua! Ich bin doch kein Wichtel!« (-2 Pkt, -1s)");
} }
setTimeout(() => hideTarget(idx), 350); setTimeout(() => hideTarget(idx), 350);
@@ -141,12 +168,12 @@
} }
score++; score++;
scoreEl.innerText = score; if (scoreEl) scoreEl.innerText = score;
if (score >= targetScore) { if (score >= targetScore) {
endGame(true); endGame(true);
} else { } else {
setTimeout(() => hideTarget(idx), 380); setTimeout(() => hideTarget(idx), 280);
} }
} }
} }
@@ -193,9 +220,9 @@
window.showChamberVictory({ window.showChamberVictory({
chamberBadge: "Kammer V: Wichtel-Panik", chamberBadge: "Kammer V: Wichtel-Panik",
title: "🧚 Wichtel Gebannt!", title: "🧚 Wichtel Gebannt!",
statsHtml: `<span>❄️ Gebannt: <strong>${score} Wichtel</strong></span><span>👦 Neville: <strong>Gerettet</strong></span>`, statsHtml: `<span>❄️ Gebannt: <strong>${score}/${targetScore} Wichtel</strong></span><span>👦 Neville: <strong>Gerettet</strong></span><span>⏱️ Restzeit: <strong>${timeLeft}s</strong></span>`,
item: newItem, item: newItem,
storyText: "Mit blitzschnellen <em>Immobilus!</em>-Zaubern hast du alle 10 Wichtel eingefroren! Ein zappelnder Wichtel landet sicher im <strong>Wichtelglas</strong>. Neville bedankt sich erleichtert. Der Weg zur berüchtigten <strong>Flammenkammer von Severus Snape</strong> liegt nun offen!", storyText: "Mit blitzschnellen <em>Immobilus!</em>-Zaubern hast du alle 12 frechen Wichtel eingefroren! Ein zappelnder Wichtel landet sicher im <strong>Wichtelglas</strong>. Neville bedankt sich erleichtert. Der Weg zur berüchtigten <strong>Flammenkammer von Severus Snape</strong> liegt nun offen!",
nextUrl: "/bottles", nextUrl: "/bottles",
nextBtnText: "➔ Weiter zu Kammer VI: Snapes Flaschen 🧪" nextBtnText: "➔ Weiter zu Kammer VI: Snapes Flaschen 🧪"
}); });
@@ -208,7 +235,7 @@
if (startBtn) startBtn.innerText = "Erneut versuchen 🔄"; if (startBtn) startBtn.innerText = "Erneut versuchen 🔄";
} }
if (window.showFloatingToast) { if (window.showFloatingToast) {
window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Versuch es noch einmal."); window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Die Wichtel entwischten. Versuch es noch einmal!");
} }
} }
} }
+4 -3
View File
@@ -7,11 +7,12 @@
<link rel="icon" href="/static/favicon.ico"> <link rel="icon" href="/static/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=MedievalSharp&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=Marck+Script&family=MedievalSharp&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/escape.css?v=5.0"> <link rel="stylesheet" href="/static/css/escape.css?v=6.0">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
<!-- Minimalist & Mobile-Friendly Header --> <!-- Minimalist & Mobile-Friendly Header -->
<header class="site-header"> <header class="site-header">
<div class="header-inner"> <div class="header-inner">
@@ -282,7 +283,7 @@
</footer> </footer>
<script src="/static/js/audio.js?v=5.0"></script> <script src="/static/js/audio.js?v=5.0"></script>
<script src="/static/js/inventory.js?v=5.0"></script> <script src="/static/js/inventory.js?v=6.0"></script>
<script> <script>
async function resetEscapeProgress() { async function resetEscapeProgress() {
if (confirm("Möchtest du deinen Escape-Fortschritt und dein Inventar wirklich zurücksetzen?")) { if (confirm("Möchtest du deinen Escape-Fortschritt und dein Inventar wirklich zurücksetzen?")) {
+114 -31
View File
@@ -39,13 +39,16 @@
<div class="brew-container"> <div class="brew-container">
<!-- Recipe Instruction Scroll --> <!-- Recipe Instruction Scroll -->
<div class="recipe-book"> <div class="recipe-book">
<span id="stepBadge" class="recipe-step-badge">Schritt 1: Zutaten zugeben</span> <span id="stepBadge" class="recipe-step-badge">Schritt 1: Zutaten dosieren</span>
<h3 id="stepInstruction" style="color: var(--gold); margin: 0.25rem 0; font-size: 1.25rem;"> <h3 id="stepInstruction" style="color: var(--gold); margin: 0.25rem 0; font-size: 1.25rem;">
Gib die 4 Essenzen in den Kessel! Dosiere die 3 Essenzen nach Snapes Rezeptur!
</h3> </h3>
<p id="stepSubtext" style="color: var(--text-dim); font-size: 0.9rem; margin: 0;"> <p id="stepSubtext" style="color: var(--text-dim); font-size: 0.9rem; margin: 0 0 0.4rem 0;">
Tippe auf die Zutaten unten, um sie in den Kessel zu werfen. Löse Snapes Rätselnotiz, um die Mengenangaben für Felix Felicis herauszufinden.
</p> </p>
<button type="button" class="btn-subtle" style="font-size: 0.9rem; border-color: var(--gold); color: var(--gold); padding: 0.4rem 0.9rem;" onclick="openRecipeRiddleModal()">
📜 Snapes Rezeptur-Notiz öffnen
</button>
</div> </div>
<!-- The SVG Cauldron Stage --> <!-- The SVG Cauldron Stage -->
@@ -125,36 +128,81 @@
</svg> </svg>
</div> </div>
<!-- Step 1: Ingredient Shelf & Counter Bar --> <!-- Step 1: Ingredient Dosage Shelf & +/- Controls -->
<div id="step1Wrapper" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.6rem;"> <div id="step1Wrapper" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
<div style="font-size: 0.9rem; color: var(--gold); font-weight: bold;"> <div style="font-size: 0.95rem; color: var(--gold); font-weight: bold; text-align: center;">
🧪 Essenzen im Kessel: <span id="totalIngredientsCount">0</span> / 4 ⚗️ Dosiere die Essenzen laut Snapes Notiz:
</div> </div>
<div id="ingredientsShelf" class="ingredients-shelf"> <div class="dose-grid">
<button type="button" class="ingredient-btn" id="ingDragon" onclick="addIngredient('dragon')"> <!-- Dragon Blood -->
<span class="ing-icon">🐉</span> <div class="dose-card">
<span class="ing-name">Drachenblut</span> <span style="font-size: 2.2rem;">🐉</span>
<span class="ing-count" id="countDragon">0 / 2</span> <strong style="color: var(--text-light); font-size: 0.85rem;">Drachenblut</strong>
</button> <div class="dose-controls">
<button type="button" class="ingredient-btn" id="ingMandrake" onclick="addIngredient('mandrake')"> <button type="button" class="dose-btn" onclick="adjustDose('dragon', -1)">-</button>
<span class="ing-icon">🌿</span> <span id="doseValDragon" class="dose-val">0</span>
<span class="ing-name">Alraunenblatt</span> <button type="button" class="dose-btn" onclick="adjustDose('dragon', 1)">+</button>
<span class="ing-count" id="countMandrake">0 / 1</span> </div>
</button> </div>
<button type="button" class="ingredient-btn" id="ingStar" onclick="addIngredient('star')"> <!-- Mandrake -->
<span class="ing-icon">✨</span> <div class="dose-card">
<span class="ing-name">Sternenstaub</span> <span style="font-size: 2.2rem;">🌿</span>
<span class="ing-count" id="countStar">0 / 1</span> <strong style="color: var(--text-light); font-size: 0.85rem;">Alraune</strong>
</button> <div class="dose-controls">
<button type="button" class="dose-btn" onclick="adjustDose('mandrake', -1)">-</button>
<span id="doseValMandrake" class="dose-val">0</span>
<button type="button" class="dose-btn" onclick="adjustDose('mandrake', 1)">+</button>
</div>
</div>
<!-- Star Dust -->
<div class="dose-card">
<span style="font-size: 2.2rem;">✨</span>
<strong style="color: var(--text-light); font-size: 0.85rem;">Sternenstaub</strong>
<div class="dose-controls">
<button type="button" class="dose-btn" onclick="adjustDose('star', -1)">-</button>
<span id="doseValStar" class="dose-val">0</span>
<button type="button" class="dose-btn" onclick="adjustDose('star', 1)">+</button>
</div>
</div>
</div> </div>
<button type="button" id="brewVerifyBtn" class="btn-magical" style="padding: 0.85rem 1.8rem; font-size: 1.1rem; width: 100%; max-width: 340px;" onclick="verifyAndMixIngredients()">
🧪 Essenzen in den Kessel geben & prüfen
</button>
</div> </div>
<!-- Step 2: Stir Control Panel --> <!-- Step 2: Two-Way Stirring Gauge (Links ↺ und Rechts ↻) -->
<div id="stirPanel" style="display: none; flex-direction: column; align-items: center; gap: 0.5rem; width: 100%;"> <div id="stirPanel" style="display: none; flex-direction: column; align-items: center; gap: 0.75rem; width: 100%;">
<button type="button" id="stirBtn" class="btn-magical" style="padding: 0.9rem 1.8rem; font-size: 1.15rem; width: 100%; max-width: 320px;" onclick="doStir()"> <div class="stir-gauge-wrapper">
🥄 Kessel umrühren! (<span id="stirCount">0</span> / 3) <div style="font-size: 0.95rem; color: var(--gold); font-weight: bold; text-align: center;">
</button> 🥄 Rühr-Takt: <span id="stirPhaseLabel" style="color: #4cc9f0;">↺ LINKS (Phase 1)</span>
<span style="color: var(--text-dim); font-size: 0.85rem;">Tippe auf den Button, um den Sud umzurühren</span> </div>
<div class="stir-track">
<div class="stir-target-zone"></div>
<div id="stirIndicatorBar" class="stir-indicator-bar"></div>
</div>
<div style="display: flex; justify-content: space-between; width: 100%; font-size: 0.78rem; color: var(--text-dim);">
<span>Zu träge</span>
<span style="color: #2ecc71; font-weight: bold;">Ideal (40% - 75%)</span>
<span>Zu wild!</span>
</div>
<div style="font-size: 0.9rem; color: var(--text-light); font-weight: bold;">
Takt gehalten: <strong id="stirTimerVal" style="color: var(--gold);">0.0</strong> / 3.0s
</div>
</div>
<div class="stir-buttons-row">
<button type="button" id="btnStirLeft" class="stir-action-btn active-target" onclick="stirTap('left')">
<span style="font-size: 1.6rem;">↺</span>
<span>Linksherum</span>
</button>
<button type="button" id="btnStirRight" class="stir-action-btn" onclick="stirTap('right')">
<span style="font-size: 1.6rem;">↻</span>
<span>Rechtsherum</span>
</button>
</div>
<span style="color: var(--text-dim); font-size: 0.82rem; text-align: center;">
Tippe rhythmisch, um den Balken im grünen Idealbereich zu halten!
</span>
</div> </div>
<!-- Step 3: Heat Control Panel --> <!-- Step 3: Heat Control Panel -->
@@ -179,6 +227,41 @@
</button> </button>
</div> </div>
<!-- Snape Reprimand Modal -->
<div id="snapeReprimandModal" class="snape-reprimand-modal">
<div class="snape-card">
<div class="snape-avatar">🧙‍♂️</div>
<span style="display: inline-block; background: rgba(142, 68, 173, 0.25); border: 1px solid #8e44ad; color: #d2b4de; font-size: 0.8rem; font-weight: bold; padding: 0.2rem 0.75rem; border-radius: 20px; text-transform: uppercase;">
Professor Severus Snape
</span>
<h3 style="color: #e74c3c; margin: 0.5rem 0 0.2rem 0; font-size: 1.35rem;">Kläglicher Fehltritt!</h3>
<div id="snapeReprimandText" class="snape-quote-box">
»Kläglich. Sogar ein Bergtroll mit verbundenen Augen hätte die Mengen besser ergründet.«
</div>
<button type="button" class="btn-magical" style="padding: 0.75rem 1.6rem; font-size: 1.05rem;" onclick="closeSnapeModal()">
🧹 Kessel säubern (Evanesco) & neu ansetzen
</button>
</div>
</div>
<!-- Recipe Riddle Parchment Modal -->
<div id="recipeRiddleModal" class="snape-reprimand-modal">
<div class="snape-card" style="border-color: var(--gold); box-shadow: 0 0 45px rgba(229, 185, 72, 0.4);">
<div style="font-size: 3.2rem; margin-bottom: 0.3rem;">📜✨</div>
<h3 style="color: var(--gold); margin: 0.3rem 0; font-size: 1.4rem;">Snapes Felix-Felicis-Rezeptur</h3>
<div class="snape-quote-box" style="border-color: var(--gold);">
<em>»Drei Essenzen verlangt das flüssige Gold:<br><br>
• 🐉 <strong>Drachenblut:</strong> Die Köpfe des dreiköpfigen Wächters Fluffy (3 Tropfen)<br>
• 🌿 <strong>Alraunenblatt:</strong> Harrys treue Gefährten am Gryffindor-Tisch (Ron & Hermine = 2 Blätter)<br>
• ✨ <strong>Sternenstaub:</strong> Der eine Siegelring des Alchemisten (1 Prise)<br><br>
<strong>Rühren:</strong> 3 Sekunden gleichmäßig LINKS (↺), danach 3 Sekunden RECHTS (↻)! Zu hektisch oder zu heiß zerstört den Sud augenblicklich!«</em>
</div>
<button type="button" class="btn-magical" style="padding: 0.75rem 1.8rem; font-size: 1.05rem;" onclick="closeRecipeRiddleModal()">
➔ Verstanden, zum Kessel
</button>
</div>
</div>
<!-- Success Container --> <!-- Success Container -->
<div id="successPanel" style="display: none; text-align: center; margin-top: 1rem; width: 100%;"> <div id="successPanel" style="display: none; text-align: center; margin-top: 1rem; width: 100%;">
<div class="dungeon-card" style="border-color: #2ecc71;"> <div class="dungeon-card" style="border-color: #2ecc71;">
@@ -199,5 +282,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/brew.js?v=5.0"></script> <script src="/static/js/brew.js?v=6.0"></script>
{% endblock %} {% endblock %}
+21 -6
View File
@@ -12,12 +12,13 @@
</p> </p>
</div> </div>
<div class="canvas-wrapper"> <div class="canvas-wrapper" id="canvasWrapper">
<div class="game-hud"> <div class="game-hud">
<div>⏱️ Zeit: <strong id="timerVal">0s</strong></div> <div>⏱️ Zeit: <strong id="timerVal">0s</strong></div>
<div>⭕ Ringe: <strong id="ringsVal" style="color: #ffd700;">0/12</strong></div> <div>⭕ Ringe: <strong id="ringsVal" style="color: #ffd700;">0/12</strong></div>
<div>✨ Punkte: <strong id="scoreVal">0</strong></div> <div>✨ Punkte: <strong id="scoreVal">0</strong></div>
<div id="comboVal" style="color: #00ffff; font-weight: bold; min-width: 80px;"></div> <div>🧹 Zustand: <span id="livesVal" style="font-size: 1.1rem; letter-spacing: 2px;">💚💚💚</span></div>
<div id="comboVal" style="color: #00ffff; font-weight: bold; min-width: 60px;"></div>
</div> </div>
<canvas id="broomCanvas" style="touch-action: none; -webkit-user-select: none; user-select: none; width: 100%; display: block;"></canvas> <canvas id="broomCanvas" style="touch-action: none; -webkit-user-select: none; user-select: none; width: 100%; display: block;"></canvas>
@@ -26,14 +27,28 @@
<div id="broomStartOverlay" class="flight-start-overlay"> <div id="broomStartOverlay" class="flight-start-overlay">
<div style="font-size: 2.8rem; margin-bottom: 0.3rem;">🧹✨⭕</div> <div style="font-size: 2.8rem; margin-bottom: 0.3rem;">🧹✨⭕</div>
<h3 style="color: var(--gold); font-size: 1.45rem; margin: 0 0 0.4rem 0;">Bereit zum Abflug?</h3> <h3 style="color: var(--gold); font-size: 1.45rem; margin: 0 0 0.4rem 0;">Bereit zum Abflug?</h3>
<p style="color: var(--text-light); font-size: 0.95rem; margin: 0 0 1.2rem 0; text-align: center; max-width: 300px;"> <p style="color: var(--text-light); font-size: 0.95rem; margin: 0 0 1.2rem 0; text-align: center; max-width: 320px;">
Fliege durch <strong>12 magische Ringe</strong>!<br> Fliege durch <strong>12 magische Ringe</strong>!<br>
Steuere per Daumen über das Spielfeld oder mit dem Steuerkreuz unten. <span style="color: #e74c3c;">⚠️ Weiche wilden Klatschern (💣) und schwebenden Felsen (🪨) aus!</span><br>
Steuere per Daumen über das Feld oder mit dem Steuerkreuz.
</p> </p>
<button type="button" id="startFlightBtn" class="btn-magical" style="padding: 0.85rem 2.2rem; font-size: 1.2rem;" onclick="startFlight()"> <button type="button" id="startFlightBtn" class="btn-magical" style="padding: 0.85rem 2.2rem; font-size: 1.2rem;">
⚡ Flug starten! ⚡ Flug starten!
</button> </button>
</div> </div>
<!-- Crash / Game Over Overlay -->
<div id="broomCrashOverlay" class="flight-start-overlay hidden">
<div style="font-size: 2.8rem; margin-bottom: 0.3rem;">💥🧹😵</div>
<h3 style="color: #e74c3c; font-size: 1.45rem; margin: 0 0 0.4rem 0;">Klatscher-Volltreffer!</h3>
<p style="color: var(--text-light); font-size: 0.95rem; margin: 0 0 1.2rem 0; text-align: center; max-width: 300px;">
Dein Besen hat die Balance verloren und Madam Hooch pfeift zum Abbruch!<br>
Halte Abstand zu den wirbelnden Eisenbällen!
</p>
<button type="button" id="restartFlightBtn" class="btn-magical" style="padding: 0.85rem 2.2rem; font-size: 1.2rem;">
🔄 Erneut abheben!
</button>
</div>
</div> </div>
<!-- Mobile Flight Cockpit & D-Pad --> <!-- Mobile Flight Cockpit & D-Pad -->
@@ -73,5 +88,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/broom.js?v=5.0"></script> <script src="/static/js/broom.js?v=6.0"></script>
{% endblock %} {% endblock %}
+68 -7
View File
@@ -3,14 +3,75 @@
{% block title %}Die Prüfungen des Steins der Weisen · Hogwarts Escape{% endblock %} {% block title %}Die Prüfungen des Steins der Weisen · Hogwarts Escape{% endblock %}
{% block content %} {% block content %}
<div class="parchment-scroll"> <!-- Interaktiver Hogwarts-Brief & Siegel Intro -->
<h1 class="parchment-title">⚡ Die Kammern des Steins der Weisen ⚡</h1> <div class="escape-letter-intro" id="escapeLetterIntro">
<p class="riddle-poem" style="font-size: 1.3rem; margin: 0.5rem 0;"> <div class="antique-envelope-scene" id="envelope">
"Die Falltür unter Fluffys Pfoten hat sich geöffnet.<br> <div class="envelope-base">
Tief unter den Kerkern von Hogwarts wachen uralte Zauber über das größte Geheimnis der Alchemie.<br> <div class="envelope-interior"></div>
Beweise deinen Scharfsinn, dein Geschick auf dem Besen und deine eiserne Logik!" <div class="letter-sheet" id="letterSheet">
</p> <header class="letter-header">
<div class="letter-crest-box">
<img src="/static/img/hogwarts_seal.jpg" alt="Hogwarts Crest" class="letter-crest-sketch">
</div>
<h3 class="letter-school-name">Hogwarts-Schule für Hexerei und Zauberei</h3>
<p class="letter-headmaster">Schulleiter: Albus Dumbledore<br><span>(Hüter des Steins der Weisen · Verbotener Korridor)</span></p>
<div class="letter-ornament-line">
<span class="ornament-leaf">❦</span>
<span class="ornament-rule"></span>
<span class="ornament-shield">✦</span>
<span class="ornament-rule"></span>
<span class="ornament-leaf">❧</span>
</div>
</header>
<div class="letter-body-handwritten">
<p class="letter-salutation">Liebe Hexe, lieber Zauberer,</p>
<p class="letter-paragraph">
die Falltür unter Fluffys Pfoten hat sich geöffnet! Tief unter den Kerkern von Hogwarts wachen sechs uralte Schutzzauber der Lehrer über das größte Geheimnis der Alchemie: den <strong>Stein der Weisen</strong>.
</p>
<p class="letter-paragraph">
Schwinge dich auf deinen Besen, stelle dich dem Zauberstab-Duell, braue das goldene Glückselixier und durchschreite Snapes schwarze Flammen. Nur wer alle Kammern meistert, bewahrt das Schloss vor dem Schatten!
</p>
</div>
<footer class="letter-signature-block">
<p class="letter-closing">Mit magischer Hochachtung,</p>
<p class="letter-signature-ink">Minerva McGonagall</p>
<p class="letter-sign-title">Stellvertretende Schulleiterin</p>
</footer>
</div>
<div class="envelope-back-body"></div>
<div class="envelope-closing-flap envelope-v-flap" id="envelopeFlap">
<div class="v-flap-sketch-line"></div>
</div>
<div class="sketched-wax-seal" id="waxSeal" onclick="toggleEscapeLetter()" title="Siegel brechen & Botschaft lesen">
<img src="/static/img/hogwarts_seal.jpg" alt="Hogwarts-Wappen Siegel" class="seal-crest-img">
<div class="seal-emboss-ring"></div>
<div class="seal-glow"></div>
</div>
</div>
</div>
<div style="text-align: center; margin-bottom: 1.5rem;">
<button type="button" id="letterToggleBtn" class="btn-magical" onclick="toggleEscapeLetter()" style="padding: 0.65rem 1.4rem; font-size: 0.95rem;">
📜 Siegel brechen & Einladung lesen
</button>
</div>
</div> </div>
<script>
function toggleEscapeLetter() {
const env = document.getElementById('envelope');
const btn = document.getElementById('letterToggleBtn');
if (!env) return;
const opened = env.classList.toggle('envelope-opened');
if (window.soundFX) window.soundFX.playKeyChime();
const bgm = document.getElementById('bgm');
if (bgm && bgm.paused && (!window.soundFX || !window.soundFX.isMuted())) {
bgm.volume = 0.45;
bgm.play().catch(() => {});
}
if (btn) {
btn.innerText = opened ? '↩ Brief wieder verschließen' : '📜 Siegel brechen & Einladung lesen';
}
}
</script>
<div class="dungeon-card"> <div class="dungeon-card">
<h2 style="color: var(--gold); text-align: center; margin-bottom: 0.5rem; font-size: 1.8rem;"> <h2 style="color: var(--gold); text-align: center; margin-bottom: 0.5rem; font-size: 1.8rem;">
+3 -3
View File
@@ -40,10 +40,10 @@
<!-- Top HUD Bar --> <!-- Top HUD Bar -->
<div class="pixies-hud"> <div class="pixies-hud">
<div> <div>
🧚 Gefangen: <strong id="pixieScore" style="color: var(--gold); font-size: 1.3rem;">0</strong> / 10 🧚 Gebannt: <strong id="pixieScore" style="color: var(--gold); font-size: 1.3rem;">0</strong> / 12
</div> </div>
<div> <div>
⏳ Zeit: <strong id="pixieTimer" style="color: #3498db; font-size: 1.3rem;">35</strong>s ⏳ Zeit: <strong id="pixieTimer" style="color: #3498db; font-size: 1.3rem;">30</strong>s
</div> </div>
</div> </div>
@@ -131,5 +131,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/pixies.js?v=5.0"></script> <script src="/static/js/pixies.js?v=6.0"></script>
{% endblock %} {% endblock %}
+25 -4
View File
@@ -1,4 +1,25 @@
fastapi>=0.110.0 annotated-doc==0.0.5
uvicorn>=0.29.0 annotated-types==0.8.0
jinja2>=3.1.3 anyio==4.15.1
python-dotenv>=1.0.1 certifi==2026.7.22
click==8.5.0
fastapi==0.141.1
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.20
iniconfig==2.3.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
packaging==26.3
pluggy==1.6.0
pydantic==2.13.5
pydantic_core==2.46.5
Pygments==2.21.0
pytest==9.1.1
python-dotenv==1.2.3
starlette==1.6.0
typing-inspection==0.4.4
typing_extensions==4.16.0
uvicorn==0.53.0