From 2ce944d1cd6790967f472390272ed412d6224528 Mon Sep 17 00:00:00 2001 From: Maxi Date: Tue, 22 Sep 2026 19:43:12 +0200 Subject: [PATCH] Feature: Integrate all four Hogwarts mini-games (Duel, Potion Brewing, Cryptex Lock, Cornish Pixies) --- app/__pycache__/main.cpython-313.pyc | Bin 17274 -> 23322 bytes app/main.py | 168 ++++++++- app/static/css/escape.css | 492 +++++++++++++++++++++++++++ app/static/js/brew.js | 227 ++++++++++++ app/static/js/cryptex.js | 137 ++++++++ app/static/js/inventory.js | 12 + app/static/js/pixies.js | 188 ++++++++++ app/templates/base.html | 37 +- app/templates/brew.html | 105 ++++++ app/templates/cryptex.html | 99 ++++++ app/templates/index.html | 45 ++- app/templates/pixies.html | 103 ++++++ test_app.py | 62 +++- 13 files changed, 1662 insertions(+), 13 deletions(-) create mode 100755 app/static/js/brew.js create mode 100755 app/static/js/cryptex.js create mode 100755 app/static/js/pixies.js create mode 100755 app/templates/brew.html create mode 100755 app/templates/cryptex.html create mode 100755 app/templates/pixies.html diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index f6fc3f068d2258230848c4eadea0ac97a707c187..8f8c2670ab3ace5bfdd9c367c58bab9eb5bdc322 100644 GIT binary patch delta 7658 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 Y08pn_IQSr{k}(onXieu~4Q|>00gz#p@Bjb+ delta 3676 zcma)8Yiv}<6~43Hr@iZ47r)kP!}*t2wF9*RDM+XQ!RCVB&a`n&fN8y z-6GMI?sw;&^UaxaX3jaY-~9!7{0*{s#o@@8;8*s+duNnglJpQiR(}QQ!OC6JyPNwB z^GzmJO?T2=^m+OM-A%vRWTJa$2koR?bg%g8ru*oA+CzKAS06n<`{_Y?NPHb`GO`*v zFkz;H6Sb{II#eN@F|s-#IWkd8k4|h98Y`tjvuUFE=-{d+;?4i&GFJ~z*yzYE15Rr- z&{3qerCDqz zD~%bJKZ|uLi?ubw3S_ZPXR(@AS+&88NBS;=vUuAvJUNFqnZ;`cUJ!Syb}ENAoyBX( z@WMH~Gg-Xt8Qx3|FOtP;&G4c*yjT{mEyIiF@Df?P9U0zi4v%H=+NX$5(Gr&55HhE= z+a43G!_iO5Osocumo*?XA~Yd1^LGk8WR8DaSXHnQ1tkcZ_)6iAT`f4=fv^+ddH%7p zy2t^;Ja|@G43hMQLpB_>2a;x42?i1}Lm%(t zJrNp4)x*FyDQO7FckKM}I?68;cUJ5N5j%h&qVLC1Gr~UpsQ571%{xj?s`qrZf11a&Ie&ljiC8oFXf}JT{7-wyr~m{0D~jAErrj-{1(eBcFxs#`)=q zjqdH_I6vjCFin9}(dOMZ$iV?5bRl#Ayp$h~h2-;1(`qyVnrpkRJ@y;O zBA1;9gG=xz{O)>26z;9-C1?3`-KGr_xSj~EkjbGilY^>1I4vs*dj&LaYCox~Cw)2V z+fT<;H6km|cEK)ONYmLof7|mS-&0db6s=*CiHzpjRU+X~I5zeF+D+5hB6#|9jAH>o z(Js~-$v}?ng2`|!5Rsp4x zNQC`Uvg%jVfGV>~p!$QvnXk9N4#Tg*v+_RSLzNq}hrU;dk=mOq{O%U_ykmeprFd8` zOb{)A^}wh{uX<7N2jLs)L`)J@Wm1B1CJSCJ%_MSP&-MUXJdJ<>Ky`{*=@BU(L~#f~ zMwkRp%m7dU@YD08Xf?oa?5rG9KlN6PV}&i zBJ89w6i@?6tLR7ma7Z*9#Rx2ncY_|VI8g;oRIu0^p#29t3g@$XE4a7$em-6#;$;8D z|I<9i$DccCvX4kd?RZo3yU+QCun)3M0KJ{-ZEG(Iv3Cu>RJvbW0p0|QPcewi3m~ga zcMR+*=#!Qx$e{K3tZWD;9pRmgI4 zBgZ~lkNQ4@4z9Hx8bD7OHW{&rkeSzTd>uhF-n?*3P4`=riR4Q+YQ8#atwHj`ZRPpj z2e|>FQf|RkS7}$;enl$8wYOF&;sO*Gz(GFU-ZWxoM z%CTynQS7MjGii;<=lo$+j_Mw#i)0L)$?Po?sC*%f(wB6qP=*nXm{ZwGH0_E5l3_ zxHte2?qPIkE#_wY?9k5k)stFGJE&oiCT2MBxrmy+@?f{cY24x;9(d0sPUlBB@ixK| zU+n+1@+?j^={xd-4heU(mk-`ExYo|_dng^{Ck8xv_oMiY|1a1G!c%t5XVY5?=Wh*c z&;u1~kMny2HC5uy5!;x)9{eE|j?J)l!CI~69DJB(#tsL`^~KTA+8Z#0zYC)8uANQR zvG7@1Vb*{*N%(a-ShO=~h%4*}+7MAt?GZJQ`L|J!>4DGy!a-HHJA|4k1pHSbE_iV- zi`!FNhio1diwGAHE+Js^OBxe#MHQ`qVWlz5T#*qX9)@nQuOU2f;aYb`uIm1h17m$>2#DV3b dict: if "escape" not in request.session: request.session["escape"] = { @@ -91,6 +117,9 @@ def get_session_state(request: Request) -> dict: "broom_score": 0, "broom_time": 0, "duel_solved": False, + "brew_solved": False, + "cryptex_solved": False, + "pixies_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -101,14 +130,26 @@ def get_session_state(request: Request) -> dict: state = request.session["escape"] if "inventory" not in state: state["inventory"] = ["wand", "broom", "snape_scroll"] - if state.get("duel_solved"): + if state.get("duel_solved") and "duel_badge" not in state["inventory"]: state["inventory"].append("duel_badge") - if state.get("bottles_solved"): + if state.get("brew_solved") and "felix_felicis" not in state["inventory"]: + state["inventory"].append("felix_felicis") + if state.get("cryptex_solved") and "cryptex_key" not in state["inventory"]: + state["inventory"].append("cryptex_key") + if state.get("pixies_solved") and "pixie_jar" not in state["inventory"]: + state["inventory"].append("pixie_jar") + if state.get("bottles_solved") and "philosophers_stone" not in state["inventory"]: state["inventory"].append("philosophers_stone") if "house" not in state: state["house"] = "gryffindor" if "duel_solved" not in state: state["duel_solved"] = False + if "brew_solved" not in state: + state["brew_solved"] = False + if "cryptex_solved" not in state: + state["cryptex_solved"] = False + if "pixies_solved" not in state: + state["pixies_solved"] = False request.session["escape"] = state return request.session["escape"] @@ -161,6 +202,42 @@ async def page_duel(request: Request): context=get_template_context(request, state, lang) ) +@app.get("/brew", response_class=HTMLResponse) +async def page_brew(request: Request): + state = get_session_state(request) + lang = request.query_params.get("lang", state.get("lang", "de")) + state["lang"] = lang + request.session["escape"] = state + return templates.TemplateResponse( + request=request, + name="brew.html", + context=get_template_context(request, state, lang) + ) + +@app.get("/cryptex", response_class=HTMLResponse) +async def page_cryptex(request: Request): + state = get_session_state(request) + lang = request.query_params.get("lang", state.get("lang", "de")) + state["lang"] = lang + request.session["escape"] = state + return templates.TemplateResponse( + request=request, + name="cryptex.html", + context=get_template_context(request, state, lang) + ) + +@app.get("/pixies", response_class=HTMLResponse) +async def page_pixies(request: Request): + state = get_session_state(request) + lang = request.query_params.get("lang", state.get("lang", "de")) + state["lang"] = lang + request.session["escape"] = state + return templates.TemplateResponse( + request=request, + name="pixies.html", + context=get_template_context(request, state, lang) + ) + @app.get("/chess") async def redirect_chess(): # Graceful redirect for old links @@ -219,6 +296,9 @@ async def api_get_inventory(request: Request): "score": state.get("broom_score", 0), "broom_solved": state.get("broom_solved", False), "duel_solved": state.get("duel_solved", False), + "brew_solved": state.get("brew_solved", False), + "cryptex_solved": state.get("cryptex_solved", False), + "pixies_solved": state.get("pixies_solved", False), "bottles_solved": state.get("bottles_solved", False), "sliding_solved": state.get("sliding_solved", False) }) @@ -247,6 +327,9 @@ async def api_reset_state(request: Request): "broom_score": 0, "broom_time": 0, "duel_solved": False, + "brew_solved": False, + "cryptex_solved": False, + "pixies_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -283,11 +366,90 @@ async def api_duel_complete(request: Request): request.session["escape"] = state return JSONResponse({ "success": True, - "message": "Zauberspruch-Duell gewonnen! Die Pforte zur Flammenkammer steht weit offen!", + "message": "Zauberspruch-Duell gewonnen! Die Pforte zur Brauküche steht weit offen!", "new_item": ALL_ITEMS["duel_badge"], "state": state }) +@app.post("/api/brew/complete") +async def api_brew_complete(request: Request): + state = get_session_state(request) + state["brew_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "felix_felicis" not in state["inventory"]: + state["inventory"].append("felix_felicis") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Goldener Glanz erfüllt den Kessel! Felix Felicis ist meisterhaft gebraut.", + "new_item": ALL_ITEMS["felix_felicis"], + "state": state + }) + +@app.post("/api/cryptex/unlock") +async def api_cryptex_unlock(request: Request): + try: + data = await request.json() + except Exception: + data = {} + combo = data.get("combination", []) + + # Check match against CRYPTEX_SOLUTION ["🦁", "🌙", "🔥", "🪄"] + is_correct = False + if combo == CRYPTEX_SOLUTION: + is_correct = True + elif combo == [0, 1, 0, 1]: + is_correct = True + elif combo == ["lion", "moon", "fire", "wand"]: + is_correct = True + + if not is_correct: + return JSONResponse({ + "success": False, + "message": "Die Runenzylinder rasten nicht ein. Die Gravur bleibt verschlossen!" + }) + + state = get_session_state(request) + state["cryptex_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "cryptex_key" not in state["inventory"]: + state["inventory"].append("cryptex_key") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Das Runenschloss springt mit einem goldenen Glühen auf! Der Schlüssel der Gründer gehört dir.", + "new_item": ALL_ITEMS["cryptex_key"], + "state": state + }) + +@app.post("/api/pixies/complete") +async def api_pixies_complete(request: Request): + try: + data = await request.json() + except Exception: + data = {} + caught = int(data.get("caught", 0)) + if caught < 10: + return JSONResponse({ + "success": False, + "message": f"Du hast erst {caught}/10 Wichtel gebannt. Halte durch!" + }) + state = get_session_state(request) + state["pixies_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "pixie_jar" not in state["inventory"]: + state["inventory"].append("pixie_jar") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Alle frechen Wichtel mit Immobilus gebannt! Das Wichtelglas wandert in deinen Beutel.", + "new_item": ALL_ITEMS["pixie_jar"], + "state": state + }) + @app.post("/api/bottles/drink") async def api_bottles_drink(request: Request): try: diff --git a/app/static/css/escape.css b/app/static/css/escape.css index c71a7ce..3759464 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -1190,3 +1190,495 @@ body { } } +/* ======================================================== + MINI-GAME: DIE ZAUBERTRANK-BRAUKÜCHE (/brew) + ======================================================== */ +.brew-container { + max-width: 650px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; +} + +.recipe-book { + background: linear-gradient(135deg, #2b1f14, #19120b); + border: 2px solid var(--gold); + border-radius: 12px; + padding: 1rem 1.25rem; + width: 100%; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.7); + text-align: center; +} + +.recipe-step-badge { + display: inline-block; + background: rgba(212, 175, 55, 0.2); + border: 1px solid var(--gold); + color: var(--gold); + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: bold; + margin-bottom: 0.5rem; +} + +.cauldron-wrapper { + position: relative; + width: 280px; + height: 280px; + display: flex; + justify-content: center; + align-items: center; + margin: 0.5rem 0; + touch-action: none; +} + +.cauldron-pot { + width: 250px; + height: 230px; + background: radial-gradient(circle at 35% 30%, #4a4a55 0%, #202025 50%, #0d0d12 100%); + border-radius: 50% 50% 45% 45% / 30% 30% 60% 60%; + border: 6px solid #1a1a20; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.9), inset 0 -10px 20px rgba(0, 0, 0, 0.8); + position: relative; + display: flex; + justify-content: center; + align-items: flex-start; + padding-top: 25px; +} + +.cauldron-pot::before { + content: ''; + position: absolute; + top: 5px; + width: 210px; + height: 48px; + border-radius: 50%; + background: #111116; + border: 4px solid #2d2d38; +} + +.cauldron-pot::after { + content: ''; + position: absolute; + bottom: -16px; + width: 120px; + height: 18px; + background: #000; + border-radius: 50%; + filter: blur(8px); +} + +.potion-surface { + position: absolute; + top: 12px; + width: 190px; + height: 36px; + border-radius: 50%; + background: radial-gradient(ellipse at center, #2ecc71 0%, #16a085 60%, #0e5042 100%); + box-shadow: 0 0 20px #2ecc71; + overflow: hidden; + transition: background 1s ease, box-shadow 1s ease; +} + +.potion-surface.state-stir { + background: radial-gradient(ellipse at center, #9b59b6 0%, #8e44ad 60%, #4a235a 100%); + box-shadow: 0 0 25px #9b59b6; +} + +.potion-surface.state-heat { + background: radial-gradient(ellipse at center, #e67e22 0%, #d35400 60%, #78281f 100%); + box-shadow: 0 0 25px #e67e22; +} + +.potion-surface.state-gold { + background: radial-gradient(ellipse at center, #fff6a5 0%, #f1c40f 50%, #b7950b 100%); + box-shadow: 0 0 35px #f1c40f, 0 0 60px rgba(241, 196, 15, 0.6); + animation: goldShimmer 1.5s infinite alternate; +} + +@keyframes goldShimmer { + 0% { filter: brightness(1) drop-shadow(0 0 10px #f1c40f); } + 100% { filter: brightness(1.3) drop-shadow(0 0 30px #fff); } +} + +.cauldron-fire { + position: absolute; + bottom: 5px; + font-size: 2.2rem; + animation: flameFlicker 0.8s infinite alternate; + pointer-events: none; +} + +/* Stir Interactive Ring */ +.stir-circle { + position: absolute; + width: 180px; + height: 180px; + border-radius: 50%; + border: 3px dashed var(--gold); + display: none; + align-items: center; + justify-content: center; + pointer-events: auto; + cursor: grab; + animation: rotateDashed 6s linear infinite; +} + +.stir-circle.active { + display: flex; +} + +.stir-arrow-hint { + font-size: 1.8rem; + position: absolute; + top: 5px; + right: 35px; +} + +@keyframes rotateDashed { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Ingredient Shelf */ +.ingredients-shelf { + display: flex; + gap: 0.75rem; + justify-content: center; + flex-wrap: wrap; + width: 100%; +} + +.ingredient-btn { + background: rgba(30, 24, 40, 0.85); + border: 2px solid #5a422d; + border-radius: 12px; + padding: 0.6rem 0.8rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + color: var(--text-light); + cursor: pointer; + transition: all 0.2s; + user-select: none; + min-width: 90px; +} + +.ingredient-btn:hover, .ingredient-btn:active { + border-color: var(--gold); + transform: translateY(-3px) scale(1.03); + box-shadow: 0 6px 16px rgba(212, 175, 55, 0.3); +} + +.ingredient-btn.done { + opacity: 0.5; + border-color: #27ae60; + pointer-events: none; +} + +.ing-icon { + font-size: 1.8rem; +} + +.ing-name { + font-size: 0.85rem; + font-weight: bold; +} + +.ing-count { + font-size: 0.75rem; + color: var(--gold); + background: rgba(0, 0, 0, 0.4); + padding: 0.1rem 0.5rem; + border-radius: 10px; +} + +/* Heat Bellows Bar */ +.heat-control-panel { + display: none; + flex-direction: column; + align-items: center; + width: 100%; + gap: 0.75rem; +} + +.heat-control-panel.active { + display: flex; +} + +.temp-bar-container { + width: 100%; + max-width: 320px; + height: 24px; + background: #111; + border: 2px solid #555; + border-radius: 12px; + position: relative; + overflow: hidden; +} + +.temp-ideal-zone { + position: absolute; + left: 60%; + width: 25%; + height: 100%; + background: rgba(46, 204, 113, 0.4); + border-left: 2px dashed #2ecc71; + border-right: 2px dashed #2ecc71; +} + +.temp-indicator { + height: 100%; + width: 10%; + background: linear-gradient(90deg, #3498db, #f39c12, #e74c3c); + border-radius: 10px; + transition: width 0.15s ease-out; +} + +.bellows-btn { + background: linear-gradient(180deg, #c0392b, #962d22); + border: 2px solid #e74c3c; + border-radius: 12px; + color: #fff; + padding: 0.75rem 1.5rem; + font-size: 1.1rem; + font-family: inherit; + font-weight: bold; + cursor: pointer; + box-shadow: 0 6px 18px rgba(231, 76, 60, 0.4); + display: inline-flex; + align-items: center; + gap: 0.5rem; + user-select: none; + touch-action: manipulation; +} + +.bellows-btn:active { + transform: scale(0.95); + background: #e74c3c; +} + + +/* ======================================================== + MINI-GAME: DAS RUNEN-DREHSCHLOSS / CRYPTEX (/cryptex) + ======================================================== */ +.cryptex-container { + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; +} + +.cryptex-box { + background: linear-gradient(180deg, #3a2b1d 0%, #1f160e 50%, #120d08 100%); + border: 4px solid var(--gold); + border-radius: 16px; + padding: 1.25rem 1rem; + box-shadow: 0 12px 35px rgba(0, 0, 0, 0.9), inset 0 0 25px rgba(212, 175, 55, 0.15); + width: 100%; + position: relative; +} + +.cryptex-frame-line { + position: absolute; + top: 50%; + left: 4%; + right: 4%; + height: 4px; + background: rgba(212, 175, 55, 0.85); + box-shadow: 0 0 10px var(--gold); + transform: translateY(-50%); + pointer-events: none; + z-index: 10; +} + +.cryptex-rings-row { + display: flex; + justify-content: space-around; + gap: 0.5rem; + position: relative; + z-index: 5; +} + +.cryptex-dial { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.dial-step-btn { + background: rgba(212, 175, 55, 0.15); + border: 1px solid var(--gold); + color: var(--gold); + border-radius: 8px; + width: 44px; + height: 38px; + font-size: 1.2rem; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + touch-action: manipulation; + transition: all 0.15s; +} + +.dial-step-btn:active { + background: var(--gold); + color: #120d08; + transform: scale(0.92); +} + +.dial-cylinder { + width: 100%; + max-width: 90px; + height: 90px; + background: linear-gradient(180deg, #1b1712 0%, #4a3822 50%, #15110c 100%); + border: 3px solid #8c6e3b; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-shadow: inset 0 8px 12px rgba(0,0,0,0.8), inset 0 -8px 12px rgba(0,0,0,0.8); + position: relative; + user-select: none; +} + +.dial-symbol { + font-size: 2.4rem; + filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8)); + transition: transform 0.2s ease; +} + +.dial-label { + font-size: 0.72rem; + color: var(--gold-light); + margin-top: -2px; + font-weight: bold; +} + + +/* ======================================================== + MINI-GAME: WICHTEL-PANIK / CORNISH PIXIES (/pixies) + ======================================================== */ +.pixies-container { + max-width: 580px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.pixies-hud { + display: flex; + justify-content: space-between; + width: 100%; + background: rgba(20, 15, 30, 0.85); + border: 2px solid var(--gold); + border-radius: 12px; + padding: 0.6rem 1rem; + font-size: 1.1rem; + box-shadow: 0 4px 15px rgba(0,0,0,0.6); +} + +.pixies-arena { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + width: 100%; + aspect-ratio: 1 / 1; + background: linear-gradient(135deg, #1f1728, #100b17); + border: 3px solid #4a3461; + border-radius: 16px; + padding: 0.75rem; + box-shadow: inset 0 0 25px rgba(0,0,0,0.8); + touch-action: manipulation; +} + +.pixie-hole { + background: radial-gradient(circle, #2b1f3d 0%, #150f20 80%); + border: 2px solid #3c2a50; + border-radius: 12px; + position: relative; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + box-shadow: inset 0 6px 10px rgba(0,0,0,0.7); +} + +.hole-decor { + font-size: 1.5rem; + opacity: 0.25; + position: absolute; + bottom: 6px; + pointer-events: none; +} + +.pixie-target { + font-size: 3.2rem; + position: absolute; + transform: translateY(120%); + transition: transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); + pointer-events: auto; + filter: drop-shadow(0 4px 8px rgba(0,0,0,0.8)); + animation: pixieFloat 0.8s infinite alternate ease-in-out; +} + +.pixie-target.active { + transform: translateY(0); +} + +.pixie-target.frozen { + filter: drop-shadow(0 0 12px #3498db) hue-rotate(180deg) brightness(1.2); + animation: none; + transform: translateY(0) scale(1.08); +} + +.pixie-target.neville { + font-size: 3.4rem; + animation: shakeNeville 0.4s infinite alternate; +} + +@keyframes pixieFloat { + 0% { transform: translateY(-4px) rotate(-3deg); } + 100% { transform: translateY(4px) rotate(3deg); } +} + +@keyframes shakeNeville { + 0% { transform: translateY(0) rotate(-5deg); } + 100% { transform: translateY(0) rotate(5deg); } +} + +@media (max-width: 480px) { + .cauldron-wrapper { + width: 240px; + height: 240px; + } + .cauldron-pot { + width: 215px; + height: 200px; + } + .dial-cylinder { + height: 75px; + } + .dial-symbol { + font-size: 2rem; + } + .pixie-target { + font-size: 2.6rem; + } +} + diff --git a/app/static/js/brew.js b/app/static/js/brew.js new file mode 100755 index 0000000..1b6951c --- /dev/null +++ b/app/static/js/brew.js @@ -0,0 +1,227 @@ +// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine) +(function() { + let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done + const ingredients = { + dragon: 0, + mandrake: 0, + star: 0 + }; + const targetIngredients = { + dragon: 2, + mandrake: 1, + star: 1 + }; + + // DOM Elements + const stepBadge = document.getElementById('stepBadge'); + const stepInstruction = document.getElementById('stepInstruction'); + const stepSubtext = document.getElementById('stepSubtext'); + const ingredientsShelf = document.getElementById('ingredientsShelf'); + const potionSurface = document.getElementById('potionSurface'); + const stirCircle = document.getElementById('stirCircle'); + const stirTracker = document.getElementById('stirTracker'); + const stirCounterText = document.getElementById('stirCounterText'); + const stirCountSpan = document.getElementById('stirCount'); + const heatPanel = document.getElementById('heatPanel'); + const tempIndicator = document.getElementById('tempIndicator'); + const heatTimerSpan = document.getElementById('heatTimer'); + const successPanel = document.getElementById('successPanel'); + const cauldronZone = document.getElementById('cauldronZone'); + + // Step 1: Add Ingredient + window.addIngredient = function(type) { + if (currentStep !== 1) return; + if (ingredients[type] >= targetIngredients[type]) return; + + ingredients[type]++; + if (window.soundFX) window.soundFX.playBottleClink(); + + // Update UI Badge + if (type === 'dragon') { + document.getElementById('countDragon').innerText = `${ingredients.dragon} / ${targetIngredients.dragon}`; + if (ingredients.dragon >= targetIngredients.dragon) document.getElementById('ingDragon').classList.add('done'); + } else if (type === 'mandrake') { + document.getElementById('countMandrake').innerText = `${ingredients.mandrake} / ${targetIngredients.mandrake}`; + if (ingredients.mandrake >= targetIngredients.mandrake) document.getElementById('ingMandrake').classList.add('done'); + } else if (type === 'star') { + document.getElementById('countStar').innerText = `${ingredients.star} / ${targetIngredients.star}`; + if (ingredients.star >= targetIngredients.star) document.getElementById('ingStar').classList.add('done'); + } + + // Ripple effect on surface + potionSurface.style.transform = 'scale(1.08)'; + setTimeout(() => potionSurface.style.transform = 'scale(1)', 200); + + // Check if all ingredients added + const total = ingredients.dragon + ingredients.mandrake + ingredients.star; + if (total === 4) { + setTimeout(startStep2, 600); + } + }; + + // Step 2: Stirring + let completedStirs = 0; + let lastAngle = null; + let accumulatedAngle = 0; + + function startStep2() { + currentStep = 2; + stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; + stepInstruction.innerText = "Rühre den Sud 3 Runden im Uhrzeigersinn!"; + stepSubtext.innerText = "Führe deinen Finger oder die Maus im Kreis über dem Kessel oder tippe ihn an."; + ingredientsShelf.style.display = 'none'; + stirCounterText.style.display = 'block'; + stirCircle.classList.add('active'); + potionSurface.className = 'potion-surface state-stir'; + + if (window.soundFX) window.soundFX.playStoneSlide(); + } + + // Touch / Pointer stirring logic + let isStirring = false; + + function getAngle(e) { + const rect = cauldronZone.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const clientX = e.touches ? e.touches[0].clientX : e.clientX; + const clientY = e.touches ? e.touches[0].clientY : e.clientY; + return Math.atan2(clientY - centerY, clientX - centerX) * (180 / Math.PI); + } + + function onStirStart(e) { + if (currentStep !== 2) return; + isStirring = true; + lastAngle = getAngle(e); + } + + function onStirMove(e) { + if (currentStep !== 2 || !isStirring) return; + e.preventDefault(); + const angle = getAngle(e); + if (lastAngle !== null) { + let diff = angle - lastAngle; + // Handle wrap-around + if (diff < -180) diff += 360; + if (diff > 180) diff -= 360; + + // Only count clockwise + if (diff > 0) { + accumulatedAngle += diff; + if (accumulatedAngle >= 360) { + accumulatedAngle -= 360; + incrementStir(); + } + } + } + lastAngle = angle; + } + + function onStirEnd() { + isStirring = false; + lastAngle = null; + } + + function incrementStir() { + completedStirs++; + stirCountSpan.innerText = completedStirs; + if (window.soundFX) window.soundFX.playBottleClink(); + + if (completedStirs >= 3) { + stirCircle.classList.remove('active'); + stirCounterText.style.display = 'none'; + setTimeout(startStep3, 600); + } + } + + // Direct tap fallback for mobile + cauldronZone.addEventListener('click', () => { + if (currentStep === 2 && !isStirring) { + incrementStir(); + } + }); + + cauldronZone.addEventListener('mousedown', onStirStart); + window.addEventListener('mousemove', onStirMove); + window.addEventListener('mouseup', onStirEnd); + + cauldronZone.addEventListener('touchstart', onStirStart, { passive: false }); + window.addEventListener('touchmove', onStirMove, { passive: false }); + window.addEventListener('touchend', onStirEnd); + + // Step 3: Heat Control + let currentTemp = 35; // percent (0-100) + let heatTimer = 0.0; + let heatInterval = null; + + function startStep3() { + currentStep = 3; + stepBadge.innerText = "Schritt 3: Hitze balancieren"; + stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Hitze im grünen Bereich zu halten!"; + stepSubtext.innerText = "Halte die Nadel für 4 Sekunden im grünen Bereich (60% - 85%)."; + potionSurface.className = 'potion-surface state-heat'; + heatPanel.classList.add('active'); + + if (window.soundFX) window.soundFX.playWhoosh(); + + heatInterval = setInterval(updateHeatLoop, 50); + } + + window.pumpHeat = function() { + if (currentStep !== 3) return; + currentTemp = Math.min(100, currentTemp + 15); + if (window.soundFX) window.soundFX.playWhoosh(); + }; + + function updateHeatLoop() { + if (currentStep !== 3) return; + + // Slow cooling down + currentTemp = Math.max(0, currentTemp - 0.55); + tempIndicator.style.width = currentTemp + '%'; + + // Check ideal zone (60% to 85%) + if (currentTemp >= 60 && currentTemp <= 85) { + heatTimer += 0.05; + heatTimerSpan.innerText = heatTimer.toFixed(1); + tempIndicator.style.background = '#2ecc71'; + } else { + tempIndicator.style.background = 'linear-gradient(90deg, #3498db, #f39c12, #e74c3c)'; + } + + if (heatTimer >= 4.0) { + clearInterval(heatInterval); + finishPotionBrew(); + } + } + + // Step 4: Victory + async function finishPotionBrew() { + currentStep = 4; + stepBadge.innerText = "✅ Trank vollendet!"; + stepBadge.style.background = "rgba(46, 204, 113, 0.3)"; + stepBadge.style.borderColor = "#2ecc71"; + stepBadge.style.color = "#2ecc71"; + + stepInstruction.innerText = "Felix Felicis strahlt in reinem Gold!"; + stepSubtext.innerText = "Dein Zaubertrank ist perfekt gelungen."; + + heatPanel.classList.remove('active'); + potionSurface.className = 'potion-surface state-gold'; + + if (window.soundFX) window.soundFX.playMagicSuccess(); + + // API Completion Call + try { + const res = await fetch('/api/brew/complete', { method: 'POST' }); + const data = await res.json(); + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } + } catch (e) { + console.error("Brew complete error:", e); + } + + successPanel.style.display = 'block'; + } +})(); diff --git a/app/static/js/cryptex.js b/app/static/js/cryptex.js new file mode 100755 index 0000000..ba8cdd2 --- /dev/null +++ b/app/static/js/cryptex.js @@ -0,0 +1,137 @@ +// Magisches Runen-Drehschloss (Cryptex Engine) +(function() { + const DIALS = [ + // Dial 0: Houses + [ + { icon: '🦁', name: 'Löwe' }, + { icon: '🐍', name: 'Schlange' }, + { icon: '🦅', name: 'Rabe' }, + { icon: '🦡', name: 'Dachs' } + ], + // Dial 1: Astral + [ + { icon: '☀️', name: 'Sonne' }, + { icon: '🌙', name: 'Mond' }, + { icon: '⭐', name: 'Stern' }, + { icon: '⚡', name: 'Blitz' } + ], + // Dial 2: Elements + [ + { icon: '🔥', name: 'Feuer' }, + { icon: '💧', name: 'Wasser' }, + { icon: '🌿', name: 'Kräuter' }, + { icon: '💨', name: 'Wind' } + ], + // Dial 3: Artifacts + [ + { icon: '🗝️', name: 'Schlüssel' }, + { icon: '🪄', name: 'Zauberstab' }, + { icon: '📜', name: 'Rolle' }, + { icon: '⚗️', name: 'Phiole' } + ] + ]; + + // Initial indices (unsolved) + const state = [0, 0, 1, 0]; + let isUnlocked = false; + + // Elements + const feedbackBox = document.getElementById('cryptexFeedback'); + const successPanel = document.getElementById('cryptexSuccess'); + const unlockBtn = document.getElementById('unlockCryptexBtn'); + + window.stepDial = function(dialIdx, dir) { + if (isUnlocked) return; + + const dial = DIALS[dialIdx]; + state[dialIdx] = (state[dialIdx] + dir + dial.length) % dial.length; + + const symEl = document.getElementById(`sym${dialIdx}`); + const lblEl = document.getElementById(`lbl${dialIdx}`); + + // Quick flip animation + symEl.style.transform = dir > 0 ? 'translateY(15px) scale(0.8)' : 'translateY(-15px) scale(0.8)'; + symEl.style.opacity = '0.3'; + + setTimeout(() => { + const currentItem = dial[state[dialIdx]]; + symEl.innerText = currentItem.icon; + lblEl.innerText = currentItem.name; + symEl.style.transform = 'translateY(0) scale(1)'; + symEl.style.opacity = '1'; + }, 100); + + if (window.soundFX) window.soundFX.playStoneSlide(); + }; + + window.checkCryptex = async function() { + if (isUnlocked) return; + + const currentCombination = [ + DIALS[0][state[0]].icon, + DIALS[1][state[1]].icon, + DIALS[2][state[2]].icon, + DIALS[3][state[3]].icon + ]; + + try { + const res = await fetch('/api/cryptex/unlock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ combination: currentCombination }) + }); + const data = await res.json(); + + if (data.success) { + isUnlocked = true; + unlockBtn.style.display = 'none'; + + if (window.soundFX) { + window.soundFX.playKeyChime(); + window.soundFX.playMagicSuccess(); + } + + // Show golden shine on cylinders + document.querySelectorAll('.dial-cylinder').forEach(c => { + c.style.borderColor = 'var(--gold)'; + c.style.boxShadow = '0 0 20px var(--gold)'; + }); + + if (feedbackBox) { + feedbackBox.style.display = 'block'; + feedbackBox.style.background = 'rgba(46, 204, 113, 0.25)'; + feedbackBox.style.border = '1px solid #2ecc71'; + feedbackBox.style.color = '#2ecc71'; + feedbackBox.innerText = data.message; + } + + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } + + setTimeout(() => { + if (successPanel) successPanel.style.display = 'block'; + }, 800); + + } else { + if (window.soundFX) window.soundFX.playWhoosh(); + + const box = document.querySelector('.cryptex-box'); + box.style.transform = 'translateX(-8px)'; + setTimeout(() => box.style.transform = 'translateX(8px)', 80); + setTimeout(() => box.style.transform = 'translateX(-4px)', 160); + setTimeout(() => box.style.transform = 'translateX(0)', 240); + + if (feedbackBox) { + feedbackBox.style.display = 'block'; + feedbackBox.style.background = 'rgba(231, 76, 60, 0.25)'; + feedbackBox.style.border = '1px solid #e74c3c'; + feedbackBox.style.color = '#e74c3c'; + feedbackBox.innerText = data.message; + } + } + } catch (e) { + console.error("Cryptex check error:", e); + } + }; +})(); diff --git a/app/static/js/inventory.js b/app/static/js/inventory.js index 11ab456..81e2111 100755 --- a/app/static/js/inventory.js +++ b/app/static/js/inventory.js @@ -156,6 +156,18 @@ } 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 === 'spark_wand') { + if (window.soundFX) window.soundFX.playWhoosh(); + showFloatingToast("⚡ Magische Funken sprühen aus deiner Zauberstabspitze!"); + } else if (action === 'lucky_aura') { + if (window.soundFX) window.soundFX.playMagicSuccess(); + showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!"); + } else if (action === 'inspect_key') { + if (window.soundFX) window.soundFX.playKeyChime(); + showFloatingToast("🗝️ Die Gravur der vier Gründer leuchtet warm auf: 'Hogwarts, Hogwarts, warzenschweinig alt...'"); + } else if (action === 'poke_jar') { + if (window.soundFX) window.soundFX.playWhoosh(); + showFloatingToast("🧚 *Klopf-Klopf!* Der blaue Wichtel im Glas verzieht frech das Gesicht!"); } 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."); diff --git a/app/static/js/pixies.js b/app/static/js/pixies.js new file mode 100755 index 0000000..2ff786f --- /dev/null +++ b/app/static/js/pixies.js @@ -0,0 +1,188 @@ +// Wichtel-Panik / Cornish Pixies Arcade Engine +(function() { + let score = 0; + const targetScore = 10; + let timeLeft = 35; + let gameRunning = false; + let timerInterval = null; + let spawnTimeout = null; + + // DOM Elements + const scoreEl = document.getElementById('pixieScore'); + const timerEl = document.getElementById('pixieTimer'); + const startBtn = document.getElementById('startPixieBtn'); + const startOverlay = document.getElementById('startOverlay'); + const successPanel = document.getElementById('pixieSuccess'); + const holes = document.querySelectorAll('.pixie-hole'); + + const activeTargets = new Map(); // holeIndex -> { type: 'pixie'|'neville', timer: id } + + window.startPixieHunt = function() { + if (gameRunning) return; + gameRunning = true; + score = 0; + timeLeft = 35; + + scoreEl.innerText = score; + timerEl.innerText = timeLeft; + startOverlay.style.display = 'none'; + if (successPanel) successPanel.style.display = 'none'; + + if (window.soundFX) window.soundFX.playWhoosh(); + + // Start countdown + timerInterval = setInterval(() => { + timeLeft--; + timerEl.innerText = timeLeft; + + if (timeLeft <= 0) { + endGame(false); + } + }, 1000); + + scheduleNextSpawn(); + }; + + function scheduleNextSpawn() { + if (!gameRunning) return; + const delay = Math.random() * 400 + 400; // 400ms - 800ms + spawnTimeout = setTimeout(() => { + spawnTarget(); + scheduleNextSpawn(); + }, delay); + } + + function spawnTarget() { + if (!gameRunning) return; + + // Find available holes + const available = []; + for (let i = 0; i < 9; i++) { + if (!activeTargets.has(i)) available.push(i); + } + if (available.length === 0) return; + + const holeIdx = available[Math.floor(Math.random() * available.length)]; + const targetEl = document.getElementById(`target${holeIdx}`); + if (!targetEl) return; + + // 15% chance Neville, 85% chance Pixie + const isNeville = Math.random() < 0.15; + targetEl.className = 'pixie-target active'; + targetEl.innerText = isNeville ? '👦' : '🧚'; + if (isNeville) targetEl.classList.add('neville'); + + const stayTime = Math.random() * 400 + 850; // 850ms - 1250ms + + const hideTimer = setTimeout(() => { + hideTarget(holeIdx); + }, stayTime); + + activeTargets.set(holeIdx, { + isNeville: isNeville, + timer: hideTimer, + clicked: false + }); + } + + function hideTarget(holeIdx) { + const targetEl = document.getElementById(`target${holeIdx}`); + if (targetEl) { + targetEl.classList.remove('active'); + targetEl.classList.remove('frozen'); + targetEl.classList.remove('neville'); + } + activeTargets.delete(holeIdx); + } + + // Touch / Click Handler on holes + holes.forEach(hole => { + const idx = parseInt(hole.dataset.idx, 10); + + function handleTap(e) { + if (!gameRunning) return; + e.preventDefault(); + + if (!activeTargets.has(idx)) return; + const targetData = activeTargets.get(idx); + if (targetData.clicked) return; + targetData.clicked = true; + + const targetEl = document.getElementById(`target${idx}`); + + if (targetData.isNeville) { + // Clicked Neville -> Penalty! + if (window.soundFX) window.soundFX.playWhoosh(); + score = Math.max(0, score - 2); + scoreEl.innerText = score; + + targetEl.style.transform = 'scale(1.3) rotate(15deg)'; + if (window.showFloatingToast) { + window.showFloatingToast("🤕 Neville: »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)"); + } + + setTimeout(() => hideTarget(idx), 350); + + } else { + // Clicked Pixie -> Immobilus! + clearTimeout(targetData.timer); + targetEl.classList.add('frozen'); + targetEl.innerText = '❄️'; + + if (window.soundFX) { + window.soundFX.playKeyChime(); + } + + score++; + scoreEl.innerText = score; + + if (score >= targetScore) { + endGame(true); + } else { + setTimeout(() => hideTarget(idx), 400); + } + } + } + + hole.addEventListener('click', handleTap); + hole.addEventListener('touchstart', handleTap, { passive: false }); + }); + + async function endGame(won) { + gameRunning = false; + clearInterval(timerInterval); + clearTimeout(spawnTimeout); + + // Hide all active + activeTargets.forEach((val, idx) => hideTarget(idx)); + activeTargets.clear(); + + if (won) { + if (window.soundFX) window.soundFX.playMagicSuccess(); + + try { + const res = await fetch('/api/pixies/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ caught: score }) + }); + const data = await res.json(); + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } + } catch (e) { + console.error("Pixies complete error:", e); + } + + if (successPanel) successPanel.style.display = 'block'; + + } else { + if (window.soundFX) window.soundFX.playWhoosh(); + startOverlay.style.display = 'block'; + startBtn.innerText = "Erneut versuchen 🔄"; + if (window.showFloatingToast) { + window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Die Wichtel haben gewonnen. Versuch es noch einmal!"); + } + } + } +})(); diff --git a/app/templates/base.html b/app/templates/base.html index b9de723..4426ee6 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -126,6 +126,18 @@ 🪄 Spruch-Duell: {% if state.duel_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+ ⚗️ Trank-Brauküche: + {% if state.brew_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+
+ 🔐 Runen-Drehschloss: + {% if state.cryptex_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+
+ 🧚 Wichtel-Panik: + {% if state.pixies_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
🧪 Snapes Flammen: {% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %} @@ -161,15 +173,34 @@ {% if state.duel_solved %}✅ Gemeistert{% else %}⚡ Zaubergesten zeichnen{% endif %}
+ + ⚗️ +
+ Kammer III: Die Brauküche + {% if state.brew_solved %}✅ Gemeistert{% else %}🧪 Felix Felicis brauen{% endif %} +
+
+ + 🔐 +
+ Kammer IV: Das Drehschloss + {% if state.cryptex_solved %}✅ Gemeistert{% else %}🔑 Gründer-Siegel knacken{% endif %} +
+
+ + 🧚 +
+ Kammer V: Wichtel-Panik + {% if state.pixies_solved %}✅ Gemeistert{% else %}❄️ Immobilus auf Wichtel{% endif %} +
+
🧪
- Kammer III: Snapes Flaschen + Kammer VI: Snapes Flaschen {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}
- - 🧩
diff --git a/app/templates/brew.html b/app/templates/brew.html new file mode 100755 index 0000000..73b6ddc --- /dev/null +++ b/app/templates/brew.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block title %}Kammer III: Die Zaubertrank-Brauküche · Hogwarts Escape{% endblock %} + +{% block content %} +
+

⚗️ Kammer III: Die Zaubertrank-Brauküche ⚗️

+

+ "Dichte Dämpfe steigen aus dem massiven Kupferkessel auf.
+ Um das nächste Kerkertor aufzuschmelzen, musst du einen legendären Trank vollenden:
+ Felix Felicis – Das flüssige Gold des Glücks!" +

+
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/cryptex.html b/app/templates/cryptex.html new file mode 100755 index 0000000..89ef2cd --- /dev/null +++ b/app/templates/cryptex.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% block title %}Kammer IV: Das Runen-Drehschloss der Gründer · Hogwarts Escape{% endblock %} + +{% block content %} +
+

🔐 Kammer IV: Das Runen-Drehschloss 🔐

+

+ "Vor dir ragt ein gewaltiges Tresortor mit einem massiven Messing-Zylinderschloss.
+ In die Steinwand über dem Schloss ist ein altes Gründer-Rätsel gemeißelt:

+ »Vier Siegel schloss der Gründer Rat,
+ bevor man je die Kammer betrat.
+ Der Löwe wacht am Himmelstor,
+ wo nachts die Silbersichel glimmt,
+ das ew’ge Feuer niemals stirbt,
+ bis der Zauberstab die Siegel bricht.«
" +

+
+ +
+ + + + +
+
+
+ +
+ +
+
🦁
+
Löwe
+
+ +
+ + +
+ +
+
☀️
+
Sonne
+
+ +
+ + +
+ +
+
💧
+
Wasser
+
+ +
+ + +
+ +
+
🗝️
+
Schlüssel
+
+ +
+
+
+ + +
+ +
+ + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 828405a..95b44d8 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -49,14 +49,55 @@ Duell beginnen 🪄
+ +
+ + {% if state.brew_solved %}✅ Gemeistert{% else %}⚗️ Bereit{% endif %} + +
⚗️🧪
+

Kammer III: Die Brauküche

+

+ Snapes Kupferkessel brodelt! Gib Drachenblut und Sternenstaub hinzu, rühre mit kreisenden Gesten + und balanciere die Hitze, um das goldene Glückselixier Felix Felicis zu vollenden! +

+ Kessel anheizen ⚗️ +
- + +
+ + {% if state.cryptex_solved %}✅ Gemeistert{% else %}🔐 Bereit{% endif %} + +
🔐🗝️
+

Kammer IV: Das Drehschloss

+

+ Das massive Tresortor der vier Hogwarts-Gründer. Drehe die vier Messingzylinder mit Wappen, Mond und Sternen, + um das alchemistische Rätsel zu lösen und den Gründerschlüssel zu befreien! +

+ Ringe drehen 🔐 +
+ + +
+ + {% if state.pixies_solved %}✅ Gemeistert{% else %}🧚 Bereit{% endif %} + +
🧚❄️
+

Kammer V: Wichtel-Panik

+

+ Chaos im Klassenzimmer! Freche blaue Wichtel wirbeln umher. Schnelle Reflexe sind gefragt: + Banne 10 Wichtel mit dem Einfrier-Zauber »Immobilus!« und schone Neville! +

+ Wichtel jagen 🧚 +
+ +
{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
🧪🔥
-

Kammer III: Snapes Flaschen

+

Kammer VI: Snapes Flaschen

Schwarze Flammen versperren den Weg zum Stein, violette Flammen den Rückweg. Auf dem Tisch stehen 7 Flaschen und Snapes unerbittliches Logik-Gedicht. diff --git a/app/templates/pixies.html b/app/templates/pixies.html new file mode 100755 index 0000000..357c41b --- /dev/null +++ b/app/templates/pixies.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} + +{% block title %}Kammer V: Wichtel-Panik · Hogwarts Escape{% endblock %} + +{% block content %} +

+

🧚 Kammer V: Wichtel-Panik 🧚

+

+ "Lockharts Eisenkäfig ist aufgeplatzt! Ein Schwarm frecher kornischer Wichtel wirbelt durch den Raum.
+ Banne 10 Wichtel mit dem Einfrier-Zauber »Immobilus!« ❄️ mit schnellen Fingern!
+ Vorsicht: Tippe nicht auf Neville Longbottom (👦), der an der Decke baumelt!" +

+
+ +
+ +
+
+ 🧚 Gefangen: 0 / 10 +
+
+ ⏳ Zeit: 35s +
+
+ + +
+ +
+ 📚 +
🧚
+
+ +
+ 🗝️ +
🧚
+
+ +
+ 🕯️ +
🧚
+
+ +
+ 🧪 +
🧚
+
+ +
+ 📦 +
🧚
+
+ +
+ 🪟 +
🧚
+
+ +
+ 📜 +
🧚
+
+ +
+ 🕸️ +
🧚
+
+ +
+ 🕰️ +
🧚
+
+
+ + +
+ +
+ + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/test_app.py b/test_app.py index f8199f8..2c436fb 100755 --- a/test_app.py +++ b/test_app.py @@ -24,6 +24,20 @@ def test_endpoints(): assert res.status_code in [301, 302] assert res.headers["location"] == "/duel" + # Test new mini-games: Brew, Cryptex, Pixies + res = client.get("/brew") + assert res.status_code == 200 + assert "Die Zaubertrank-Brauküche" in res.text + assert "Felix Felicis" in res.text + + res = client.get("/cryptex") + assert res.status_code == 200 + assert "Das Runen-Drehschloss" in res.text + + res = client.get("/pixies") + assert res.status_code == 200 + assert "Wichtel-Panik" in res.text + res = client.get("/bottles") assert res.status_code == 200 assert "Snapes Flaschenrätsel" in res.text @@ -55,8 +69,7 @@ def test_endpoints(): # Test API Broom complete res = client.post("/api/broom/complete", json={"score": 850, "time_taken": 22}) assert res.status_code == 200 - data = res.json() - assert data["success"] is True + assert res.json()["success"] is True # Test API Duel complete (adds duel_badge) res = client.post("/api/duel/complete") @@ -64,14 +77,53 @@ def test_endpoints(): duel_data = res.json() assert duel_data["success"] is True assert duel_data["new_item"]["id"] == "duel_badge" - assert "duel_badge" in duel_data["state"]["inventory"] + + # Test API Brew complete (adds felix_felicis) + res = client.post("/api/brew/complete") + assert res.status_code == 200 + brew_data = res.json() + assert brew_data["success"] is True + assert brew_data["new_item"]["id"] == "felix_felicis" + + # Test API Cryptex unlock (wrong then correct) + res = client.post("/api/cryptex/unlock", json={"combination": ["🐍", "☀️", "💧", "🗝️"]}) + assert res.status_code == 200 + assert res.json()["success"] is False + + res = client.post("/api/cryptex/unlock", json={"combination": ["🦁", "🌙", "🔥", "🪄"]}) + assert res.status_code == 200 + cryp_data = res.json() + assert cryp_data["success"] is True + assert cryp_data["new_item"]["id"] == "cryptex_key" + + # Test API Pixies complete (under 10 then >= 10) + res = client.post("/api/pixies/complete", json={"caught": 5}) + assert res.status_code == 200 + assert res.json()["success"] is False + + res = client.post("/api/pixies/complete", json={"caught": 10}) + assert res.status_code == 200 + pix_data = res.json() + assert pix_data["success"] is True + assert pix_data["new_item"]["id"] == "pixie_jar" + + # Verify updated inventory contains all items + res = client.get("/api/inventory") + updated_inv = res.json() + inv_ids = [item["id"] for item in updated_inv["inventory"]] + assert "duel_badge" in inv_ids + assert "felix_felicis" in inv_ids + assert "cryptex_key" in inv_ids + assert "pixie_jar" in inv_ids + assert updated_inv["brew_solved"] is True + assert updated_inv["cryptex_solved"] is True + assert updated_inv["pixies_solved"] is True # 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 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 @@ -79,7 +131,7 @@ def test_endpoints(): assert res.status_code == 200 assert res.json()["success"] is True - print("ALL TESTS (RING FLIGHT & SPELL DUEL) PASSED! ⭕🪄✨") + print("ALL TESTS (ALL MINI-GAMES & APIS) PASSED! ⭕🪄⚗️🔐🧚🧪💎✨") if __name__ == "__main__": test_endpoints()