feat: Add audio assets, UI escape room polish, requirements, and INSTALL.md
This commit is contained in:
+2
-1
@@ -1,3 +1,4 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.env
|
||||
|
||||
+82
@@ -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
@@ -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",
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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
@@ -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 ? '🔇 <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() {
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
+267
-94
@@ -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! (<span>${completedStirs}</span> / 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: `<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,
|
||||
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",
|
||||
nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐"
|
||||
});
|
||||
|
||||
+235
-6
@@ -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' ?
|
||||
"💥 <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
|
||||
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);
|
||||
|
||||
@@ -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 = `
|
||||
<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') {
|
||||
if (window.soundFX) window.soundFX.playMagicSuccess();
|
||||
showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!");
|
||||
|
||||
+47
-20
@@ -1,8 +1,8 @@
|
||||
// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First)
|
||||
// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First - Frenzy & Challenge Overhaul)
|
||||
(function() {
|
||||
let score = 0;
|
||||
const targetScore = 10;
|
||||
let timeLeft = 35;
|
||||
const targetScore = 12;
|
||||
let timeLeft = 30;
|
||||
let gameRunning = false;
|
||||
let timerInterval = null;
|
||||
let spawnTimeout = null;
|
||||
@@ -21,10 +21,10 @@
|
||||
if (gameRunning) return;
|
||||
gameRunning = true;
|
||||
score = 0;
|
||||
timeLeft = 35;
|
||||
timeLeft = 30;
|
||||
|
||||
scoreEl.innerText = score;
|
||||
timerEl.innerText = timeLeft;
|
||||
if (scoreEl) scoreEl.innerText = score;
|
||||
if (timerEl) timerEl.innerText = timeLeft;
|
||||
if (startOverlay) startOverlay.style.display = 'none';
|
||||
if (successPanel) successPanel.style.display = 'none';
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
// Start countdown
|
||||
timerInterval = setInterval(() => {
|
||||
timeLeft--;
|
||||
timerEl.innerText = timeLeft;
|
||||
if (timerEl) timerEl.innerText = timeLeft;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
endGame(false);
|
||||
@@ -46,9 +46,24 @@
|
||||
|
||||
function scheduleNextSpawn() {
|
||||
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(() => {
|
||||
spawnTarget();
|
||||
// Chance of double spawn when frenzy begins (score >= 4)
|
||||
if (score >= 4 && Math.random() < 0.40) {
|
||||
setTimeout(spawnTarget, 80);
|
||||
}
|
||||
scheduleNextSpawn();
|
||||
}, delay);
|
||||
}
|
||||
@@ -67,13 +82,23 @@
|
||||
const targetEl = document.getElementById(`target${holeIdx}`);
|
||||
if (!targetEl) return;
|
||||
|
||||
// 12% chance Neville, 88% chance Pixie
|
||||
const isNeville = Math.random() < 0.12;
|
||||
// 16% chance Neville, 84% chance Pixie
|
||||
const isNeville = Math.random() < 0.16;
|
||||
targetEl.className = 'pixie-target active';
|
||||
targetEl.innerText = isNeville ? '👦' : '🧚';
|
||||
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(() => {
|
||||
hideTarget(holeIdx);
|
||||
@@ -113,16 +138,18 @@
|
||||
const targetEl = document.getElementById(`target${idx}`);
|
||||
|
||||
if (targetData.isNeville) {
|
||||
// Clicked Neville -> Penalty!
|
||||
// Clicked Neville -> Penalty (-2 Pkt and -1s)!
|
||||
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);
|
||||
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)';
|
||||
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);
|
||||
@@ -141,12 +168,12 @@
|
||||
}
|
||||
|
||||
score++;
|
||||
scoreEl.innerText = score;
|
||||
if (scoreEl) scoreEl.innerText = score;
|
||||
|
||||
if (score >= targetScore) {
|
||||
endGame(true);
|
||||
} else {
|
||||
setTimeout(() => hideTarget(idx), 380);
|
||||
setTimeout(() => hideTarget(idx), 280);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,9 +220,9 @@
|
||||
window.showChamberVictory({
|
||||
chamberBadge: "Kammer V: Wichtel-Panik",
|
||||
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,
|
||||
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",
|
||||
nextBtnText: "➔ Weiter zu Kammer VI: Snapes Flaschen 🧪"
|
||||
});
|
||||
@@ -208,7 +235,7 @@
|
||||
if (startBtn) startBtn.innerText = "Erneut versuchen 🔄";
|
||||
}
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
<link rel="icon" href="/static/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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 rel="stylesheet" href="/static/css/escape.css?v=5.0">
|
||||
<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=6.0">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
|
||||
<!-- Minimalist & Mobile-Friendly Header -->
|
||||
<header class="site-header">
|
||||
<div class="header-inner">
|
||||
@@ -282,7 +283,7 @@
|
||||
</footer>
|
||||
|
||||
<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>
|
||||
async function resetEscapeProgress() {
|
||||
if (confirm("Möchtest du deinen Escape-Fortschritt und dein Inventar wirklich zurücksetzen?")) {
|
||||
|
||||
+114
-31
@@ -39,13 +39,16 @@
|
||||
<div class="brew-container">
|
||||
<!-- Recipe Instruction Scroll -->
|
||||
<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;">
|
||||
Gib die 4 Essenzen in den Kessel!
|
||||
Dosiere die 3 Essenzen nach Snapes Rezeptur!
|
||||
</h3>
|
||||
<p id="stepSubtext" style="color: var(--text-dim); font-size: 0.9rem; margin: 0;">
|
||||
Tippe auf die Zutaten unten, um sie in den Kessel zu werfen.
|
||||
<p id="stepSubtext" style="color: var(--text-dim); font-size: 0.9rem; margin: 0 0 0.4rem 0;">
|
||||
Löse Snapes Rätselnotiz, um die Mengenangaben für Felix Felicis herauszufinden.
|
||||
</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>
|
||||
|
||||
<!-- The SVG Cauldron Stage -->
|
||||
@@ -125,36 +128,81 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Ingredient Shelf & Counter Bar -->
|
||||
<div id="step1Wrapper" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.6rem;">
|
||||
<div style="font-size: 0.9rem; color: var(--gold); font-weight: bold;">
|
||||
🧪 Essenzen im Kessel: <span id="totalIngredientsCount">0</span> / 4
|
||||
<!-- Step 1: Ingredient Dosage Shelf & +/- Controls -->
|
||||
<div id="step1Wrapper" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.75rem;">
|
||||
<div style="font-size: 0.95rem; color: var(--gold); font-weight: bold; text-align: center;">
|
||||
⚗️ Dosiere die Essenzen laut Snapes Notiz:
|
||||
</div>
|
||||
<div id="ingredientsShelf" class="ingredients-shelf">
|
||||
<button type="button" class="ingredient-btn" id="ingDragon" onclick="addIngredient('dragon')">
|
||||
<span class="ing-icon">🐉</span>
|
||||
<span class="ing-name">Drachenblut</span>
|
||||
<span class="ing-count" id="countDragon">0 / 2</span>
|
||||
</button>
|
||||
<button type="button" class="ingredient-btn" id="ingMandrake" onclick="addIngredient('mandrake')">
|
||||
<span class="ing-icon">🌿</span>
|
||||
<span class="ing-name">Alraunenblatt</span>
|
||||
<span class="ing-count" id="countMandrake">0 / 1</span>
|
||||
</button>
|
||||
<button type="button" class="ingredient-btn" id="ingStar" onclick="addIngredient('star')">
|
||||
<span class="ing-icon">✨</span>
|
||||
<span class="ing-name">Sternenstaub</span>
|
||||
<span class="ing-count" id="countStar">0 / 1</span>
|
||||
</button>
|
||||
<div class="dose-grid">
|
||||
<!-- Dragon Blood -->
|
||||
<div class="dose-card">
|
||||
<span style="font-size: 2.2rem;">🐉</span>
|
||||
<strong style="color: var(--text-light); font-size: 0.85rem;">Drachenblut</strong>
|
||||
<div class="dose-controls">
|
||||
<button type="button" class="dose-btn" onclick="adjustDose('dragon', -1)">-</button>
|
||||
<span id="doseValDragon" class="dose-val">0</span>
|
||||
<button type="button" class="dose-btn" onclick="adjustDose('dragon', 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mandrake -->
|
||||
<div class="dose-card">
|
||||
<span style="font-size: 2.2rem;">🌿</span>
|
||||
<strong style="color: var(--text-light); font-size: 0.85rem;">Alraune</strong>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- Step 2: Stir Control Panel -->
|
||||
<div id="stirPanel" style="display: none; flex-direction: column; align-items: center; gap: 0.5rem; 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()">
|
||||
🥄 Kessel umrühren! (<span id="stirCount">0</span> / 3)
|
||||
</button>
|
||||
<span style="color: var(--text-dim); font-size: 0.85rem;">Tippe auf den Button, um den Sud umzurühren</span>
|
||||
<!-- Step 2: Two-Way Stirring Gauge (Links ↺ und Rechts ↻) -->
|
||||
<div id="stirPanel" style="display: none; flex-direction: column; align-items: center; gap: 0.75rem; width: 100%;">
|
||||
<div class="stir-gauge-wrapper">
|
||||
<div style="font-size: 0.95rem; color: var(--gold); font-weight: bold; text-align: center;">
|
||||
🥄 Rühr-Takt: <span id="stirPhaseLabel" style="color: #4cc9f0;">↺ LINKS (Phase 1)</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>
|
||||
|
||||
<!-- Step 3: Heat Control Panel -->
|
||||
@@ -179,6 +227,41 @@
|
||||
</button>
|
||||
</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 -->
|
||||
<div id="successPanel" style="display: none; text-align: center; margin-top: 1rem; width: 100%;">
|
||||
<div class="dungeon-card" style="border-color: #2ecc71;">
|
||||
@@ -199,5 +282,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/brew.js?v=5.0"></script>
|
||||
<script src="/static/js/brew.js?v=6.0"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<div class="canvas-wrapper" id="canvasWrapper">
|
||||
<div class="game-hud">
|
||||
<div>⏱️ Zeit: <strong id="timerVal">0s</strong></div>
|
||||
<div>⭕ Ringe: <strong id="ringsVal" style="color: #ffd700;">0/12</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>
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
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>
|
||||
<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!
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Mobile Flight Cockpit & D-Pad -->
|
||||
@@ -73,5 +88,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/broom.js?v=5.0"></script>
|
||||
<script src="/static/js/broom.js?v=6.0"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,14 +3,75 @@
|
||||
{% block title %}Die Prüfungen des Steins der Weisen · Hogwarts Escape{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="parchment-scroll">
|
||||
<h1 class="parchment-title">⚡ Die Kammern des Steins der Weisen ⚡</h1>
|
||||
<p class="riddle-poem" style="font-size: 1.3rem; margin: 0.5rem 0;">
|
||||
"Die Falltür unter Fluffys Pfoten hat sich geöffnet.<br>
|
||||
Tief unter den Kerkern von Hogwarts wachen uralte Zauber über das größte Geheimnis der Alchemie.<br>
|
||||
Beweise deinen Scharfsinn, dein Geschick auf dem Besen und deine eiserne Logik!"
|
||||
</p>
|
||||
<!-- Interaktiver Hogwarts-Brief & Siegel Intro -->
|
||||
<div class="escape-letter-intro" id="escapeLetterIntro">
|
||||
<div class="antique-envelope-scene" id="envelope">
|
||||
<div class="envelope-base">
|
||||
<div class="envelope-interior"></div>
|
||||
<div class="letter-sheet" id="letterSheet">
|
||||
<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>
|
||||
<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">
|
||||
<h2 style="color: var(--gold); text-align: center; margin-bottom: 0.5rem; font-size: 1.8rem;">
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<!-- Top HUD Bar -->
|
||||
<div class="pixies-hud">
|
||||
<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>
|
||||
⏳ 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>
|
||||
|
||||
@@ -131,5 +131,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/pixies.js?v=5.0"></script>
|
||||
<script src="/static/js/pixies.js?v=6.0"></script>
|
||||
{% endblock %}
|
||||
|
||||
+25
-4
@@ -1,4 +1,25 @@
|
||||
fastapi>=0.110.0
|
||||
uvicorn>=0.29.0
|
||||
jinja2>=3.1.3
|
||||
python-dotenv>=1.0.1
|
||||
annotated-doc==0.0.5
|
||||
annotated-types==0.8.0
|
||||
anyio==4.15.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
|
||||
|
||||
Reference in New Issue
Block a user