diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc old mode 100755 new mode 100644 index 71f543c..91b5932 Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/main.py b/app/main.py index 5e54e2b..8cecfbf 100755 --- a/app/main.py +++ b/app/main.py @@ -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: diff --git a/app/static/css/escape.css b/app/static/css/escape.css index 716485b..7f87aa9 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -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; +} diff --git a/app/static/js/bottles.js b/app/static/js/bottles.js index 959eed6..ebb7a07 100755 --- a/app/static/js/bottles.js +++ b/app/static/js/bottles.js @@ -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 = "🟣"; diff --git a/app/static/js/broom.js b/app/static/js/broom.js index 168d359..9be927b 100755 --- a/app/static/js/broom.js +++ b/app/static/js/broom.js @@ -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 = "✨ Schlüssel gefangen! Fliege jetzt zur schweren Holztür rechts und öffne das Schloss!"; + statusMsg.innerHTML = "✨ Schlüssel gefangen & im Beutel! Fliege zur Holztür rechts und öffne das Schloss!"; statusMsg.style.color = "#2ecc71"; } } + } else { // Master key follows player masterKey.x = player.x + 10; diff --git a/app/static/js/inventory.js b/app/static/js/inventory.js new file mode 100755 index 0000000..11ab456 --- /dev/null +++ b/app/static/js/inventory.js @@ -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 = ` +
+ "Gefahr liegt vor euch, die Rettung zurück,
+ Zwei von uns helfen euch, habt ihr das Glück,
+ Eine von sieben führt euch hindurch,
+ Eine bringt den, der sie trinkt, sicher zurück,
+ Zwei von ihnen enthalten nur Brennnesselwein,
+ Drei sind tödlich, schenkt ihr sie ein.
+ Wählt oder bleibt auf ewig hier stehen,
+ Vier Hinweise helfen euch, alles zu sehen..."


+ 1. Links neben jedem Wein steht immer Gift.
+ 2. Die Enden sind verschieden, keines führt nach vorn.
+ 3. Weder Zwerg noch Riese birgt Gift.
+ 4. Zweite von links & zweite von rechts schmecken gleich! +
+ `; + 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(`🎒 Neuer Gegenstand im Beutel: ${item.icon} ${item.name}!`); + }; + + // Auto-select first item if available + if (itemSlots.length > 0) { + selectItem(itemSlots[0]); + } +})(); diff --git a/app/templates/base.html b/app/templates/base.html index 8bbc2c2..e036f43 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -8,28 +8,28 @@ - + {% block head %}{% endblock %} +