Initial commit: Harry Potter Escape Adventure on bootle.dev.waiswuaschd.de

This commit is contained in:
Maxi
2026-09-22 16:26:48 +02:00
commit c5c0bad3e6
22 changed files with 2413 additions and 0 deletions
+590
View File
@@ -0,0 +1,590 @@
:root {
--bg-dark: #0d0c13;
--bg-card: rgba(26, 24, 38, 0.92);
--gold: #e5b948;
--gold-glow: rgba(229, 185, 72, 0.4);
--gold-hover: #f7d274;
--parchment-bg: #f8f1de;
--parchment-border: #8b6d3b;
--parchment-ink: #2b1f14;
--purple-fire: #aa26eb;
--purple-fire-glow: rgba(170, 38, 235, 0.6);
--black-fire: #120d1e;
--black-fire-glow: rgba(67, 18, 114, 0.8);
--poison-green: #2ecc71;
--wine-red: #9e2a2b;
--text-light: #ece8e1;
--text-dim: #a59fa8;
--font-gothic: 'MedievalSharp', cursive, serif;
--font-script: 'Caveat', cursive, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
background-image:
radial-gradient(circle at 15% 20%, rgba(138, 43, 226, 0.12) 0%, transparent 40%),
radial-gradient(circle at 85% 80%, rgba(229, 185, 72, 0.08) 0%, transparent 40%),
url('/static/img/parchment_texture.jpg');
background-blend-mode: overlay;
color: var(--text-light);
font-family: var(--font-gothic);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Site Header */
.site-header {
background: rgba(13, 12, 19, 0.95);
border-bottom: 2px solid var(--gold);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: blur(10px);
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0.75rem 1.25rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
}
.brand-link {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--gold);
font-size: 1.4rem;
text-shadow: 0 0 10px var(--gold-glow);
transition: transform 0.2s;
}
.brand-link:hover {
transform: scale(1.03);
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.nav-links {
display: flex;
align-items: center;
gap: 1rem;
}
.nav-link {
color: var(--text-light);
text-decoration: none;
font-size: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 6px;
border: 1px solid transparent;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
color: var(--gold);
border-color: var(--gold);
background: rgba(229, 185, 72, 0.1);
}
/* Control Buttons */
.btn-control {
background: rgba(255, 255, 255, 0.08);
color: var(--gold);
border: 1px solid var(--gold);
padding: 0.4rem 0.8rem;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s;
}
.btn-control:hover {
background: var(--gold);
color: var(--bg-dark);
box-shadow: 0 0 12px var(--gold-glow);
}
.lang-btn.active {
background: var(--gold);
color: var(--bg-dark);
font-weight: bold;
}
/* Container */
.container {
max-width: 1100px;
margin: 1.5rem auto 3rem auto;
padding: 0 1rem;
flex: 1;
width: 100%;
}
/* Parchment Box */
.parchment-scroll {
background: var(--parchment-bg);
color: var(--parchment-ink);
border: 4px double var(--parchment-border);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7), inset 0 0 40px rgba(139, 109, 59, 0.25);
position: relative;
margin-bottom: 2rem;
}
.parchment-title {
font-size: 2rem;
text-align: center;
color: #5c1818;
margin-bottom: 1rem;
text-shadow: 1px 1px 0px rgba(255,255,255,0.7);
}
.riddle-poem {
font-family: var(--font-script);
font-size: 1.5rem;
line-height: 1.8;
text-align: center;
margin: 1.5rem 0;
white-space: pre-line;
font-weight: bold;
}
/* Dungeon Card */
.dungeon-card {
background: var(--bg-card);
border: 1px solid rgba(229, 185, 72, 0.4);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
margin-bottom: 2rem;
}
/* Chamber Overview Grid */
.chamber-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.chamber-card {
background: rgba(20, 18, 30, 0.9);
border: 2px solid rgba(229, 185, 72, 0.3);
border-radius: 12px;
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.chamber-card:hover {
transform: translateY(-5px);
border-color: var(--gold);
box-shadow: 0 10px 30px var(--gold-glow);
}
.chamber-badge {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(229, 185, 72, 0.2);
border: 1px solid var(--gold);
color: var(--gold);
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
}
.chamber-badge.solved {
background: rgba(46, 204, 113, 0.25);
border-color: #2ecc71;
color: #2ecc71;
}
.chamber-icon {
font-size: 3.5rem;
margin-bottom: 1rem;
text-align: center;
}
.chamber-title {
font-size: 1.5rem;
color: var(--gold);
margin-bottom: 0.75rem;
text-align: center;
}
.chamber-desc {
color: var(--text-dim);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 1.25rem;
text-align: center;
}
/* Magical Buttons */
.btn-magical {
display: inline-block;
background: linear-gradient(135deg, #b8860b, #d4af37, #f7d274);
color: #1a1505;
text-decoration: none;
font-weight: bold;
font-size: 1.1rem;
padding: 0.8rem 1.5rem;
border-radius: 8px;
border: none;
cursor: pointer;
text-align: center;
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.4);
transition: all 0.2s;
text-shadow: 0 1px 1px rgba(255,255,255,0.4);
}
.btn-magical:hover {
transform: translateY(-2px);
box-shadow: 0 6px 22px rgba(212, 175, 55, 0.7);
color: #000;
}
/* Flaming Gates (Bottles Riddle) */
.flame-chamber-arena {
position: relative;
background: radial-gradient(circle at center, #191629 0%, #0c0a14 100%);
border: 2px solid #4a3b63;
border-radius: 16px;
padding: 2rem 1.5rem;
box-shadow: inset 0 0 80px rgba(0,0,0,0.9), 0 10px 40px rgba(0,0,0,0.8);
overflow: hidden;
}
.flame-wall-container {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
position: relative;
}
.flame-gate {
width: 48%;
padding: 1.5rem 1rem;
border-radius: 12px;
text-align: center;
position: relative;
overflow: hidden;
}
.flame-gate.purple {
background: rgba(75, 15, 110, 0.35);
border: 2px solid var(--purple-fire);
box-shadow: 0 0 35px var(--purple-fire-glow);
}
.flame-gate.black {
background: rgba(18, 12, 28, 0.8);
border: 2px solid #5a287a;
box-shadow: 0 0 35px var(--black-fire-glow);
}
.flame-icon-animated {
font-size: 3rem;
animation: flameFlicker 1.5s infinite alternate;
}
@keyframes flameFlicker {
0% { transform: scale(1) rotate(-2deg); opacity: 0.85; }
50% { transform: scale(1.1) rotate(3deg); opacity: 1; filter: drop-shadow(0 0 15px currentColor); }
100% { transform: scale(0.95) rotate(-1deg); opacity: 0.9; }
}
/* 7 Bottles Table */
.bottles-table-wrapper {
margin: 2rem 0;
padding: 1.5rem 1rem;
background: linear-gradient(180deg, #2a201b 0%, #17110e 100%);
border: 3px solid #6b5338;
border-radius: 14px;
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
}
.bottles-table {
display: flex;
justify-content: space-around;
align-items: flex-end;
gap: 0.75rem;
flex-wrap: nowrap;
overflow-x: auto;
padding: 1rem 0;
}
.bottle-slot {
display: flex;
flex-direction: column;
align-items: center;
min-width: 95px;
cursor: pointer;
transition: transform 0.2s;
user-select: none;
}
.bottle-slot:hover {
transform: translateY(-8px);
}
.bottle-slot.selected {
transform: translateY(-12px);
filter: drop-shadow(0 0 15px var(--gold));
}
.bottle-pedestal {
width: 80px;
height: 18px;
background: linear-gradient(180deg, #574635 0%, #2f251c 100%);
border: 2px solid #856a52;
border-radius: 6px;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
color: var(--gold);
font-weight: bold;
}
/* SVG Bottles */
.bottle-svg {
filter: drop-shadow(0 8px 12px rgba(0,0,0,0.6));
transition: filter 0.3s;
}
/* Bottle tags */
.bottle-tag-select {
margin-top: 0.4rem;
background: rgba(13, 12, 19, 0.9);
border: 1px solid var(--gold);
color: var(--text-light);
font-family: inherit;
font-size: 0.75rem;
padding: 0.2rem;
border-radius: 4px;
cursor: pointer;
}
/* Canvas Broom Game */
.canvas-wrapper {
position: relative;
width: 100%;
max-width: 900px;
margin: 0 auto;
border: 3px solid var(--gold);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 0 40px rgba(0,0,0,0.9), 0 0 20px var(--gold-glow);
background: #000;
}
#broomCanvas {
display: block;
width: 100%;
height: 520px;
cursor: crosshair;
}
.game-hud {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background: rgba(13, 12, 19, 0.9);
border-bottom: 2px solid var(--gold);
font-size: 1.1rem;
color: var(--gold);
}
.mobile-controls {
display: none;
margin-top: 1rem;
justify-content: center;
gap: 1rem;
}
@media (max-width: 768px) {
.mobile-controls {
display: flex;
}
.flame-wall-container {
flex-direction: column;
gap: 1rem;
}
.flame-gate {
width: 100%;
}
#broomCanvas {
height: 380px;
}
}
/* Modal Feedback */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 1rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.modal-backdrop.show {
opacity: 1;
pointer-events: auto;
}
.modal-content {
background: var(--bg-card);
border: 2px solid var(--gold);
border-radius: 14px;
max-width: 550px;
width: 100%;
padding: 2rem;
text-align: center;
box-shadow: 0 0 50px var(--gold-glow);
transform: scale(0.9);
transition: transform 0.3s;
}
.modal-backdrop.show .modal-content {
transform: scale(1);
}
.modal-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.modal-title {
font-size: 1.8rem;
color: var(--gold);
margin-bottom: 1rem;
}
.modal-text {
color: var(--text-light);
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 1.5rem;
}
/* Footer */
.footer {
background: rgba(10, 9, 15, 0.95);
border-top: 1px solid rgba(229, 185, 72, 0.3);
text-align: center;
padding: 1.5rem 1rem;
color: var(--text-dim);
font-size: 0.9rem;
}
.footer a {
color: var(--gold);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Sliding Puzzle (Schieberätsel) */
.sliding-board-container {
max-width: 440px;
margin: 1.5rem auto;
background: #171421;
border: 3px solid var(--gold);
border-radius: 14px;
padding: 1.25rem;
box-shadow: 0 0 30px rgba(0,0,0,0.8), inset 0 0 30px rgba(0,0,0,0.6);
}
.sliding-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
width: 100%;
aspect-ratio: 1 / 1;
}
.sliding-tile {
background: linear-gradient(145deg, #2b253b, #1b1725);
border: 2px solid var(--gold);
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
transition: transform 0.15s, background 0.2s;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
padding: 0.5rem;
}
.sliding-tile:hover {
transform: scale(1.03);
background: #39324e;
}
.sliding-tile.blank {
background: transparent;
border: 2px dashed rgba(229, 185, 72, 0.2);
box-shadow: none;
cursor: default;
}
.tile-icon {
font-size: 2.2rem;
}
.tile-num {
font-size: 0.85rem;
color: var(--gold);
font-weight: bold;
}
.tile-name {
font-size: 0.75rem;
color: var(--text-dim);
text-align: center;
margin-top: 2px;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

+162
View File
@@ -0,0 +1,162 @@
// Web Audio API Synthesizer for magical escape sounds
class SoundFX {
constructor() {
this.ctx = null;
this.muted = localStorage.getItem('escape_sound_muted') === 'true';
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
toggleMute() {
this.muted = !this.muted;
localStorage.setItem('escape_sound_muted', this.muted);
return !this.muted;
}
isMuted() {
return this.muted;
}
// Whoosh of flying broomstick
playWhoosh() {
if (this.muted) return;
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const filter = this.ctx.createBiquadFilter();
osc.type = 'sawtooth';
filter.type = 'lowpass';
filter.frequency.setValueAtTime(300, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.15);
filter.frequency.exponentialRampToValueAtTime(200, this.ctx.currentTime + 0.35);
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.35);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.36);
}
// Key caught / fluttering chime
playKeyChime() {
if (this.muted) return;
this.init();
const freqs = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
freqs.forEach((freq, idx) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
const startTime = this.ctx.currentTime + idx * 0.05;
gain.gain.setValueAtTime(0.1, startTime);
gain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(startTime);
osc.stop(startTime + 0.41);
});
}
// Clink of glass bottle
playBottleClink() {
if (this.muted) return;
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(1200, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.12);
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.16);
}
// Success magical harmony
playMagicSuccess() {
if (this.muted) return;
this.init();
const chords = [440, 554.37, 659.25, 880, 1108.73]; // A Major triumph
chords.forEach((freq, i) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.06);
gain.gain.setValueAtTime(0.12, this.ctx.currentTime + i * 0.06);
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + i * 0.06 + 1.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(this.ctx.currentTime + i * 0.06);
osc.stop(this.ctx.currentTime + i * 0.06 + 1.25);
});
}
// Poison fizzle
playPoisonSizzle() {
if (this.muted) return;
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(180, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(90, this.ctx.currentTime + 0.5);
gain.gain.setValueAtTime(0.25, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.51);
}
// Stone slide
playStoneSlide() {
if (this.muted) return;
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.21);
}
}
window.soundFX = new SoundFX();
function updateAudioButtonUI() {
const btn = document.getElementById('audioToggleBtn');
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>';
}
function toggleSound() {
window.soundFX.toggleMute();
updateAudioButtonUI();
}
document.addEventListener('DOMContentLoaded', () => {
updateAudioButtonUI();
document.body.addEventListener('click', () => {
window.soundFX.init();
}, { once: true });
});
+181
View File
@@ -0,0 +1,181 @@
// Snape's 7 Potions Riddle & Interactive Bottle Board
(function() {
let selectedBottle = null;
const slots = document.querySelectorAll('.bottle-slot');
const selectedInfo = document.getElementById('selectedBottleInfo');
const btnDrinkAhead = document.getElementById('btnDrinkAhead');
const btnDrinkBack = document.getElementById('btnDrinkBack');
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');
// Bottle Descriptions based on Book Clues
const bottleDescriptions = {
1: "Eine dunkelbraune Phiole mittlerer Größe. Auf der linken Seite von Flasche 2.",
2: "Eine schlanke, grüne Glasflasche. Schmeckt laut Hinweis 4 exakt wie Flasche 6!",
3: "🔍 Die kleinste Flasche von allen (der 'Zwerg')! Sie enthält kaum mehr als einen einzigen Schluck.",
4: "Ein eigenartiger, kantiger Glaskolben mit trüber, unruhiger Flüssigkeit.",
5: "Eine bauchige Flasche aus Rauchglas. Steht direkt links von Flasche 6.",
6: "Eine elegante Flasche, die Zwillingsschwester von Flasche 2, auch wenn sie äußerlich verschieden wirken.",
7: "🏺 Die größte Flasche in der Reihe (der 'Riese')! Ein schwerer, bauchiger Dekanter."
};
slots.forEach(slot => {
slot.addEventListener('click', (e) => {
if (e.target.tagName === 'SELECT') return; // Don't trigger on tag dropdown
selectSlot(slot);
});
// Drag & Drop / Reordering support
slot.setAttribute('draggable', 'true');
slot.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', slot.dataset.index);
if (window.soundFX) window.soundFX.playStoneSlide();
});
slot.addEventListener('dragover', (e) => {
e.preventDefault();
slot.classList.add('drag-over');
});
slot.addEventListener('dragleave', () => {
slot.classList.remove('drag-over');
});
slot.addEventListener('drop', (e) => {
e.preventDefault();
slot.classList.remove('drag-over');
const sourceIndex = e.dataTransfer.getData('text/plain');
const sourceSlot = document.querySelector(`.bottle-slot[data-index="${sourceIndex}"]`);
if (sourceSlot && sourceSlot !== slot) {
swapSlots(sourceSlot, slot);
}
});
});
function swapSlots(slotA, slotB) {
if (window.soundFX) {
window.soundFX.playStoneSlide();
window.soundFX.playBottleClink();
}
// Swap inner contents except the pedestal number
const tempHTML = slotA.innerHTML;
slotA.innerHTML = slotB.innerHTML;
slotB.innerHTML = tempHTML;
// Restore event listeners on newly swapped selects
attachTagListeners();
selectSlot(slotB);
}
function selectSlot(slot) {
slots.forEach(s => s.classList.remove('selected'));
slot.classList.add('selected');
selectedBottle = parseInt(slot.dataset.index, 10);
if (window.soundFX) window.soundFX.playBottleClink();
if (btnDrinkAhead) btnDrinkAhead.disabled = false;
if (btnDrinkBack) btnDrinkBack.disabled = false;
if (selectedInfo) {
const desc = bottleDescriptions[selectedBottle] || "Eine geheimnisvolle Flasche.";
selectedInfo.innerHTML = `Gewählt: <strong>Flasche ${selectedBottle}</strong> — <em>${desc}</em>`;
}
}
function attachTagListeners() {
document.querySelectorAll('.bottle-tag-select').forEach(sel => {
sel.addEventListener('change', () => {
if (window.soundFX) window.soundFX.playStoneSlide();
});
});
}
attachTagListeners();
// Drink to pass Ahead (Black Fire)
if (btnDrinkAhead) {
btnDrinkAhead.addEventListener('click', () => {
if (!selectedBottle) return;
drinkBottle(selectedBottle, 'ahead');
});
}
// Drink to pass Back (Purple Fire)
if (btnDrinkBack) {
btnDrinkBack.addEventListener('click', () => {
if (!selectedBottle) return;
drinkBottle(selectedBottle, 'back');
});
}
async function drinkBottle(bottleIdx, gate) {
if (window.soundFX) window.soundFX.playBottleClink();
try {
const res = await fetch('/api/bottles/drink', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bottle: bottleIdx, gate: gate, lang: 'de' })
});
const data = await res.json();
if (data.result === 'success_ahead') {
if (window.soundFX) window.soundFX.playMagicSuccess();
modalIcon.innerText = "❄️⚡";
modalTitle.innerText = "Hervorragend kombiniert!";
modalTitle.style.color = "#ffd700";
modalText.innerHTML = `Du hast die <strong>kleinste Flasche (Flasche 3, den 'Zwerg')</strong> getrunken!<br><br>
${data.message}<br><br>
Das schwarze Feuer teilt sich mit einem frostigen Zischen. Der Weg zum Stein der Weisen liegt frei vor dir!`;
modalBtn.innerText = "Zum Finale & Escape-Triumph 🏆";
modalBtn.onclick = () => { window.location.href = data.next_url; };
modalBackdrop.classList.add('show');
} else if (data.result === 'success_back') {
if (window.soundFX) window.soundFX.playMagicSuccess();
modalIcon.innerText = "🟣";
modalTitle.innerText = "Rückzug gesichert!";
modalText.innerHTML = `Du hast die <strong>größte Flasche (Flasche 7, den 'Riesen')</strong> getrunken.<br><br>
${data.message}<br><br>
Du kannst sicher durch die violetten Flammen zurückweichen, um deinen Gefährten zu helfen oder den Besenflug nochmals zu betreten.`;
modalBtn.innerText = "Zurück zum Besenflug 🧹";
modalBtn.onclick = () => { window.location.href = data.next_url; };
modalBackdrop.classList.add('show');
} else if (data.result === 'poison') {
if (window.soundFX) window.soundFX.playPoisonSizzle();
modalIcon.innerText = "💀";
modalTitle.innerText = "Tödliches Gift!";
modalTitle.style.color = "#e74c3c";
modalText.innerHTML = `${data.message}<br><br>
<strong>Snapes Rätsel-Hinweis:</strong><br>
<em>"So schlau sich das Gift auch zu tarnen mag,<br>
stets steht es zur Linken vom Wein im Gelag."</em>`;
modalBtn.innerText = "Erneut analysieren 📜";
modalBtn.onclick = () => { modalBackdrop.classList.remove('show'); };
modalBackdrop.classList.add('show');
} else if (data.result === 'wine') {
if (window.soundFX) window.soundFX.playPoisonSizzle();
modalIcon.innerText = "🍷";
modalTitle.innerText = "Harmloser Brennnesselwein";
modalTitle.style.color = "#e67e22";
modalText.innerHTML = `${data.message}<br><br>
Du spürst keinerlei Schutz vor der Hitze der Flammen. Wenn du jetzt hineintrittst, verbrennst du! Lies die Verse auf dem Pergament noch einmal gründlich.`;
modalBtn.innerText = "Verstanden 📜";
modalBtn.onclick = () => { modalBackdrop.classList.remove('show'); };
modalBackdrop.classList.add('show');
} else {
if (window.soundFX) window.soundFX.playPoisonSizzle();
modalIcon.innerText = "⚠️";
modalTitle.innerText = "Falsche Flamme!";
modalText.innerText = data.message;
modalBtn.innerText = "Zurück";
modalBtn.onclick = () => { modalBackdrop.classList.remove('show'); };
modalBackdrop.classList.add('show');
}
} catch (e) {
console.error(e);
}
}
})();
+425
View File
@@ -0,0 +1,425 @@
// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge)
(function() {
const canvas = document.getElementById('broomCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
// UI elements
const scoreVal = document.getElementById('scoreVal');
const timerVal = document.getElementById('timerVal');
const statusMsg = document.getElementById('statusMsg');
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
let width, height;
function resize() {
width = canvas.parentElement.clientWidth;
height = Math.min(520, window.innerHeight * 0.65);
canvas.width = width;
canvas.height = height;
}
window.addEventListener('resize', resize);
resize();
// Game State
let gameRunning = true;
let startTime = Date.now();
let score = 0;
let keyCaught = false;
let doorUnlocked = false;
// Player (Broomstick)
const player = {
x: 80,
y: height / 2,
vx: 0,
vy: 0,
speed: 5.5,
boost: 1.0,
angle: 0,
trail: []
};
// Keys Swarm
const keys = [];
const NUM_KEYS = 24;
// Master Key (Damaged wing silver key)
const masterKey = {
x: width - 150,
y: height / 2,
vx: -2,
vy: 1.5,
wingPhase: 0,
caught: false,
size: 16
};
// Target Door
const door = {
x: width - 60,
y: height / 2 - 45,
w: 45,
h: 90
};
// Spawn decoy keys
for (let i = 0; i < NUM_KEYS; i++) {
keys.push({
x: Math.random() * (width - 150) + 100,
y: Math.random() * (height - 60) + 30,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 3,
size: Math.random() * 6 + 8,
wingPhase: Math.random() * Math.PI * 2,
wingSpeed: Math.random() * 0.3 + 0.2,
gold: Math.random() > 0.4
});
}
// Controls
const keysDown = {};
window.addEventListener('keydown', e => {
keysDown[e.key.toLowerCase()] = true;
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.6;
});
window.addEventListener('keyup', e => {
keysDown[e.key.toLowerCase()] = false;
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0;
});
// Pointer / Touch control
let pointerTarget = null;
canvas.addEventListener('pointermove', e => {
const rect = canvas.getBoundingClientRect();
pointerTarget = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
});
canvas.addEventListener('pointerdown', e => {
player.boost = 1.6;
if (window.soundFX) window.soundFX.playWhoosh();
});
canvas.addEventListener('pointerup', () => {
player.boost = 1.0;
});
canvas.addEventListener('pointerleave', () => {
pointerTarget = null;
player.boost = 1.0;
});
// Mobile buttons
const btnUp = document.getElementById('mBtnUp');
const btnDown = document.getElementById('mBtnDown');
const btnBoost = document.getElementById('mBtnBoost');
if (btnUp) {
btnUp.addEventListener('pointerdown', () => { keysDown['arrowup'] = true; });
btnUp.addEventListener('pointerup', () => { keysDown['arrowup'] = false; });
}
if (btnDown) {
btnDown.addEventListener('pointerdown', () => { keysDown['arrowdown'] = true; });
btnDown.addEventListener('pointerup', () => { keysDown['arrowdown'] = false; });
}
if (btnBoost) {
btnBoost.addEventListener('pointerdown', () => { player.boost = 1.8; if (window.soundFX) window.soundFX.playWhoosh(); });
btnBoost.addEventListener('pointerup', () => { player.boost = 1.0; });
}
function update() {
if (!gameRunning) return;
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
if (timerVal) timerVal.innerText = elapsedSec + 's';
// Keyboard Movement
let ax = 0, ay = 0;
if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
if (keysDown['arrowdown'] || keysDown['s']) ay += 1;
if (keysDown['arrowleft'] || keysDown['a']) ax -= 1;
if (keysDown['arrowright'] || keysDown['d']) ax += 1;
// Pointer steer
if (pointerTarget) {
const dx = pointerTarget.x - player.x;
const dy = pointerTarget.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 15) {
ax = dx / dist;
ay = dy / dist;
}
}
const curSpeed = player.speed * player.boost;
player.vx = player.vx * 0.85 + ax * curSpeed * 0.2;
player.vy = player.vy * 0.85 + ay * curSpeed * 0.2;
player.x += player.vx;
player.y += player.vy;
// Bounds
player.x = Math.max(30, Math.min(width - 70, player.x));
player.y = Math.max(30, Math.min(height - 30, player.y));
player.angle = Math.atan2(player.vy, player.vx + 2) * 0.5;
// Particles trail
player.trail.push({ x: player.x - 20, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
if (player.trail.length > 20) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.9; p.x -= 2; });
// Update decoy keys
keys.forEach(k => {
k.wingPhase += k.wingSpeed;
k.x += k.vx;
k.y += k.vy;
if (k.x < 50 || k.x > width - 100) k.vx *= -1;
if (k.y < 30 || k.y > height - 30) k.vy *= -1;
});
// Update Master Key
if (!masterKey.caught) {
masterKey.wingPhase += 0.35;
// Evasive maneuver if player is close
const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y);
if (dToPlayer < 140) {
masterKey.vx += (masterKey.x - player.x) * 0.04;
masterKey.vy += (masterKey.y - player.y) * 0.04;
} else {
masterKey.vx += (Math.random() - 0.5) * 0.8;
masterKey.vy += (Math.random() - 0.5) * 0.8;
}
masterKey.vx = Math.max(-5, Math.min(5, masterKey.vx * 0.95));
masterKey.vy = Math.max(-4, Math.min(4, masterKey.vy * 0.95));
masterKey.x += masterKey.vx;
masterKey.y += masterKey.vy;
masterKey.x = Math.max(120, Math.min(width - 120, masterKey.x));
masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y));
// Catch check
if (dToPlayer < 35) {
masterKey.caught = true;
keyCaught = true;
score += 500;
if (scoreVal) scoreVal.innerText = score;
if (window.soundFX) window.soundFX.playKeyChime();
if (statusMsg) {
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen!</strong> Fliege jetzt zur schweren Holztür rechts und öffne das Schloss!";
statusMsg.style.color = "#2ecc71";
}
}
} else {
// Master key follows player
masterKey.x = player.x + 10;
masterKey.y = player.y - 15;
// Check if player reached the door
const dToDoor = Math.hypot(player.x - (door.x + door.w / 2), player.y - (door.y + door.h / 2));
if (dToDoor < 55 && !doorUnlocked) {
doorUnlocked = true;
gameRunning = false;
onGameWin(elapsedSec);
}
}
}
function render() {
ctx.clearRect(0, 0, width, height);
// Dungeon stone background & arches
ctx.fillStyle = '#0f0e17';
ctx.fillRect(0, 0, width, height);
// Stone pillars
ctx.fillStyle = 'rgba(25, 22, 36, 0.7)';
for (let x = 120; x < width - 100; x += 180) {
ctx.fillRect(x, 0, 35, height);
ctx.strokeStyle = '#352e47';
ctx.lineWidth = 2;
ctx.strokeRect(x, 0, 35, height);
}
// Vaulted arches at top
ctx.strokeStyle = '#2d273d';
ctx.lineWidth = 4;
ctx.beginPath();
for (let x = 0; x < width; x += 90) {
ctx.moveTo(x, 0);
ctx.quadraticCurveTo(x + 45, 60, x + 90, 0);
}
ctx.stroke();
// The Door
ctx.fillStyle = doorUnlocked ? '#1f6f3e' : '#4a2f16';
ctx.fillRect(door.x, door.y, door.w, door.h);
ctx.strokeStyle = '#e5b948';
ctx.lineWidth = 3;
ctx.strokeRect(door.x, door.y, door.w, door.h);
// Keyhole
ctx.fillStyle = keyCaught ? '#ffd700' : '#111';
ctx.beginPath();
ctx.arc(door.x + door.w / 2, door.y + door.h / 2 - 6, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(door.x + door.w / 2 - 3, door.y + door.h / 2 - 6, 6, 14);
if (keyCaught) {
ctx.shadowColor = '#ffd700';
ctx.shadowBlur = 15;
}
// Sparkle trail
player.trail.forEach(p => {
ctx.fillStyle = `rgba(229, 185, 72, ${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.shadowBlur = 0;
// Decoy keys
keys.forEach(k => {
ctx.save();
ctx.translate(k.x, k.y);
// wings
const wingY = Math.sin(k.wingPhase) * (k.size * 0.7);
ctx.fillStyle = k.gold ? 'rgba(255, 215, 0, 0.7)' : 'rgba(200, 220, 255, 0.65)';
ctx.beginPath();
ctx.ellipse(-k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, 0.3, 0, Math.PI * 2);
ctx.ellipse(k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, -0.3, 0, Math.PI * 2);
ctx.fill();
// Key body
ctx.fillStyle = k.gold ? '#e5b948' : '#c0c5cc';
ctx.beginPath();
ctx.arc(0, 0, k.size * 0.35, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(-1.5, 0, 3, k.size * 1.1);
ctx.fillRect(0, k.size * 0.7, k.size * 0.4, 2.5);
ctx.restore();
});
// Master Key (Damaged Wing)
ctx.save();
ctx.translate(masterKey.x, masterKey.y);
const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8);
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = masterKey.caught ? 5 : 12;
// Right intact wing
ctx.fillStyle = 'rgba(180, 225, 255, 0.85)';
ctx.beginPath();
ctx.ellipse(masterKey.size * 0.7, mWingY, masterKey.size * 0.9, masterKey.size * 0.4, -0.3, 0, Math.PI * 2);
ctx.fill();
// Left BENT wing (as in the book!)
ctx.fillStyle = 'rgba(140, 190, 230, 0.75)';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-masterKey.size * 0.5, mWingY * 0.6);
ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3); // bent angle!
ctx.lineTo(-masterKey.size * 0.4, mWingY * 0.2);
ctx.closePath();
ctx.fill();
// Heavy antique key body
ctx.fillStyle = '#e0e8f0';
ctx.beginPath();
ctx.arc(0, 0, masterKey.size * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(-2, 0, 4, masterKey.size * 1.3);
ctx.fillRect(0, masterKey.size * 0.8, masterKey.size * 0.5, 3);
ctx.fillRect(0, masterKey.size * 1.05, masterKey.size * 0.4, 3);
ctx.restore();
ctx.shadowBlur = 0;
// Player (Broom & Wizard)
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.angle);
// Broomstick handle
ctx.strokeStyle = '#5a3d1b';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(-35, 2);
ctx.lineTo(30, -2);
ctx.stroke();
// Broom twigs
ctx.fillStyle = '#8a6229';
ctx.beginPath();
ctx.moveTo(-35, 2);
ctx.lineTo(-55, -8);
ctx.lineTo(-58, 12);
ctx.closePath();
ctx.fill();
// Wizard silhouette (Cloak & Hat)
ctx.fillStyle = '#1c172a';
ctx.beginPath();
ctx.arc(5, -12, 7, 0, Math.PI * 2); // head
ctx.fill();
// Hat
ctx.fillStyle = '#120e1e';
ctx.beginPath();
ctx.moveTo(-4, -13);
ctx.lineTo(14, -13);
ctx.lineTo(2, -26);
ctx.closePath();
ctx.fill();
// Cloak fluttering
ctx.fillStyle = '#7a1f28'; // Gryffindor red cloak
ctx.beginPath();
ctx.moveTo(0, -6);
ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3);
ctx.lineTo(-24, 6);
ctx.lineTo(10, 0);
ctx.closePath();
ctx.fill();
// Wand tip spark
ctx.fillStyle = '#ffff99';
ctx.shadowColor = '#ffff00';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(24, -8, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
}
function loop() {
update();
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
async function onGameWin(timeTaken) {
if (window.soundFX) window.soundFX.playMagicSuccess();
try {
await fetch('/api/broom/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score: score, time_taken: timeTaken })
});
} catch (e) {
console.error(e);
}
modalTitle.innerText = "🏆 Raum 1 gemeistert!";
modalText.innerHTML = `Mit waghalsigem Flug hast du den großen Silberschlüssel mit dem geknickten Flügel erwischt und das schwere Schloss entriegelt!<br><br>
<strong>Flugzeit:</strong> ${timeTaken} Sekunden<br>
<strong>Punkte:</strong> ${score}<br><br>
Die Tür schwingt knarrend auf... Vor dir lodern violette und schwarze Flammen in Snapes Prüfung!`;
modalBtn.innerText = "Weiter zur Flammenkammer 🧪";
modalBtn.onclick = () => { window.location.href = '/bottles'; };
modalBackdrop.classList.add('show');
}
})();
+147
View File
@@ -0,0 +1,147 @@
// Alchemy Potion Sliding Block Puzzle (Schieberätsel)
(function() {
const gridEl = document.getElementById('slidingGrid');
if (!gridEl) return;
const movesVal = document.getElementById('slidingMoves');
const timerVal = document.getElementById('slidingTimer');
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
const btnScramble = document.getElementById('btnScramble');
const SIZE = 3; // 3x3 sliding puzzle (8 tiles + 1 blank)
let tiles = [];
let blankIndex = 8;
let moves = 0;
let timerInterval = null;
let secondsElapsed = 0;
let isSolved = false;
// Tile images / potion runes
const tileSymbols = [
{ id: 1, icon: "🧪", name: "Alraune", color: "#2ecc71" },
{ id: 2, icon: "🍷", name: "Brennnesselwein", color: "#e74c3c" },
{ id: 3, icon: "⚡", name: "Blitzphiole", color: "#f1c40f" },
{ id: 4, icon: "🔮", name: "Phönix-Träne", color: "#9b59b6" },
{ id: 5, icon: "🐉", name: "Drachenblut", color: "#e67e22" },
{ id: 6, icon: "💀", name: "Tobsuchtsgift", color: "#1abc9c" },
{ id: 7, icon: "💎", name: "Elixier des Lebens", color: "#3498db" },
{ id: 8, icon: "🕯️", name: "Ewige Flamme", color: "#d35400" },
{ id: 0, icon: "", name: "Leerer Runen-Sockel", color: "transparent" } // Blank
];
function init() {
tiles = [1, 2, 3, 4, 5, 6, 7, 8, 0];
blankIndex = 8;
moves = 0;
secondsElapsed = 0;
isSolved = false;
if (movesVal) movesVal.innerText = moves;
if (timerVal) timerVal.innerText = '0s';
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (!isSolved) {
secondsElapsed++;
if (timerVal) timerVal.innerText = secondsElapsed + 's';
}
}, 1000);
scramble();
render();
}
function scramble() {
// Perform 60 random valid moves from blank to ensure solvability
for (let i = 0; i < 60; i++) {
const neighbors = getNeighbors(blankIndex);
const randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
tiles[blankIndex] = tiles[randomNeighbor];
tiles[randomNeighbor] = 0;
blankIndex = randomNeighbor;
}
moves = 0;
if (movesVal) movesVal.innerText = moves;
}
function getNeighbors(idx) {
const row = Math.floor(idx / SIZE);
const col = idx % SIZE;
const res = [];
if (row > 0) res.push(idx - SIZE); // Up
if (row < SIZE - 1) res.push(idx + SIZE); // Down
if (col > 0) res.push(idx - 1); // Left
if (col < SIZE - 1) res.push(idx + 1); // Right
return res;
}
function render() {
gridEl.innerHTML = '';
tiles.forEach((val, idx) => {
const tile = document.createElement('div');
tile.className = 'sliding-tile' + (val === 0 ? ' blank' : '');
tile.dataset.index = idx;
if (val !== 0) {
const info = tileSymbols.find(s => s.id === val);
tile.innerHTML = `
<span class="tile-icon">${info.icon}</span>
<span class="tile-num">${val}</span>
<span class="tile-name">${info.name}</span>
`;
tile.style.borderColor = info.color;
}
tile.addEventListener('click', () => onTileClick(idx));
gridEl.appendChild(tile);
});
}
function onTileClick(idx) {
if (isSolved) return;
const neighbors = getNeighbors(idx);
if (neighbors.includes(blankIndex)) {
// Swap with blank
tiles[blankIndex] = tiles[idx];
tiles[idx] = 0;
blankIndex = idx;
moves++;
if (movesVal) movesVal.innerText = moves;
if (window.soundFX) window.soundFX.playStoneSlide();
render();
checkWin();
}
}
function checkWin() {
const winning = [1, 2, 3, 4, 5, 6, 7, 8, 0];
const match = tiles.every((v, i) => v === winning[i]);
if (match) {
isSolved = true;
clearInterval(timerInterval);
if (window.soundFX) window.soundFX.playMagicSuccess();
fetch('/api/sliding/complete', { method: 'POST' }).catch(console.error);
modalTitle.innerText = "✨ Schieberätsel gelöst!";
modalText.innerHTML = `Mit geschicktem Schieben hast du alle 8 alchemistischen Phiolen in die perfekte harmonische Ordnung gebracht!<br><br>
<strong>Züge:</strong> ${moves}<br>
<strong>Zeit:</strong> ${secondsElapsed} Sekunden<br><br>
Die Runenplatte leuchtet golden auf und der Weg zur Flammenkammer öffnet sich!`;
modalBtn.innerText = "Zur Flammenkammer 🧪";
modalBtn.onclick = () => { window.location.href = '/bottles'; };
modalBackdrop.classList.add('show');
}
}
if (btnScramble) {
btnScramble.addEventListener('click', () => {
if (window.soundFX) window.soundFX.playStoneSlide();
init();
});
}
init();
})();