diff --git a/.gitignore b/.gitignore index 9b5f630..2b91c72 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +venv/ __pycache__/ *.pyc -venv/ +.env diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..caf6bfb --- /dev/null +++ b/INSTALL.md @@ -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; + } +} +``` diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc deleted file mode 100644 index 38fc24a..0000000 Binary files a/app/__pycache__/main.cpython-313.pyc and /dev/null differ diff --git a/app/main.py b/app/main.py index 2e7bb72..5da1a14 100755 --- a/app/main.py +++ b/app/main.py @@ -58,11 +58,11 @@ ALL_ITEMS = { }, "duel_badge": { "id": "duel_badge", - "name": "Duellanten-Abzeichen", + "name": "Duellanten-Abzeichen & Snapes Notiz", "icon": "⚡", - "desc": "Ein goldenes Abzeichen für meisterhafte Zauberstab-Gesten im Duellier-Club von Hogwarts.", - "action": "spark_wand", - "action_label": "Funkenregen sprühen ⚡" + "desc": "Ein goldenes Abzeichen mit Snapes handschriftlicher Rezeptur-Notiz für Felix Felicis auf der Rückseite.", + "action": "read_recipe", + "action_label": "Snapes Rezeptur lesen 📜" }, "felix_felicis": { "id": "felix_felicis", diff --git a/app/static/audio/music.mp3 b/app/static/audio/music.mp3 new file mode 100755 index 0000000..59059b2 Binary files /dev/null and b/app/static/audio/music.mp3 differ diff --git a/app/static/css/escape.css b/app/static/css/escape.css index 6ce0f66..67401f8 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -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; +} diff --git a/app/static/js/audio.js b/app/static/js/audio.js index 42e5d5c..74e012c 100755 --- a/app/static/js/audio.js +++ b/app/static/js/audio.js @@ -144,9 +144,18 @@ window.soundFX = new SoundFX(); function updateAudioButtonUI() { const btn = document.getElementById('audioToggleBtn'); + const bgm = document.getElementById('bgm'); if (!btn) return; const isMuted = window.soundFX.isMuted(); - btn.innerHTML = isMuted ? '🔇 Ton: Aus' : '🔊 Ton: An'; + btn.innerHTML = isMuted ? '🔇 Musik & Ton: Aus' : '🔊 Musik & Ton: An'; + if (bgm) { + bgm.volume = 0.45; + if (isMuted && !bgm.paused) { + bgm.pause(); + } else if (!isMuted && bgm.paused) { + bgm.play().catch(() => {}); + } + } } function toggleSound() { @@ -158,5 +167,10 @@ document.addEventListener('DOMContentLoaded', () => { updateAudioButtonUI(); document.body.addEventListener('click', () => { window.soundFX.init(); + const bgm = document.getElementById('bgm'); + if (bgm && !window.soundFX.isMuted() && bgm.paused) { + bgm.volume = 0.45; + bgm.play().catch(() => {}); + } }, { once: true }); }); diff --git a/app/static/js/brew.js b/app/static/js/brew.js index d58005f..d72b7ee 100755 --- a/app/static/js/brew.js +++ b/app/static/js/brew.js @@ -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() { - let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done - const ingredients = { + let currentStep = 1; // 1: Dosage, 2: Stirring, 3: Heat, 4: Done + + // Step 1: Ingredient Doses + const doses = { dragon: 0, mandrake: 0, star: 0 }; - const targetIngredients = { - dragon: 2, - mandrake: 1, - star: 1 + const targetDoses = { + dragon: 3, // Fluffy's 3 heads + mandrake: 2, // Harry's companions Ron & Hermione + star: 1 // The Alchemist's 1 ring }; // DOM Elements @@ -17,7 +19,9 @@ const stepInstruction = document.getElementById('stepInstruction'); const stepSubtext = document.getElementById('stepSubtext'); 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 const gradStop1 = document.getElementById('gradStop1'); @@ -27,121 +31,275 @@ const stirSpoon = document.getElementById('stirSpoon'); const dropItemAnim = document.getElementById('dropItemAnim'); - // Controls + // Step 2: Stirring Controls const stirPanel = document.getElementById('stirPanel'); - const stirCountSpan = document.getElementById('stirCount'); - const stirBtn = document.getElementById('stirBtn'); + const stirPhaseLabel = document.getElementById('stirPhaseLabel'); + 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 tempIndicator = document.getElementById('tempIndicator'); const heatTimerSpan = document.getElementById('heatTimer'); + const bellowsBtn = document.getElementById('bellowsBtn'); + + // Success & Modals const successPanel = document.getElementById('successPanel'); + const snapeModal = document.getElementById('snapeReprimandModal'); + const snapeText = document.getElementById('snapeReprimandText'); + const riddleModal = document.getElementById('recipeRiddleModal'); - // Step 1: Add Ingredient - window.addIngredient = function(type) { - if (currentStep !== 1) return; - if (ingredients[type] >= targetIngredients[type]) return; + // Snape Insult Repository + const snapeQuotes = [ + "»Kläglich. Sogar ein Bergtroll mit verbundenen Augen hätte die Mengenangaben treffender ergründet.«", + "»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]++; - if (window.soundFX) window.soundFX.playBottleClink(); - if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(30); + function getRandomSnapeQuote() { + return snapeQuotes[Math.floor(Math.random() * snapeQuotes.length)]; + } - // Update UI Badge - if (type === 'dragon') { - document.getElementById('countDragon').innerText = `${ingredients.dragon} / ${targetIngredients.dragon}`; - if (ingredients.dragon >= targetIngredients.dragon) document.getElementById('ingDragon').classList.add('done'); - } 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); - } + // Modal helpers + window.openRecipeRiddleModal = function() { + if (riddleModal) riddleModal.classList.add('show'); + if (window.soundFX) window.soundFX.playStoneSlide(); }; - // Step 2: Stirring - let completedStirs = 0; + window.closeRecipeRiddleModal = function() { + 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() { currentStep = 2; - stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; - stepInstruction.innerText = "Rühre den Kessel 3-mal um!"; - stepSubtext.innerText = "Tippe auf den großen Rühr-Button, um die Essenzen zu vermengen."; + stirPhase = 'left'; + phaseTimer = 0.0; + 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 (stirPanel) stirPanel.style.display = 'flex'; - - // Show wooden spoon in cauldron if (stirSpoon) stirSpoon.style.display = 'block'; - // Shift liquid color to mystical purple + // Set liquid to rich amethyst purple if (gradStop1 && gradStop2 && gradStop3) { gradStop1.setAttribute('stop-color', '#9b59b6'); gradStop2.setAttribute('stop-color', '#8e44ad'); gradStop3.setAttribute('stop-color', '#341f47'); } + updateStirUI(); 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; - completedStirs++; - if (stirCountSpan) stirCountSpan.innerText = completedStirs; - if (stirBtn) stirBtn.innerHTML = `🥄 Kessel umrühren! (${completedStirs} / 3)`; - if (window.soundFX) window.soundFX.playBottleClink(); - if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(35); - - // Animate stirring spoon - if (stirSpoon) { - stirSpoon.classList.remove('stirring-active'); - void stirSpoon.offsetWidth; // re-flow - stirSpoon.classList.add('stirring-active'); + // Check if wrong direction tapped! + if (direction !== stirPhase) { + const wrongDirQuote = (stirPhase === 'left') ? + "»Linksherum, sagte ich! Verwechseln Sie die Drehrichtungen wie Mr. Finnigan?«" : + "»Nun rechtsherum! Hören Sie überhaupt zu oder träumen Sie von Schokofröschen?«"; + window.triggerSnapeInsult(wrongDirQuote); + phaseTimer = 0.0; + stirTempo = 25; + updateStirUI(); + return; } - if (completedStirs >= 3) { - setTimeout(() => { - if (stirSpoon) stirSpoon.style.display = 'none'; - if (stirPanel) stirPanel.style.display = 'none'; - startStep3(); - }, 600); + // Correct direction tap: give a rhythmic boost + stirTempo = Math.min(100, stirTempo + 13); + + if (window.soundFX) window.soundFX.playBottleClink(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); + + // Animate spoon + if (stirSpoon) { + stirSpoon.classList.remove('stirring-active'); + void stirSpoon.offsetWidth; + stirSpoon.classList.add('stirring-active'); } }; - // Step 3: Heat Control - let currentTemp = 35; // percent (0-100) + function updateStirLoop() { + 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 heatInterval = null; + let overheatSeconds = 0.0; function startStep3() { currentStep = 3; + heatTimer = 0.0; + currentTemp = 35; + overheatSeconds = 0.0; + stepBadge.innerText = "Schritt 3: Hitze balancieren"; 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'); // Shift liquid color to fiery orange @@ -152,7 +310,7 @@ } if (window.soundFX) window.soundFX.playWhoosh(); - + if (heatInterval) clearInterval(heatInterval); heatInterval = setInterval(updateHeatLoop, 50); } @@ -162,21 +320,35 @@ if (window.soundFX) window.soundFX.playWhoosh(); if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); - const bellows = document.getElementById('bellowsBtn'); - if (bellows) { - bellows.style.transform = 'scale(0.92)'; - setTimeout(() => bellows.style.transform = 'scale(1)', 100); + if (bellowsBtn) { + bellowsBtn.style.transform = 'scale(0.92)'; + setTimeout(() => bellowsBtn.style.transform = 'scale(1)', 100); } }; function updateHeatLoop() { if (currentStep !== 3) return; - // Gentle cooling down + // Cooling decay currentTemp = Math.max(0, currentTemp - 0.45); 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) { heatTimer += 0.05; if (heatTimerSpan) heatTimerSpan.innerText = heatTimer.toFixed(1); @@ -187,6 +359,7 @@ if (heatTimer >= 3.0) { clearInterval(heatInterval); + heatInterval = null; finishPotionBrew(); } } @@ -200,11 +373,11 @@ stepBadge.style.color = "#2ecc71"; 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'); - // Turn liquid into sparkling golden elixir + // Turn liquid into radiant shimmering gold if (gradStop1 && gradStop2 && gradStop3) { gradStop1.setAttribute('stop-color', '#fff6a5'); gradStop2.setAttribute('stop-color', '#ffd700'); @@ -239,9 +412,9 @@ window.showChamberVictory({ chamberBadge: "Kammer III: Die Brauküche", title: "⚗️ Zaubertrank Gemeistert!", - statsHtml: `🧪 Elixier: Felix Felicis🌡️ Hitze: Perfekt gemeistert`, + statsHtml: `🧪 Elixier: Felix Felicis🥄 Rühren: 2 Phasen perfekt🌡️ Hitze: Meisterlich`, item: newItem, - storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren Felix Felicis ab. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige Runen-Drehschloss der vier Gründer!", + storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren Felix Felicis 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 Runen-Drehschloss der vier Gründer!", nextUrl: "/cryptex", nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐" }); diff --git a/app/static/js/broom.js b/app/static/js/broom.js index e84c067..e669a5a 100755 --- a/app/static/js/broom.js +++ b/app/static/js/broom.js @@ -9,7 +9,11 @@ const timerVal = document.getElementById('timerVal'); const ringsVal = document.getElementById('ringsVal'); const comboVal = document.getElementById('comboVal'); + const livesVal = document.getElementById('livesVal'); 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 modalTitle = document.getElementById('modalTitle'); const modalText = document.getElementById('modalText'); @@ -32,15 +36,28 @@ let score = 0; let ringsCollected = 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() { - const overlay = document.getElementById('broomStartOverlay'); - if (overlay) { - overlay.classList.add('hidden'); - overlay.style.display = 'none'; + const startOverlay = document.getElementById('broomStartOverlay'); + if (startOverlay) { + startOverlay.classList.add('hidden'); + startOverlay.style.display = 'none'; + } + if (broomCrashOverlay) { + broomCrashOverlay.classList.add('hidden'); + broomCrashOverlay.style.display = 'none'; } 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 + 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 }); @@ -48,19 +65,24 @@ score = 0; ringsCollected = 0; combo = 0; + lives = 3; + player.invulnerableTimer = 0; + updateLivesDisplay(); + if (scoreVal) scoreVal.innerText = score; if (ringsVal) ringsVal.innerText = `0/${TOTAL_RINGS_NEEDED}`; if (comboVal) comboVal.innerText = ''; startTime = Date.now(); lastRingSpawn = Date.now(); + lastObstacleSpawn = Date.now() + 1200; gameRunning = true; if (window.soundFX) window.soundFX.playWhoosh(); 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'); if (startFlightBtn) { const onStartTrigger = (e) => { @@ -74,6 +96,18 @@ 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) const player = { x: 90, @@ -85,6 +119,8 @@ speed: 6.5, boost: 1.0, angle: 0, + invulnerableTimer: 0, + hitRadius: 18, 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 const particles = []; function spawnSparkles(x, y, color) { @@ -264,6 +333,17 @@ 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 let ax = 0, ay = 0; if (keysDown['arrowup'] || keysDown['w']) ay -= 1; @@ -299,6 +379,71 @@ if (player.trail.length > 20) player.trail.shift(); 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' ? + "💥 Klatscher-Treffer! Besen-Zustand beschädigt! (-100 Pkt)" : + "🪨 Felsen gerammt! 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 for (let i = rings.length - 1; i >= 0; i--) { const r = rings[i]; @@ -445,6 +590,85 @@ 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; // Render Sparkles @@ -457,7 +681,12 @@ }); 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.translate(player.x, player.y); ctx.rotate(player.angle); diff --git a/app/static/js/inventory.js b/app/static/js/inventory.js index 7474235..b5346be 100755 --- a/app/static/js/inventory.js +++ b/app/static/js/inventory.js @@ -159,6 +159,33 @@ } else if (action === 'spark_wand') { if (window.soundFX) window.soundFX.playWhoosh(); 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 = ` +
- "Die Falltür unter Fluffys Pfoten hat sich geöffnet.
- Tief unter den Kerkern von Hogwarts wachen uralte Zauber über das größte Geheimnis der Alchemie.
- Beweise deinen Scharfsinn, dein Geschick auf dem Besen und deine eiserne Logik!"
-
+ Schulleiter: Albus Dumbledore
(Hüter des Steins der Weisen · Verbotener Korridor)
Liebe Hexe, lieber Zauberer,
++ 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 Stein der Weisen. +
++ 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! +
+
+
+
+