Add Player Menu with Inventory drawer, house selection, and mobile-first header

This commit is contained in:
Maxi
2026-09-22 19:02:07 +02:00
parent c5c0bad3e6
commit 9f5e5085ec
9 changed files with 1076 additions and 182 deletions
Regular → Executable
View File
BIN
View File
Binary file not shown.
+125 -20
View File
@@ -21,13 +21,6 @@ app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), na
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
# Standard Classic Riddle Solution (Book Accurate):
# 1: Poison
# 2: Wine
# 3: Ahead (Smallest / Dwarf - passes through black fire)
# 4: Poison
# 5: Poison
# 6: Wine
# 7: Back (Largest / Giant - passes through purple fire)
CLASSIC_SOLUTION = {
1: "poison",
2: "wine",
@@ -38,10 +31,62 @@ CLASSIC_SOLUTION = {
7: "back"
}
ALL_ITEMS = {
"wand": {
"id": "wand",
"name": "Zauberstab",
"icon": "🪄",
"desc": "Stechpalme und Phönixfeder, 11 Zoll. Liegt warm in deiner Hand.",
"action": "lumos",
"action_label": "Lumos / Licht 💡"
},
"broom": {
"id": "broom",
"name": "Nimbus 2000",
"icon": "🧹",
"desc": "Ein pfeilschneller Rennbesen. Perfekt balanciert für Wendemanöver im Schlüsselraum.",
"action": "broom_polish",
"action_label": "Besen polieren ✨"
},
"snape_scroll": {
"id": "snape_scroll",
"name": "Snapes Logik-Rolle",
"icon": "📜",
"desc": "Ein vergilbtes Pergament mit Snapes Vierzeilern zur Flammenkammer.",
"action": "read_scroll",
"action_label": "Rätsel-Verse lesen 📖"
},
"silver_key": {
"id": "silver_key",
"name": "Geknickter Silberschlüssel",
"icon": "🗝️",
"desc": "Der schwere antike Schlüssel aus der Schlüsselkammer. Sein geknickter Flügel verrät ihn.",
"action": "examine_key",
"action_label": "Schloss prüfen 🔍"
},
"philosophers_stone": {
"id": "philosophers_stone",
"name": "Stein der Weisen",
"icon": "💎",
"desc": "Der legendäre blutrote Alchemistenstein. Er schenkt unsterbliches Leben und pure Weisheit!",
"action": "feel_power",
"action_label": "Macht spüren ⚡"
}
}
HOUSES = {
"gryffindor": {"id": "gryffindor", "name": "Gryffindor", "icon": "🦁", "color": "#740001", "accent": "#d3a625"},
"slytherin": {"id": "slytherin", "name": "Slytherin", "icon": "🐍", "color": "#1a472a", "accent": "#aaaaaa"},
"ravenclaw": {"id": "ravenclaw", "name": "Ravenclaw", "icon": "🦅", "color": "#0e1a40", "accent": "#946b2d"},
"hufflepuff": {"id": "hufflepuff", "name": "Hufflepuff", "icon": "🦡", "color": "#ecb939", "accent": "#372e29"}
}
def get_session_state(request: Request) -> dict:
if "escape" not in request.session:
request.session["escape"] = {
"session_id": str(uuid.uuid4()),
"house": "gryffindor",
"inventory": ["wand", "broom", "snape_scroll"],
"broom_solved": False,
"broom_score": 0,
"broom_time": 0,
@@ -51,8 +96,33 @@ def get_session_state(request: Request) -> dict:
"completed_at": None,
"lang": "de"
}
else:
# Ensure new fields exist for existing sessions
state = request.session["escape"]
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if state.get("broom_solved"):
state["inventory"].append("silver_key")
if state.get("bottles_solved"):
state["inventory"].append("philosophers_stone")
if "house" not in state:
state["house"] = "gryffindor"
request.session["escape"] = state
return request.session["escape"]
def get_template_context(request: Request, state: dict, lang: str) -> dict:
inv_ids = state.get("inventory", ["wand", "broom", "snape_scroll"])
inv_items = [ALL_ITEMS[i] for i in inv_ids if i in ALL_ITEMS]
house_info = HOUSES.get(state.get("house", "gryffindor"), HOUSES["gryffindor"])
return {
"state": state,
"lang": lang,
"inventory_items": inv_items,
"all_items": ALL_ITEMS,
"house": house_info,
"houses": HOUSES
}
@app.get("/", response_class=HTMLResponse)
async def page_index(request: Request):
state = get_session_state(request)
@@ -62,7 +132,7 @@ async def page_index(request: Request):
return templates.TemplateResponse(
request=request,
name="index.html",
context={"state": state, "lang": lang}
context=get_template_context(request, state, lang)
)
@app.get("/broom", response_class=HTMLResponse)
@@ -74,7 +144,7 @@ async def page_broom(request: Request):
return templates.TemplateResponse(
request=request,
name="broom.html",
context={"state": state, "lang": lang}
context=get_template_context(request, state, lang)
)
@app.get("/bottles", response_class=HTMLResponse)
@@ -86,7 +156,7 @@ async def page_bottles(request: Request):
return templates.TemplateResponse(
request=request,
name="bottles.html",
context={"state": state, "lang": lang}
context=get_template_context(request, state, lang)
)
@app.get("/sliding", response_class=HTMLResponse)
@@ -98,7 +168,7 @@ async def page_sliding(request: Request):
return templates.TemplateResponse(
request=request,
name="sliding.html",
context={"state": state, "lang": lang}
context=get_template_context(request, state, lang)
)
@app.get("/finale", response_class=HTMLResponse)
@@ -110,7 +180,7 @@ async def page_finale(request: Request):
return templates.TemplateResponse(
request=request,
name="finale.html",
context={"state": state, "lang": lang}
context=get_template_context(request, state, lang)
)
@app.get("/api/state")
@@ -118,10 +188,41 @@ async def api_get_state(request: Request):
state = get_session_state(request)
return JSONResponse(state)
@app.get("/api/inventory")
async def api_get_inventory(request: Request):
state = get_session_state(request)
inv_ids = state.get("inventory", ["wand", "broom", "snape_scroll"])
inv_items = [ALL_ITEMS[i] for i in inv_ids if i in ALL_ITEMS]
house_info = HOUSES.get(state.get("house", "gryffindor"), HOUSES["gryffindor"])
return JSONResponse({
"inventory": inv_items,
"house": house_info,
"score": state.get("broom_score", 0),
"broom_solved": state.get("broom_solved", False),
"bottles_solved": state.get("bottles_solved", False),
"sliding_solved": state.get("sliding_solved", False)
})
@app.post("/api/player/house")
async def api_set_house(request: Request):
try:
data = await request.json()
except Exception:
data = {}
house_id = data.get("house", "gryffindor").lower()
if house_id not in HOUSES:
raise HTTPException(status_code=400, detail="Ungültiges Hogwarts-Haus.")
state = get_session_state(request)
state["house"] = house_id
request.session["escape"] = state
return JSONResponse({"success": True, "house": HOUSES[house_id]})
@app.post("/api/reset")
async def api_reset_state(request: Request):
request.session["escape"] = {
"session_id": str(uuid.uuid4()),
"house": "gryffindor",
"inventory": ["wand", "broom", "snape_scroll"],
"broom_solved": False,
"broom_score": 0,
"broom_time": 0,
@@ -143,28 +244,27 @@ async def api_broom_complete(request: Request):
state["broom_solved"] = True
state["broom_score"] = int(data.get("score", 100))
state["broom_time"] = int(data.get("time_taken", 30))
# Add Silver Key to Inventory!
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if "silver_key" not in state["inventory"]:
state["inventory"].append("silver_key")
request.session["escape"] = state
return JSONResponse({
"success": True,
"message": "Der Schlüssel passt ins Schloss! Die Tür zur Flammenkammer steht offen!",
"new_item": ALL_ITEMS["silver_key"],
"state": state
})
@app.post("/api/bottles/drink")
async def api_bottles_drink(request: Request):
"""
Check the bottle the player decided to drink to pass through the flames.
Bottle 3 is Ahead (Black fire -> Success).
Bottle 7 is Back (Purple fire -> Escaped back).
Bottles 1, 4, 5 are Poison (Fizzle/Defeat).
Bottles 2, 6 are Nettle Wine (Harmless wine).
"""
try:
data = await request.json()
except Exception:
data = {}
bottle_index = int(data.get("bottle", 0))
target_gate = data.get("gate", "ahead") # 'ahead' (black fire) or 'back' (purple fire)
target_gate = data.get("gate", "ahead")
lang = data.get("lang", "de")
if bottle_index not in CLASSIC_SOLUTION:
@@ -178,6 +278,10 @@ async def api_bottles_drink(request: Request):
state["bottles_solved"] = True
if state.get("broom_solved"):
state["completed_at"] = datetime.datetime.utcnow().isoformat()
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if "philosophers_stone" not in state["inventory"]:
state["inventory"].append("philosophers_stone")
request.session["escape"] = state
msg_de = "Ein eisiger Schauer durchströmt dich! Das schwarze Feuer weicht vor dir zurück. Du trittst siegreich in die Schatzkammer des Steins der Weisen!"
msg_en = "An icy chill flows through you! The black flames retreat. You step victoriously into the chamber of the Philosopher's Stone!"
@@ -186,6 +290,7 @@ async def api_bottles_drink(request: Request):
"result": "success_ahead",
"content": content,
"message": msg_de if lang == "de" else msg_en,
"new_item": ALL_ITEMS["philosophers_stone"],
"next_url": "/finale"
})
else:
+529 -125
View File
@@ -1,6 +1,6 @@
:root {
--bg-dark: #0d0c13;
--bg-card: rgba(26, 24, 38, 0.92);
--bg-card: rgba(26, 24, 38, 0.94);
--gold: #e5b948;
--gold-glow: rgba(229, 185, 72, 0.4);
--gold-hover: #f7d274;
@@ -15,6 +15,7 @@
--wine-red: #9e2a2b;
--text-light: #ece8e1;
--text-dim: #a59fa8;
--house-accent: #d3a625;
--font-gothic: 'MedievalSharp', cursive, serif;
--font-script: 'Caveat', cursive, sans-serif;
}
@@ -39,7 +40,7 @@ body {
flex-direction: column;
}
/* Site Header */
/* Site Header (Sleek & Mobile First) */
.site-header {
background: rgba(13, 12, 19, 0.95);
border-bottom: 2px solid var(--gold);
@@ -53,55 +54,89 @@ body {
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0.75rem 1.25rem;
padding: 0.6rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
gap: 0.5rem;
}
.brand-link {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.4rem;
text-decoration: none;
color: var(--gold);
font-size: 1.4rem;
font-size: 1.3rem;
text-shadow: 0 0 10px var(--gold-glow);
transition: transform 0.2s;
white-space: nowrap;
}
.brand-link:hover {
transform: scale(1.03);
.brand-sparkle {
font-size: 1.3rem;
}
.brand-title {
font-weight: bold;
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.6rem;
}
.nav-links {
/* Player Bag Button */
.btn-bag {
background: linear-gradient(135deg, #2b1f14, #4a3520);
color: var(--gold);
border: 2px solid var(--gold);
padding: 0.4rem 0.85rem;
border-radius: 20px;
cursor: pointer;
font-family: inherit;
font-size: 1rem;
font-weight: bold;
display: flex;
align-items: center;
gap: 1rem;
gap: 0.4rem;
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
user-select: none;
}
.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;
.btn-bag:hover {
background: linear-gradient(135deg, #4a3520, #6e4f30);
transform: translateY(-2px) scale(1.04);
box-shadow: 0 4px 15px var(--gold-glow);
color: #fff;
}
.nav-link:hover, .nav-link.active {
color: var(--gold);
border-color: var(--gold);
background: rgba(229, 185, 72, 0.1);
.bag-house-icon {
font-size: 1.15rem;
}
.bag-label {
display: inline-block;
}
.bag-badge {
background: #e63946;
color: #fff;
font-size: 0.75rem;
padding: 0.15rem 0.45rem;
border-radius: 10px;
font-weight: bold;
border: 1px solid #ffd700;
}
.bag-badge.pulse {
animation: badgePulse 0.6s infinite alternate;
}
@keyframes badgePulse {
0% { transform: scale(1); filter: brightness(1); }
100% { transform: scale(1.3); filter: brightness(1.4) drop-shadow(0 0 8px #ff0055); }
}
/* Control Buttons */
@@ -109,15 +144,22 @@ body {
background: rgba(255, 255, 255, 0.08);
color: var(--gold);
border: 1px solid var(--gold);
padding: 0.4rem 0.8rem;
border-radius: 6px;
padding: 0.4rem 0.75rem;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s;
user-select: none;
}
.btn-control.icon-only {
padding: 0.4rem 0.55rem;
font-size: 1.1rem;
border-radius: 50%;
}
.btn-control:hover {
@@ -126,16 +168,371 @@ body {
box-shadow: 0 0 12px var(--gold-glow);
}
.lang-btn.active {
background: var(--gold);
color: var(--bg-dark);
/* Slide-Over Drawer & Backdrop */
.drawer-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.drawer-backdrop.open {
opacity: 1;
pointer-events: auto;
}
.player-drawer {
position: fixed;
top: 0;
right: 0;
width: 400px;
max-width: 92vw;
height: 100vh;
background: var(--bg-dark);
background-image:
radial-gradient(circle at 10% 10%, rgba(229, 185, 72, 0.08) 0%, transparent 40%),
url('/static/img/parchment_texture.jpg');
background-blend-mode: overlay;
border-left: 3px solid var(--gold);
box-shadow: -5px 0 35px rgba(0, 0, 0, 0.85);
z-index: 2001;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.2, 0.9, 0.3, 1);
}
.player-drawer.open {
transform: translateX(0);
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.1rem 1.25rem;
border-bottom: 2px solid rgba(229, 185, 72, 0.3);
background: rgba(17, 15, 25, 0.95);
}
.drawer-title {
font-size: 1.4rem;
color: var(--gold);
display: flex;
align-items: center;
gap: 0.5rem;
}
.drawer-close-btn {
background: none;
border: none;
color: var(--text-dim);
font-size: 1.5rem;
cursor: pointer;
transition: color 0.2s;
line-height: 1;
}
.drawer-close-btn:hover {
color: var(--gold);
}
.drawer-tabs {
display: flex;
border-bottom: 1px solid rgba(229, 185, 72, 0.25);
background: rgba(22, 19, 32, 0.9);
}
.drawer-tab-btn {
flex: 1;
padding: 0.75rem 0.5rem;
background: none;
border: none;
color: var(--text-dim);
font-family: inherit;
font-size: 0.95rem;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.drawer-tab-btn:hover {
color: var(--text-light);
background: rgba(229, 185, 72, 0.05);
}
.drawer-tab-btn.active {
color: var(--gold);
border-bottom-color: var(--gold);
font-weight: bold;
background: rgba(229, 185, 72, 0.1);
}
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 1.25rem;
}
.drawer-tab-pane {
display: none;
}
.drawer-tab-pane.active {
display: block;
animation: fadeIn 0.25s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.drawer-footer {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(229, 185, 72, 0.25);
background: rgba(17, 15, 25, 0.95);
}
/* Inventory Grid (Tab 1) */
.inv-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.inv-item-slot {
background: rgba(26, 23, 38, 0.85);
border: 2px solid rgba(229, 185, 72, 0.35);
border-radius: 10px;
padding: 0.85rem 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.inv-item-slot:hover {
border-color: var(--gold);
transform: translateY(-3px);
box-shadow: 0 4px 15px rgba(229, 185, 72, 0.25);
}
.inv-item-slot.selected {
border-color: var(--gold);
background: rgba(229, 185, 72, 0.15);
box-shadow: 0 0 15px var(--gold-glow);
}
.inv-item-icon {
font-size: 2.2rem;
margin-bottom: 0.35rem;
}
.inv-item-name {
font-size: 0.85rem;
text-align: center;
color: var(--text-light);
font-weight: bold;
}
/* Item Detail Card */
.inv-detail-card {
background: rgba(18, 15, 26, 0.95);
border: 2px solid var(--gold);
border-radius: 12px;
padding: 1.1rem;
text-align: center;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.6);
}
.inv-detail-icon {
font-size: 3rem;
margin-bottom: 0.4rem;
}
.inv-detail-name {
font-size: 1.25rem;
color: var(--gold);
margin-bottom: 0.4rem;
font-weight: bold;
}
.inv-detail-desc {
color: var(--text-dim);
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 1rem;
}
/* House Picker (Tab 2) */
.house-picker-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.house-select-btn {
background: rgba(22, 19, 32, 0.85);
border: 2px solid rgba(229, 185, 72, 0.3);
border-radius: 10px;
padding: 0.75rem 0.5rem;
color: var(--text-light);
font-family: inherit;
font-size: 0.95rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
transition: all 0.2s;
}
.house-select-btn:hover {
border-color: var(--gold);
transform: scale(1.03);
}
.house-select-btn.active {
border-color: var(--gold);
background: rgba(229, 185, 72, 0.2);
box-shadow: 0 0 12px var(--gold-glow);
font-weight: bold;
}
.house-icon {
font-size: 1.8rem;
}
.profile-stats-card {
background: rgba(20, 18, 29, 0.9);
border: 1px solid rgba(229, 185, 72, 0.3);
border-radius: 10px;
padding: 0.9rem 1rem;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.45rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
font-size: 0.9rem;
}
.stat-row:last-child {
border-bottom: none;
}
/* Chambers Navigation (Tab 3) */
.rooms-nav-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.room-nav-item {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.75rem 1rem;
background: rgba(25, 22, 36, 0.85);
border: 1px solid rgba(229, 185, 72, 0.3);
border-radius: 10px;
text-decoration: none;
color: var(--text-light);
transition: all 0.2s;
}
.room-nav-item:hover, .room-nav-item.current {
border-color: var(--gold);
background: rgba(229, 185, 72, 0.15);
transform: translateX(4px);
}
.room-nav-item.external {
border-color: #ffd700;
background: rgba(212, 175, 55, 0.1);
}
.room-icon {
font-size: 1.8rem;
}
.room-info {
display: flex;
flex-direction: column;
}
.room-info strong {
font-size: 1rem;
color: var(--gold);
}
.room-info small {
font-size: 0.8rem;
color: var(--text-dim);
}
/* Floating Toast */
.escape-toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(30px);
background: rgba(18, 14, 26, 0.96);
border: 2px solid var(--gold);
border-radius: 12px;
padding: 0.8rem 1.4rem;
color: var(--text-light);
font-size: 1rem;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.8), 0 0 20px var(--gold-glow);
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-align: center;
max-width: 90vw;
}
.escape-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Lumos Effect Overlay */
.lumos-light {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 500;
background: radial-gradient(circle at 50% 50%, rgba(255, 248, 220, 0.18) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.5s ease;
}
.lumos-light.active {
opacity: 1;
}
/* Container */
.container {
max-width: 1100px;
margin: 1.5rem auto 3rem auto;
margin: 1.2rem auto 2.5rem auto;
padding: 0 1rem;
flex: 1;
width: 100%;
@@ -147,26 +544,26 @@ body {
color: var(--parchment-ink);
border: 4px double var(--parchment-border);
border-radius: 12px;
padding: 2rem;
padding: 1.5rem 1.25rem;
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;
margin-bottom: 1.5rem;
}
.parchment-title {
font-size: 2rem;
font-size: 1.8rem;
text-align: center;
color: #5c1818;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
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;
font-size: 1.35rem;
line-height: 1.7;
text-align: center;
margin: 1.5rem 0;
margin: 1rem 0;
white-space: pre-line;
font-weight: bold;
}
@@ -178,22 +575,22 @@ body {
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}
/* Chamber Overview Grid */
.chamber-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.25rem;
margin-top: 1.25rem;
}
.chamber-card {
background: rgba(20, 18, 30, 0.9);
border: 2px solid rgba(229, 185, 72, 0.3);
border-radius: 12px;
padding: 1.5rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -203,20 +600,20 @@ body {
}
.chamber-card:hover {
transform: translateY(-5px);
transform: translateY(-4px);
border-color: var(--gold);
box-shadow: 0 10px 30px var(--gold-glow);
}
.chamber-badge {
position: absolute;
top: 1rem;
right: 1rem;
top: 0.8rem;
right: 0.8rem;
background: rgba(229, 185, 72, 0.2);
border: 1px solid var(--gold);
color: var(--gold);
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 20px;
}
@@ -227,23 +624,23 @@ body {
}
.chamber-icon {
font-size: 3.5rem;
margin-bottom: 1rem;
font-size: 3rem;
margin-bottom: 0.75rem;
text-align: center;
}
.chamber-title {
font-size: 1.5rem;
font-size: 1.35rem;
color: var(--gold);
margin-bottom: 0.75rem;
margin-bottom: 0.6rem;
text-align: center;
}
.chamber-desc {
color: var(--text-dim);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 1.25rem;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 1rem;
text-align: center;
}
@@ -254,8 +651,8 @@ body {
color: #1a1505;
text-decoration: none;
font-weight: bold;
font-size: 1.1rem;
padding: 0.8rem 1.5rem;
font-size: 1.05rem;
padding: 0.75rem 1.25rem;
border-radius: 8px;
border: none;
cursor: pointer;
@@ -271,13 +668,13 @@ body {
color: #000;
}
/* Flaming Gates (Bottles Riddle) */
/* Flaming Gates */
.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;
padding: 1.5rem 1rem;
box-shadow: inset 0 0 80px rgba(0,0,0,0.9), 0 10px 40px rgba(0,0,0,0.8);
overflow: hidden;
}
@@ -285,13 +682,14 @@ body {
.flame-wall-container {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
position: relative;
gap: 0.75rem;
}
.flame-gate {
width: 48%;
padding: 1.5rem 1rem;
flex: 1;
padding: 1.2rem 0.75rem;
border-radius: 12px;
text-align: center;
position: relative;
@@ -301,17 +699,17 @@ body {
.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);
box-shadow: 0 0 25px 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);
box-shadow: 0 0 25px var(--black-fire-glow);
}
.flame-icon-animated {
font-size: 3rem;
font-size: 2.5rem;
animation: flameFlicker 1.5s infinite alternate;
}
@@ -323,8 +721,8 @@ body {
/* 7 Bottles Table */
.bottles-table-wrapper {
margin: 2rem 0;
padding: 1.5rem 1rem;
margin: 1.5rem 0;
padding: 1.25rem 0.75rem;
background: linear-gradient(180deg, #2a201b 0%, #17110e 100%);
border: 3px solid #6b5338;
border-radius: 14px;
@@ -335,63 +733,63 @@ body {
display: flex;
justify-content: space-around;
align-items: flex-end;
gap: 0.75rem;
gap: 0.5rem;
flex-wrap: nowrap;
overflow-x: auto;
padding: 1rem 0;
padding: 0.5rem 0;
-webkit-overflow-scrolling: touch;
}
.bottle-slot {
display: flex;
flex-direction: column;
align-items: center;
min-width: 95px;
min-width: 85px;
cursor: pointer;
transition: transform 0.2s;
user-select: none;
}
.bottle-slot:hover {
transform: translateY(-8px);
transform: translateY(-6px);
}
.bottle-slot.selected {
transform: translateY(-12px);
transform: translateY(-10px);
filter: drop-shadow(0 0 15px var(--gold));
}
.bottle-pedestal {
width: 80px;
width: 75px;
height: 18px;
background: linear-gradient(180deg, #574635 0%, #2f251c 100%);
border: 2px solid #856a52;
border-radius: 6px;
margin-top: 0.5rem;
margin-top: 0.4rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--gold);
font-weight: bold;
}
/* SVG Bottles */
.bottle-svg {
filter: drop-shadow(0 8px 12px rgba(0,0,0,0.6));
filter: drop-shadow(0 6px 10px rgba(0,0,0,0.6));
transition: filter 0.3s;
}
/* Bottle tags */
.bottle-tag-select {
margin-top: 0.4rem;
margin-top: 0.35rem;
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;
padding: 0.15rem;
border-radius: 4px;
cursor: pointer;
width: 80px;
}
/* Canvas Broom Game */
@@ -410,7 +808,7 @@ body {
#broomCanvas {
display: block;
width: 100%;
height: 520px;
height: 480px;
cursor: crosshair;
}
@@ -418,18 +816,20 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
padding: 0.6rem 1rem;
background: rgba(13, 12, 19, 0.9);
border-bottom: 2px solid var(--gold);
font-size: 1.1rem;
font-size: 1rem;
color: var(--gold);
flex-wrap: wrap;
gap: 0.5rem;
}
.mobile-controls {
display: none;
margin-top: 1rem;
justify-content: center;
gap: 1rem;
gap: 0.8rem;
}
@media (max-width: 768px) {
@@ -438,13 +838,18 @@ body {
}
.flame-wall-container {
flex-direction: column;
gap: 1rem;
}
.flame-gate {
width: 100%;
}
#broomCanvas {
height: 380px;
height: 360px;
}
.brand-title {
font-size: 1.15rem;
}
.bag-label {
display: none; /* Icon and badge only on small screens for ultra compact header */
}
.btn-bag {
padding: 0.35rem 0.6rem;
}
}
@@ -475,9 +880,9 @@ body {
background: var(--bg-card);
border: 2px solid var(--gold);
border-radius: 14px;
max-width: 550px;
max-width: 520px;
width: 100%;
padding: 2rem;
padding: 1.75rem;
text-align: center;
box-shadow: 0 0 50px var(--gold-glow);
transform: scale(0.9);
@@ -489,57 +894,38 @@ body {
}
.modal-icon {
font-size: 4rem;
margin-bottom: 1rem;
font-size: 3.5rem;
margin-bottom: 0.8rem;
}
.modal-title {
font-size: 1.8rem;
font-size: 1.6rem;
color: var(--gold);
margin-bottom: 1rem;
margin-bottom: 0.8rem;
}
.modal-text {
color: var(--text-light);
font-size: 1.1rem;
font-size: 1.05rem;
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;
margin-bottom: 1.25rem;
}
/* Sliding Puzzle (Schieberätsel) */
.sliding-board-container {
max-width: 440px;
margin: 1.5rem auto;
max-width: 380px;
margin: 1.25rem auto;
background: #171421;
border: 3px solid var(--gold);
border-radius: 14px;
padding: 1.25rem;
padding: 1rem;
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;
grid-gap: 8px;
width: 100%;
aspect-ratio: 1 / 1;
}
@@ -556,7 +942,7 @@ body {
user-select: none;
transition: transform 0.15s, background 0.2s;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
padding: 0.5rem;
padding: 0.4rem;
}
.sliding-tile:hover {
@@ -572,19 +958,37 @@ body {
}
.tile-icon {
font-size: 2.2rem;
font-size: 2rem;
}
.tile-num {
font-size: 0.85rem;
font-size: 0.8rem;
color: var(--gold);
font-weight: bold;
}
.tile-name {
font-size: 0.75rem;
font-size: 0.7rem;
color: var(--text-dim);
text-align: center;
margin-top: 2px;
}
/* Footer */
.footer {
background: rgba(10, 9, 15, 0.95);
border-top: 1px solid rgba(229, 185, 72, 0.3);
text-align: center;
padding: 1.25rem 1rem;
color: var(--text-dim);
font-size: 0.85rem;
}
.footer a {
color: var(--gold);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
+4
View File
@@ -124,6 +124,9 @@
if (data.result === 'success_ahead') {
if (window.soundFX) window.soundFX.playMagicSuccess();
if (window.notifyNewItem && data.new_item) {
window.notifyNewItem(data.new_item);
}
modalIcon.innerText = "❄️⚡";
modalTitle.innerText = "Hervorragend kombiniert!";
modalTitle.style.color = "#ffd700";
@@ -133,6 +136,7 @@
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 = "🟣";
+5 -1
View File
@@ -207,11 +207,15 @@
score += 500;
if (scoreVal) scoreVal.innerText = score;
if (window.soundFX) window.soundFX.playKeyChime();
if (window.notifyNewItem) {
window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" });
}
if (statusMsg) {
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen!</strong> Fliege jetzt zur schweren Holztür rechts und öffne das Schloss!";
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen & im Beutel!</strong> Fliege zur Holztür rechts und öffne das Schloss!";
statusMsg.style.color = "#2ecc71";
}
}
} else {
// Master key follows player
masterKey.x = player.x + 10;
+225
View File
@@ -0,0 +1,225 @@
// Player Menu & Inventory (Zauberer-Beutel & Profil)
(function() {
const drawer = document.getElementById('playerDrawer');
const drawerBackdrop = document.getElementById('drawerBackdrop');
const bagBtn = document.getElementById('openBagBtn');
const closeBtn = document.getElementById('closeDrawerBtn');
const bagBadge = document.getElementById('bagBadge');
const bagHouseIcon = document.getElementById('bagHouseIcon');
// Tabs
const tabBtns = document.querySelectorAll('.drawer-tab-btn');
const tabPanes = document.querySelectorAll('.drawer-tab-pane');
// Item Detail Card
const itemDetailBox = document.getElementById('itemDetailBox');
const itemDetailIcon = document.getElementById('itemDetailIcon');
const itemDetailName = document.getElementById('itemDetailName');
const itemDetailDesc = document.getElementById('itemDetailDesc');
const itemDetailActionBtn = document.getElementById('itemDetailActionBtn');
// House selection buttons
const houseBtns = document.querySelectorAll('.house-select-btn');
let currentItem = null;
let lumosActive = false;
window.togglePlayerDrawer = function() {
if (!drawer) return;
const isOpen = drawer.classList.contains('open');
if (isOpen) {
closePlayerDrawer();
} else {
openPlayerDrawer();
}
};
window.openPlayerDrawer = function(tabName) {
if (!drawer) return;
drawer.classList.add('open');
if (drawerBackdrop) drawerBackdrop.classList.add('open');
document.body.style.overflow = 'hidden';
if (window.soundFX) window.soundFX.playStoneSlide();
if (tabName) switchTab(tabName);
};
window.closePlayerDrawer = function() {
if (!drawer) return;
drawer.classList.remove('open');
if (drawerBackdrop) drawerBackdrop.classList.remove('open');
document.body.style.overflow = '';
if (window.soundFX) window.soundFX.playStoneSlide();
};
if (bagBtn) bagBtn.addEventListener('click', window.togglePlayerDrawer);
if (closeBtn) closeBtn.addEventListener('click', window.closePlayerDrawer);
if (drawerBackdrop) drawerBackdrop.addEventListener('click', window.closePlayerDrawer);
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && drawer && drawer.classList.contains('open')) {
window.closePlayerDrawer();
}
});
// Tab Switching
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.dataset.tab;
switchTab(target);
});
});
function switchTab(tabId) {
tabBtns.forEach(b => b.classList.toggle('active', b.dataset.tab === tabId));
tabPanes.forEach(p => p.classList.toggle('active', p.id === tabId));
if (window.soundFX) window.soundFX.playBottleClink();
}
// Item Selection in Bag
const itemSlots = document.querySelectorAll('.inv-item-slot');
itemSlots.forEach(slot => {
slot.addEventListener('click', () => {
selectItem(slot);
});
});
function selectItem(slot) {
itemSlots.forEach(s => s.classList.remove('selected'));
slot.classList.add('selected');
const itemId = slot.dataset.itemId;
const itemName = slot.dataset.itemName;
const itemIcon = slot.dataset.itemIcon;
const itemDesc = slot.dataset.itemDesc;
const actionLabel = slot.dataset.actionLabel;
const action = slot.dataset.action;
currentItem = { id: itemId, name: itemName, icon: itemIcon, action: action };
if (itemDetailBox) {
itemDetailBox.style.display = 'block';
itemDetailIcon.innerText = itemIcon;
itemDetailName.innerText = itemName;
itemDetailDesc.innerText = itemDesc;
itemDetailActionBtn.innerText = actionLabel || "Benutzen ✨";
itemDetailActionBtn.onclick = () => performItemAction(action);
}
if (window.soundFX) window.soundFX.playBottleClink();
}
function performItemAction(action) {
if (action === 'lumos') {
lumosActive = !lumosActive;
let glow = document.getElementById('lumosLightOverlay');
if (!glow) {
glow = document.createElement('div');
glow.id = 'lumosLightOverlay';
glow.className = 'lumos-light';
document.body.appendChild(glow);
}
glow.classList.toggle('active', lumosActive);
itemDetailActionBtn.innerText = lumosActive ? "Nox / Erlöschen 🌑" : "Lumos wirken 💡";
if (window.soundFX) window.soundFX.playKeyChime();
showFloatingToast(lumosActive ? "💡 Lumos! Dein Zauberstab erleuchtet die Kammer." : "🌑 Nox! Das Zauberlicht erlischt.");
} else if (action === 'read_scroll') {
// Open scroll modal
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) {
modalIcon.innerText = "📜";
modalTitle.innerText = "Snapes Logik-Rätsel";
modalText.innerHTML = `
<div style="font-family: 'Caveat', cursive; font-size: 1.35rem; line-height: 1.7; text-align: left; max-height: 50vh; overflow-y: auto; padding: 0.5rem 1rem;">
<em>"Gefahr liegt vor euch, die Rettung zurück,<br>
Zwei von uns helfen euch, habt ihr das Glück,<br>
Eine von sieben führt euch hindurch,<br>
Eine bringt den, der sie trinkt, sicher zurück,<br>
Zwei von ihnen enthalten nur Brennnesselwein,<br>
Drei sind tödlich, schenkt ihr sie ein.<br>
Wählt oder bleibt auf ewig hier stehen,<br>
Vier Hinweise helfen euch, alles zu sehen..."</em><br><br>
<strong>1.</strong> Links neben jedem Wein steht immer Gift.<br>
<strong>2.</strong> Die Enden sind verschieden, keines führt nach vorn.<br>
<strong>3.</strong> Weder Zwerg noch Riese birgt Gift.<br>
<strong>4.</strong> Zweite von links & zweite von rechts schmecken gleich!
</div>
`;
modalBtn.innerText = "Verstanden ➔";
modalBtn.onclick = () => modalBackdrop.classList.remove('show');
modalBackdrop.classList.add('show');
}
} else if (action === 'broom_polish') {
if (window.soundFX) window.soundFX.playWhoosh();
showFloatingToast("🧹 Der Nimbus 2000 glänzt wie neu. Die Zweige sind perfekt gestutzt!");
} else if (action === 'examine_key') {
if (window.soundFX) window.soundFX.playKeyChime();
showFloatingToast("🗝️ Der alte Silberschlüssel ist schwer und trägt Spuren vergangener Kämpfe.");
} else if (action === 'feel_power') {
if (window.soundFX) window.soundFX.playMagicSuccess();
showFloatingToast("💎 Eine unendliche Wärme durchströmt deine Finger. Der Stein schützt die Welt!");
}
}
// House selection
houseBtns.forEach(btn => {
btn.addEventListener('click', async () => {
const house = btn.dataset.house;
try {
const res = await fetch('/api/player/house', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ house: house })
});
const data = await res.json();
if (data.success) {
houseBtns.forEach(b => b.classList.toggle('active', b.dataset.house === house));
if (bagHouseIcon) bagHouseIcon.innerText = data.house.icon;
document.documentElement.style.setProperty('--house-accent', data.house.accent);
if (window.soundFX) window.soundFX.playMagicSuccess();
showFloatingToast(`Du gehörst nun stolz zum Haus ${data.house.name} ${data.house.icon}!`);
}
} catch (e) {
console.error(e);
}
});
});
// Toast Notification
window.showFloatingToast = function(msg) {
let toast = document.getElementById('floatingEscapeToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'floatingEscapeToast';
toast.className = 'escape-toast';
document.body.appendChild(toast);
}
toast.innerHTML = msg;
toast.classList.add('show');
clearTimeout(toast._timeout);
toast._timeout = setTimeout(() => {
toast.classList.remove('show');
}, 3500);
};
window.notifyNewItem = function(item) {
if (!item) return;
if (bagBadge) {
let count = parseInt(bagBadge.innerText, 10) || 0;
bagBadge.innerText = count + 1;
bagBadge.classList.add('pulse');
setTimeout(() => bagBadge.classList.remove('pulse'), 2000);
}
if (window.soundFX) window.soundFX.playMagicSuccess();
window.showFloatingToast(`🎒 <strong>Neuer Gegenstand im Beutel:</strong> ${item.icon} ${item.name}!`);
};
// Auto-select first item if available
if (itemSlots.length > 0) {
selectItem(itemSlots[0]);
}
})();
+163 -17
View File
@@ -8,28 +8,28 @@
<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=1.0">
<link rel="stylesheet" href="/static/css/escape.css?v=2.0">
{% block head %}{% endblock %}
</head>
<body>
<!-- Minimalist & Mobile-Friendly Header -->
<header class="site-header">
<div class="header-inner">
<a href="/" class="brand-link">
<span>🏰⚡</span>
<span class="brand-sparkle">🏰⚡</span>
<span class="brand-title">Hogwarts Escape</span>
</a>
<nav class="nav-links">
<a href="/" class="nav-link {% if request.url.path == '/' %}active{% endif %}">🚪 Kammern</a>
<a href="/broom" class="nav-link {% if request.url.path == '/broom' %}active{% endif %}">🧹 Besenflug</a>
<a href="/bottles" class="nav-link {% if request.url.path == '/bottles' %}active{% endif %}">🧪 Snapes Flaschen</a>
<a href="/sliding" class="nav-link {% if request.url.path == '/sliding' %}active{% endif %}">🧩 Schieberätsel</a>
<a href="https://quiz.dev.waiswuaschd.de" class="nav-link" target="_blank" rel="noopener">📜 Hogwarts-Quiz ↗</a>
</nav>
<div class="header-right">
<button type="button" id="audioToggleBtn" class="btn-control" onclick="toggleSound()">
🔊 <span class="audio-lbl">Ton: An</span>
<button type="button" id="audioToggleBtn" class="btn-control icon-only" onclick="toggleSound()" title="Ton an/aus">
🔊
</button>
<!-- Player Menu & Bag Button -->
<button type="button" id="openBagBtn" class="btn-bag" title="Zauberbeutel & Profil öffnen">
<span id="bagHouseIcon" class="bag-house-icon">{{ house.icon }}</span>
<span class="bag-label">Beutel</span>
<span id="bagBadge" class="bag-badge">{{ inventory_items | length }}</span>
</button>
</div>
</div>
@@ -49,18 +49,164 @@
</div>
</div>
<!-- Slide-Over Player Drawer & Inventory -->
<div id="drawerBackdrop" class="drawer-backdrop"></div>
<div id="playerDrawer" class="player-drawer">
<div class="drawer-header">
<div class="drawer-title">
<span>🎒</span> Zauberer-Cockpit
</div>
<button type="button" id="closeDrawerBtn" class="drawer-close-btn" aria-label="Schließen">✕</button>
</div>
<!-- Navigation Tabs inside Drawer -->
<div class="drawer-tabs">
<button type="button" class="drawer-tab-btn active" data-tab="backpackTab">
🎒 Beutel
</button>
<button type="button" class="drawer-tab-btn" data-tab="profileTab">
🧙‍♂️ Profil
</button>
<button type="button" class="drawer-tab-btn" data-tab="roomsTab">
🗺️ Kammern
</button>
</div>
<div class="drawer-body">
<!-- TAB 1: INVENTORY (BEUTEL) -->
<div id="backpackTab" class="drawer-tab-pane active">
<div class="inv-grid">
{% for item in inventory_items %}
<div class="inv-item-slot"
data-item-id="{{ item.id }}"
data-item-name="{{ item.name }}"
data-item-icon="{{ item.icon }}"
data-item-desc="{{ item.desc }}"
data-action="{{ item.action }}"
data-action-label="{{ item.action_label }}">
<div class="inv-item-icon">{{ item.icon }}</div>
<div class="inv-item-name">{{ item.name }}</div>
</div>
{% endfor %}
</div>
<!-- Item Detail & Interaction Box -->
<div id="itemDetailBox" class="inv-detail-card" style="display: none;">
<div id="itemDetailIcon" class="inv-detail-icon">🪄</div>
<div id="itemDetailName" class="inv-detail-name">Gegenstand</div>
<p id="itemDetailDesc" class="inv-detail-desc">Beschreibung des Gegenstands...</p>
<button type="button" id="itemDetailActionBtn" class="btn-magical" style="padding: 0.6rem 1.2rem; font-size: 0.95rem; width: 100%;">
Benutzen ✨
</button>
</div>
</div>
<!-- TAB 2: PROFILE & HOUSE (HAUS) -->
<div id="profileTab" class="drawer-tab-pane">
<h4 style="color: var(--gold); margin-bottom: 0.75rem; text-align: center;">Wähle dein Hogwarts-Haus:</h4>
<div class="house-picker-grid">
{% for hid, h in houses.items() %}
<button type="button" class="house-select-btn {% if state.house == hid %}active{% endif %}" data-house="{{ hid }}">
<span class="house-icon">{{ h.icon }}</span>
<span class="house-name">{{ h.name }}</span>
</button>
{% endfor %}
</div>
<div class="profile-stats-card">
<div class="stat-row">
<span>⚡ Gesammelte Punkte:</span>
<strong style="color: var(--gold);">{{ state.broom_score }} Pkt</strong>
</div>
<div class="stat-row">
<span>🧹 Besenflug:</span>
<span>{% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
</div>
<div class="stat-row">
<span>🧪 Snapes Flammen:</span>
<span>{% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %}</span>
</div>
<div class="stat-row">
<span>🧩 Alchemie-Schieberätsel:</span>
<span>{% if state.sliding_solved %}✅ Gelöst{% else %}⏳ Frei spielbar{% endif %}</span>
</div>
</div>
</div>
<!-- TAB 3: CHAMBERS MAP (KAMMERN) -->
<div id="roomsTab" class="drawer-tab-pane">
<div class="rooms-nav-list">
<a href="/" class="room-nav-item {% if request.url.path == '/' %}current{% endif %}">
<span class="room-icon">🚪</span>
<div class="room-info">
<strong>Vorhalle & Übersicht</strong>
<small>Die Pforten zu den Verliesen</small>
</div>
</a>
<a href="/broom" class="room-nav-item {% if request.url.path == '/broom' %}current{% endif %}">
<span class="room-icon">🧹</span>
<div class="room-info">
<strong>Kammer I: Der Besenflug</strong>
<small>{% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %}</small>
</div>
</a>
<a href="/bottles" class="room-nav-item {% if request.url.path == '/bottles' %}current{% endif %}">
<span class="room-icon">🧪</span>
<div class="room-info">
<strong>Kammer II: Snapes Flaschen</strong>
<small>{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}</small>
</div>
</a>
<a href="/sliding" class="room-nav-item {% if request.url.path == '/sliding' %}current{% endif %}">
<span class="room-icon">🧩</span>
<div class="room-info">
<strong>Alchemie-Schieberätsel</strong>
<small>{% if state.sliding_solved %}✅ Gelöst{% else %}⚗️ Runen-Schiebepuzzle{% endif %}</small>
</div>
</a>
<a href="/finale" class="room-nav-item {% if request.url.path == '/finale' %}current{% endif %}">
<span class="room-icon">🏆</span>
<div class="room-info">
<strong>Das Finale</strong>
<small>Der Stein der Weisen</small>
</div>
</a>
<a href="https://quiz.dev.waiswuaschd.de" class="room-nav-item external" target="_blank" rel="noopener">
<span class="room-icon">📜</span>
<div class="room-info">
<strong>Hogwarts-Quiz Portal ↗</strong>
<small>Große Wissensprüfung</small>
</div>
</a>
</div>
</div>
</div>
<div class="drawer-footer">
<button type="button" class="btn-control" onclick="resetEscapeProgress()" style="width: 100%; justify-content: center;">
🔄 Fortschritt zurücksetzen
</button>
</div>
</div>
<footer class="footer">
<p>⚡ <strong>Hogwarts Escape Adventure</strong> · Teil des Magischen Zauberer-Kosmos ⚡</p>
<p style="margin-top: 0.5rem;">
<a href="/">Startseite</a> ·
<a href="/broom">Der Besenflug</a> ·
<a href="/bottles">Snapes Flaschenrätsel</a> ·
<a href="/sliding">Alchemie-Schieberätsel</a> ·
<a href="javascript:void(0)" onclick="openPlayerDrawer('backpackTab')">🎒 Zauberbeutel öffnen</a> ·
<a href="https://quiz.dev.waiswuaschd.de" target="_blank" rel="noopener">Hogwarts Quiz Portal</a>
</p>
</footer>
<script src="/static/js/audio.js?v=1.0"></script>
<script src="/static/js/audio.js?v=2.0"></script>
<script src="/static/js/inventory.js?v=2.0"></script>
<script>
async function resetEscapeProgress() {
if (confirm("Möchtest du deinen Escape-Fortschritt und dein Inventar wirklich zurücksetzen?")) {
await fetch('/api/reset', { method: 'POST' });
window.location.reload();
}
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+25 -19
View File
@@ -9,6 +9,7 @@ def test_endpoints():
res = client.get("/")
assert res.status_code == 200, f"Index failed: {res.status_code}"
assert "Die Kammern des Steins der Weisen" in res.text
assert "Zauberer-Cockpit" in res.text
res = client.get("/broom")
assert res.status_code == 200
@@ -26,38 +27,43 @@ def test_endpoints():
assert res.status_code == 200
assert "ESCAPE GEMEISTERT" in res.text
# Test API Broom complete
# Test Inventory API
res = client.get("/api/inventory")
assert res.status_code == 200
inv_data = res.json()
assert "inventory" in inv_data
assert len(inv_data["inventory"]) >= 3
item_ids = [item["id"] for item in inv_data["inventory"]]
assert "wand" in item_ids
assert "broom" in item_ids
assert "snape_scroll" in item_ids
# Test House API
res = client.post("/api/player/house", json={"house": "slytherin"})
assert res.status_code == 200
assert res.json()["house"]["name"] == "Slytherin"
# Test API Broom complete (adds silver_key)
res = client.post("/api/broom/complete", json={"score": 500, "time_taken": 25})
assert res.status_code == 200
data = res.json()
assert data["success"] is True
assert "silver_key" in data["state"]["inventory"]
# Test API Bottles drink: Poison (Bottle 1)
res = client.post("/api/bottles/drink", json={"bottle": 1, "gate": "ahead"})
assert res.status_code == 200
assert res.json()["result"] == "poison"
# Test API Bottles drink: Wine (Bottle 2)
res = client.post("/api/bottles/drink", json={"bottle": 2, "gate": "ahead"})
assert res.status_code == 200
assert res.json()["result"] == "wine"
# Test API Bottles drink: Back (Bottle 7)
res = client.post("/api/bottles/drink", json={"bottle": 7, "gate": "back"})
assert res.status_code == 200
assert res.json()["result"] == "success_back"
# Test API Bottles drink: Ahead / Black fire (Bottle 3 - Dwarf)
# Test API Bottles drink: Ahead / Black fire (Bottle 3 - Dwarf, adds philosophers_stone)
res = client.post("/api/bottles/drink", json={"bottle": 3, "gate": "ahead"})
assert res.status_code == 200
assert res.json()["result"] == "success_ahead"
drink_data = res.json()
assert drink_data["result"] == "success_ahead"
assert "new_item" in drink_data
assert drink_data["new_item"]["id"] == "philosophers_stone"
# Test API Reset
res = client.post("/api/reset")
assert res.status_code == 200
assert res.json()["success"] is True
print("ALL TESTS PASSED SUCCESSFULLY! 🧙‍♂️✨")
print("ALL INVENTORY & GAMEPLAY TESTS PASSED! 🎒✨")
if __name__ == "__main__":
test_endpoints()