Fix mobile broom flight with D-pad & touch-action, add Chamber II McGonagalls Wizard Chess
This commit is contained in:
Binary file not shown.
+46
-4
@@ -64,6 +64,14 @@ ALL_ITEMS = {
|
||||
"action": "examine_key",
|
||||
"action_label": "Schloss prüfen 🔍"
|
||||
},
|
||||
"chess_knight": {
|
||||
"id": "chess_knight",
|
||||
"name": "Schwarzer Springer",
|
||||
"icon": "♟️",
|
||||
"desc": "Ein massiver schwarzer Springer aus McGonagalls verzaubertem Riesenschach. Hat das weiße Heer geschlagen!",
|
||||
"action": "chess_piece",
|
||||
"action_label": "Stein berühren ⚡"
|
||||
},
|
||||
"philosophers_stone": {
|
||||
"id": "philosophers_stone",
|
||||
"name": "Stein der Weisen",
|
||||
@@ -90,6 +98,7 @@ def get_session_state(request: Request) -> dict:
|
||||
"broom_solved": False,
|
||||
"broom_score": 0,
|
||||
"broom_time": 0,
|
||||
"chess_solved": False,
|
||||
"bottles_solved": False,
|
||||
"sliding_solved": False,
|
||||
"started_at": datetime.datetime.utcnow().isoformat(),
|
||||
@@ -97,16 +106,19 @@ def get_session_state(request: Request) -> dict:
|
||||
"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("chess_solved"):
|
||||
state["inventory"].append("chess_knight")
|
||||
if state.get("bottles_solved"):
|
||||
state["inventory"].append("philosophers_stone")
|
||||
if "house" not in state:
|
||||
state["house"] = "gryffindor"
|
||||
if "chess_solved" not in state:
|
||||
state["chess_solved"] = False
|
||||
request.session["escape"] = state
|
||||
return request.session["escape"]
|
||||
|
||||
@@ -147,6 +159,18 @@ async def page_broom(request: Request):
|
||||
context=get_template_context(request, state, lang)
|
||||
)
|
||||
|
||||
@app.get("/chess", response_class=HTMLResponse)
|
||||
async def page_chess(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="chess.html",
|
||||
context=get_template_context(request, state, lang)
|
||||
)
|
||||
|
||||
@app.get("/bottles", response_class=HTMLResponse)
|
||||
async def page_bottles(request: Request):
|
||||
state = get_session_state(request)
|
||||
@@ -199,6 +223,7 @@ async def api_get_inventory(request: Request):
|
||||
"house": house_info,
|
||||
"score": state.get("broom_score", 0),
|
||||
"broom_solved": state.get("broom_solved", False),
|
||||
"chess_solved": state.get("chess_solved", False),
|
||||
"bottles_solved": state.get("bottles_solved", False),
|
||||
"sliding_solved": state.get("sliding_solved", False)
|
||||
})
|
||||
@@ -226,6 +251,7 @@ async def api_reset_state(request: Request):
|
||||
"broom_solved": False,
|
||||
"broom_score": 0,
|
||||
"broom_time": 0,
|
||||
"chess_solved": False,
|
||||
"bottles_solved": False,
|
||||
"sliding_solved": False,
|
||||
"started_at": datetime.datetime.utcnow().isoformat(),
|
||||
@@ -244,7 +270,7 @@ 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!
|
||||
# Add Silver Key to Inventory
|
||||
if "inventory" not in state:
|
||||
state["inventory"] = ["wand", "broom", "snape_scroll"]
|
||||
if "silver_key" not in state["inventory"]:
|
||||
@@ -252,11 +278,27 @@ async def api_broom_complete(request: Request):
|
||||
request.session["escape"] = state
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": "Der Schlüssel passt ins Schloss! Die Tür zur Flammenkammer steht offen!",
|
||||
"message": "Der Schlüssel passt ins Schloss! Die Tür zu McGonagalls Zauberschach steht offen!",
|
||||
"new_item": ALL_ITEMS["silver_key"],
|
||||
"state": state
|
||||
})
|
||||
|
||||
@app.post("/api/chess/complete")
|
||||
async def api_chess_complete(request: Request):
|
||||
state = get_session_state(request)
|
||||
state["chess_solved"] = True
|
||||
if "inventory" not in state:
|
||||
state["inventory"] = ["wand", "broom", "snape_scroll"]
|
||||
if "chess_knight" not in state["inventory"]:
|
||||
state["inventory"].append("chess_knight")
|
||||
request.session["escape"] = state
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": "Schachmatt! Die weiße Königin senkt ihr Schwert. Der Weg zur Flammenkammer ist frei!",
|
||||
"new_item": ALL_ITEMS["chess_knight"],
|
||||
"state": state
|
||||
})
|
||||
|
||||
@app.post("/api/bottles/drink")
|
||||
async def api_bottles_drink(request: Request):
|
||||
try:
|
||||
@@ -307,7 +349,7 @@ async def api_bottles_drink(request: Request):
|
||||
"result": "success_back",
|
||||
"content": content,
|
||||
"message": msg_de if lang == "de" else msg_en,
|
||||
"next_url": "/broom"
|
||||
"next_url": "/chess"
|
||||
})
|
||||
else:
|
||||
msg_de = "Dieser Trank schützt vor den violetten Flammen, führt dich aber nicht vorwärts durch das schwarze Feuer!"
|
||||
|
||||
+210
-12
@@ -808,7 +808,10 @@ body {
|
||||
#broomCanvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
touch-action: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
@@ -825,34 +828,128 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
/* Mobile Flight Cockpit & D-Pad */
|
||||
.mobile-flight-cockpit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
padding: 1rem;
|
||||
background: rgba(18, 15, 26, 0.9);
|
||||
border: 2px solid rgba(229, 185, 72, 0.35);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.dpad-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dpad-middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dpad-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(145deg, #2a2238, #161220);
|
||||
border: 2px solid var(--gold);
|
||||
color: var(--gold);
|
||||
border-radius: 8px;
|
||||
font-size: 1.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
transition: transform 0.1s, background 0.1s;
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.dpad-btn:active {
|
||||
transform: scale(0.92);
|
||||
background: var(--gold);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dpad-core {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
color: var(--gold);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mobile-flight-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.dpad-boost-btn {
|
||||
width: 100%;
|
||||
padding: 0.9rem 1.25rem;
|
||||
background: linear-gradient(135deg, #d4af37, #f39c12);
|
||||
color: #1a1100;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 12px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 20px var(--gold-glow);
|
||||
touch-action: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.dpad-boost-btn:active {
|
||||
transform: scale(0.95);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.touch-hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-controls {
|
||||
display: flex;
|
||||
}
|
||||
.flame-wall-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
#broomCanvas {
|
||||
height: 360px;
|
||||
}
|
||||
.brand-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.bag-label {
|
||||
display: none; /* Icon and badge only on small screens for ultra compact header */
|
||||
display: none;
|
||||
}
|
||||
.btn-bag {
|
||||
padding: 0.35rem 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-bag {
|
||||
padding: 0.35rem 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Feedback */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
@@ -992,3 +1089,104 @@ body {
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* McGonagall's Wizard Chess (Zauberschach) */
|
||||
.chess-board-wrapper {
|
||||
max-width: 440px;
|
||||
margin: 1.25rem auto;
|
||||
background: #171322;
|
||||
border: 3px solid var(--gold);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.9), inset 0 0 20px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.chess-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
border: 2px solid #5a4b38;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.chess-sq {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.chess-sq.light {
|
||||
background: #e8d8be;
|
||||
}
|
||||
|
||||
.chess-sq.dark {
|
||||
background: #5c4b37;
|
||||
}
|
||||
|
||||
.chess-sq.selected {
|
||||
background: #d4af37 !important;
|
||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.chess-piece {
|
||||
font-size: 2.2rem;
|
||||
line-height: 1;
|
||||
transition: transform 0.15s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.white-piece {
|
||||
color: #ffffff;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
|
||||
text-shadow: 0 0 2px #333;
|
||||
}
|
||||
|
||||
.black-piece {
|
||||
color: #1a1226;
|
||||
filter: drop-shadow(0 2px 4px rgba(229, 185, 72, 0.4));
|
||||
text-shadow: 0 0 3px #e5b948;
|
||||
}
|
||||
|
||||
.chess-sq:hover .chess-piece {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.move-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: rgba(229, 185, 72, 0.75);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 8px var(--gold);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.move-capture-ring {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
border: 3px solid rgba(231, 76, 60, 0.85);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 10px #e74c3c;
|
||||
pointer-events: none;
|
||||
animation: ringPulse 1s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes ringPulse {
|
||||
0% { transform: scale(0.9); opacity: 0.7; }
|
||||
100% { transform: scale(1.05); opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chess-piece {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+154
-86
@@ -1,4 +1,4 @@
|
||||
// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge)
|
||||
// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge) - Mobile & Touch Optimized
|
||||
(function() {
|
||||
const canvas = document.getElementById('broomCanvas');
|
||||
if (!canvas) return;
|
||||
@@ -13,15 +13,28 @@
|
||||
const modalText = document.getElementById('modalText');
|
||||
const modalBtn = document.getElementById('modalBtn');
|
||||
|
||||
let width, height;
|
||||
let width = 800, height = 480;
|
||||
function resize() {
|
||||
width = canvas.parentElement.clientWidth;
|
||||
height = Math.min(520, window.innerHeight * 0.65);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
width = Math.round(rect.width || canvas.parentElement.clientWidth || 800);
|
||||
height = Math.round(Math.min(520, Math.max(300, width * 0.58)));
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.height = height + 'px';
|
||||
|
||||
if (door) {
|
||||
door.x = width - 55;
|
||||
door.y = height / 2 - 45;
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
// Target Door
|
||||
const door = {
|
||||
x: width - 55,
|
||||
y: height / 2 - 45,
|
||||
w: 42,
|
||||
h: 90
|
||||
};
|
||||
|
||||
// Game State
|
||||
let gameRunning = true;
|
||||
@@ -33,10 +46,10 @@
|
||||
// Player (Broomstick)
|
||||
const player = {
|
||||
x: 80,
|
||||
y: height / 2,
|
||||
y: 200,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
speed: 5.5,
|
||||
speed: 6.0,
|
||||
boost: 1.0,
|
||||
angle: 0,
|
||||
trail: []
|
||||
@@ -44,26 +57,22 @@
|
||||
|
||||
// Keys Swarm
|
||||
const keys = [];
|
||||
const NUM_KEYS = 24;
|
||||
const NUM_KEYS = 22;
|
||||
|
||||
// Master Key (Damaged wing silver key)
|
||||
const masterKey = {
|
||||
x: width - 150,
|
||||
y: height / 2,
|
||||
vx: -2,
|
||||
x: 600,
|
||||
y: 200,
|
||||
vx: -2.5,
|
||||
vy: 1.5,
|
||||
wingPhase: 0,
|
||||
caught: false,
|
||||
size: 16
|
||||
size: 17
|
||||
};
|
||||
|
||||
// Target Door
|
||||
const door = {
|
||||
x: width - 60,
|
||||
y: height / 2 - 45,
|
||||
w: 45,
|
||||
h: 90
|
||||
};
|
||||
// Initialize Canvas Size
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
// Spawn decoy keys
|
||||
for (let i = 0; i < NUM_KEYS; i++) {
|
||||
@@ -79,53 +88,106 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Controls
|
||||
// Keyboard Controls
|
||||
const keysDown = {};
|
||||
window.addEventListener('keydown', e => {
|
||||
keysDown[e.key.toLowerCase()] = true;
|
||||
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.6;
|
||||
if (e.key === ' ' || e.key === 'Shift') {
|
||||
player.boost = 1.7;
|
||||
if (window.soundFX) window.soundFX.playWhoosh();
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', e => {
|
||||
keysDown[e.key.toLowerCase()] = false;
|
||||
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0;
|
||||
});
|
||||
|
||||
// Pointer / Touch control
|
||||
// Touch & Pointer Steering
|
||||
let pointerTarget = null;
|
||||
canvas.addEventListener('pointermove', e => {
|
||||
|
||||
function getCanvasCoords(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointerTarget = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
return {
|
||||
x: (clientX - rect.left) * (canvas.width / rect.width),
|
||||
y: (clientY - rect.top) * (canvas.height / rect.height)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Direct Touch Listeners to prevent browser scrolling
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
if (e.touches.length > 0) {
|
||||
pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
|
||||
player.boost = 1.4;
|
||||
if (window.soundFX) window.soundFX.playWhoosh();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
if (e.touches.length > 0) {
|
||||
pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
e.preventDefault();
|
||||
pointerTarget = null;
|
||||
player.boost = 1.0;
|
||||
}, { passive: false });
|
||||
|
||||
// Pointer events for mouse
|
||||
canvas.addEventListener('pointerdown', e => {
|
||||
player.boost = 1.6;
|
||||
if (window.soundFX) window.soundFX.playWhoosh();
|
||||
if (e.pointerType === 'mouse') {
|
||||
pointerTarget = getCanvasCoords(e.clientX, e.clientY);
|
||||
player.boost = 1.5;
|
||||
if (window.soundFX) window.soundFX.playWhoosh();
|
||||
}
|
||||
});
|
||||
canvas.addEventListener('pointermove', e => {
|
||||
if (e.pointerType === 'mouse' && (e.buttons > 0 || pointerTarget)) {
|
||||
pointerTarget = getCanvasCoords(e.clientX, e.clientY);
|
||||
}
|
||||
});
|
||||
canvas.addEventListener('pointerup', () => {
|
||||
player.boost = 1.0;
|
||||
});
|
||||
canvas.addEventListener('pointerleave', () => {
|
||||
pointerTarget = null;
|
||||
player.boost = 1.0;
|
||||
});
|
||||
|
||||
// Mobile buttons
|
||||
const btnUp = document.getElementById('mBtnUp');
|
||||
const btnDown = document.getElementById('mBtnDown');
|
||||
// Mobile D-Pad Buttons
|
||||
function bindDpad(btnId, keyName) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!btn) return;
|
||||
const start = (e) => { e.preventDefault(); keysDown[keyName] = true; };
|
||||
const end = (e) => { e.preventDefault(); keysDown[keyName] = false; };
|
||||
btn.addEventListener('touchstart', start, { passive: false });
|
||||
btn.addEventListener('touchend', end, { passive: false });
|
||||
btn.addEventListener('pointerdown', start);
|
||||
btn.addEventListener('pointerup', end);
|
||||
btn.addEventListener('pointerleave', end);
|
||||
}
|
||||
|
||||
bindDpad('mBtnUp', 'arrowup');
|
||||
bindDpad('mBtnDown', 'arrowdown');
|
||||
bindDpad('mBtnLeft', 'arrowleft');
|
||||
bindDpad('mBtnRight', 'arrowright');
|
||||
|
||||
const btnBoost = document.getElementById('mBtnBoost');
|
||||
if (btnUp) {
|
||||
btnUp.addEventListener('pointerdown', () => { keysDown['arrowup'] = true; });
|
||||
btnUp.addEventListener('pointerup', () => { keysDown['arrowup'] = false; });
|
||||
}
|
||||
if (btnDown) {
|
||||
btnDown.addEventListener('pointerdown', () => { keysDown['arrowdown'] = true; });
|
||||
btnDown.addEventListener('pointerup', () => { keysDown['arrowdown'] = false; });
|
||||
}
|
||||
if (btnBoost) {
|
||||
btnBoost.addEventListener('pointerdown', () => { player.boost = 1.8; if (window.soundFX) window.soundFX.playWhoosh(); });
|
||||
btnBoost.addEventListener('pointerup', () => { player.boost = 1.0; });
|
||||
const startBoost = (e) => {
|
||||
e.preventDefault();
|
||||
player.boost = 1.8;
|
||||
if (window.soundFX) window.soundFX.playWhoosh();
|
||||
};
|
||||
const endBoost = (e) => {
|
||||
e.preventDefault();
|
||||
player.boost = 1.0;
|
||||
};
|
||||
btnBoost.addEventListener('touchstart', startBoost, { passive: false });
|
||||
btnBoost.addEventListener('touchend', endBoost, { passive: false });
|
||||
btnBoost.addEventListener('pointerdown', startBoost);
|
||||
btnBoost.addEventListener('pointerup', endBoost);
|
||||
btnBoost.addEventListener('pointerleave', endBoost);
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -134,74 +196,73 @@
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
if (timerVal) timerVal.innerText = elapsedSec + 's';
|
||||
|
||||
// Keyboard Movement
|
||||
// Keyboard & D-Pad Movement
|
||||
let ax = 0, ay = 0;
|
||||
if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
|
||||
if (keysDown['arrowdown'] || keysDown['s']) ay += 1;
|
||||
if (keysDown['arrowleft'] || keysDown['a']) ax -= 1;
|
||||
if (keysDown['arrowright'] || keysDown['d']) ax += 1;
|
||||
|
||||
// Pointer steer
|
||||
// Pointer steer (Touch / Mouse direct following)
|
||||
if (pointerTarget) {
|
||||
const dx = pointerTarget.x - player.x;
|
||||
const dy = pointerTarget.y - player.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist > 15) {
|
||||
if (dist > 12) {
|
||||
ax = dx / dist;
|
||||
ay = dy / dist;
|
||||
}
|
||||
}
|
||||
|
||||
const curSpeed = player.speed * player.boost;
|
||||
player.vx = player.vx * 0.85 + ax * curSpeed * 0.2;
|
||||
player.vy = player.vy * 0.85 + ay * curSpeed * 0.2;
|
||||
player.vx = player.vx * 0.82 + ax * curSpeed * 0.22;
|
||||
player.vy = player.vy * 0.82 + ay * curSpeed * 0.22;
|
||||
|
||||
player.x += player.vx;
|
||||
player.y += player.vy;
|
||||
|
||||
// Bounds
|
||||
player.x = Math.max(30, Math.min(width - 70, player.x));
|
||||
player.x = Math.max(30, Math.min(width - 65, player.x));
|
||||
player.y = Math.max(30, Math.min(height - 30, player.y));
|
||||
|
||||
player.angle = Math.atan2(player.vy, player.vx + 2) * 0.5;
|
||||
player.angle = Math.atan2(player.vy, player.vx + 2) * 0.55;
|
||||
|
||||
// Particles trail
|
||||
player.trail.push({ x: player.x - 20, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
|
||||
if (player.trail.length > 20) player.trail.shift();
|
||||
player.trail.forEach(p => { p.alpha *= 0.9; p.x -= 2; });
|
||||
player.trail.push({ x: player.x - 22, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
|
||||
if (player.trail.length > 22) player.trail.shift();
|
||||
player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; });
|
||||
|
||||
// Update decoy keys
|
||||
keys.forEach(k => {
|
||||
k.wingPhase += k.wingSpeed;
|
||||
k.x += k.vx;
|
||||
k.y += k.vy;
|
||||
if (k.x < 50 || k.x > width - 100) k.vx *= -1;
|
||||
if (k.x < 40 || k.x > width - 90) k.vx *= -1;
|
||||
if (k.y < 30 || k.y > height - 30) k.vy *= -1;
|
||||
});
|
||||
|
||||
// Update Master Key
|
||||
if (!masterKey.caught) {
|
||||
masterKey.wingPhase += 0.35;
|
||||
// Evasive maneuver if player is close
|
||||
const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y);
|
||||
if (dToPlayer < 140) {
|
||||
masterKey.vx += (masterKey.x - player.x) * 0.04;
|
||||
masterKey.vy += (masterKey.y - player.y) * 0.04;
|
||||
if (dToPlayer < 150) {
|
||||
masterKey.vx += (masterKey.x - player.x) * 0.045;
|
||||
masterKey.vy += (masterKey.y - player.y) * 0.045;
|
||||
} else {
|
||||
masterKey.vx += (Math.random() - 0.5) * 0.8;
|
||||
masterKey.vy += (Math.random() - 0.5) * 0.8;
|
||||
masterKey.vx += (Math.random() - 0.5) * 0.9;
|
||||
masterKey.vy += (Math.random() - 0.5) * 0.9;
|
||||
}
|
||||
masterKey.vx = Math.max(-5, Math.min(5, masterKey.vx * 0.95));
|
||||
masterKey.vy = Math.max(-4, Math.min(4, masterKey.vy * 0.95));
|
||||
masterKey.vx = Math.max(-5.5, Math.min(5.5, masterKey.vx * 0.95));
|
||||
masterKey.vy = Math.max(-4.5, Math.min(4.5, masterKey.vy * 0.95));
|
||||
|
||||
masterKey.x += masterKey.vx;
|
||||
masterKey.y += masterKey.vy;
|
||||
|
||||
masterKey.x = Math.max(120, Math.min(width - 120, masterKey.x));
|
||||
masterKey.x = Math.max(90, Math.min(width - 100, masterKey.x));
|
||||
masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y));
|
||||
|
||||
// Catch check
|
||||
if (dToPlayer < 35) {
|
||||
// Catch check (generous hit box for mobile thumbs: 42px)
|
||||
if (dToPlayer < 42) {
|
||||
masterKey.caught = true;
|
||||
keyCaught = true;
|
||||
score += 500;
|
||||
@@ -211,11 +272,10 @@
|
||||
window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" });
|
||||
}
|
||||
if (statusMsg) {
|
||||
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen & im Beutel!</strong> Fliege zur Holztür rechts und öffne das Schloss!";
|
||||
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen & im Beutel!</strong> Fliege jetzt zur schweren Holztür rechts!";
|
||||
statusMsg.style.color = "#2ecc71";
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Master key follows player
|
||||
masterKey.x = player.x + 10;
|
||||
@@ -223,7 +283,7 @@
|
||||
|
||||
// Check if player reached the door
|
||||
const dToDoor = Math.hypot(player.x - (door.x + door.w / 2), player.y - (door.y + door.h / 2));
|
||||
if (dToDoor < 55 && !doorUnlocked) {
|
||||
if (dToDoor < 60 && !doorUnlocked) {
|
||||
doorUnlocked = true;
|
||||
gameRunning = false;
|
||||
onGameWin(elapsedSec);
|
||||
@@ -240,20 +300,20 @@
|
||||
|
||||
// Stone pillars
|
||||
ctx.fillStyle = 'rgba(25, 22, 36, 0.7)';
|
||||
for (let x = 120; x < width - 100; x += 180) {
|
||||
ctx.fillRect(x, 0, 35, height);
|
||||
for (let x = 110; x < width - 90; x += 160) {
|
||||
ctx.fillRect(x, 0, 30, height);
|
||||
ctx.strokeStyle = '#352e47';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, 0, 35, height);
|
||||
ctx.strokeRect(x, 0, 30, height);
|
||||
}
|
||||
|
||||
// Vaulted arches at top
|
||||
ctx.strokeStyle = '#2d273d';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
for (let x = 0; x < width; x += 90) {
|
||||
for (let x = 0; x < width; x += 80) {
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.quadraticCurveTo(x + 45, 60, x + 90, 0);
|
||||
ctx.quadraticCurveTo(x + 40, 50, x + 80, 0);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
@@ -285,11 +345,19 @@
|
||||
});
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Touch target indicator
|
||||
if (pointerTarget && gameRunning) {
|
||||
ctx.strokeStyle = 'rgba(229, 185, 72, 0.5)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(pointerTarget.x, pointerTarget.y, 20 + Math.sin(Date.now() * 0.01) * 4, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Decoy keys
|
||||
keys.forEach(k => {
|
||||
ctx.save();
|
||||
ctx.translate(k.x, k.y);
|
||||
// wings
|
||||
const wingY = Math.sin(k.wingPhase) * (k.size * 0.7);
|
||||
ctx.fillStyle = k.gold ? 'rgba(255, 215, 0, 0.7)' : 'rgba(200, 220, 255, 0.65)';
|
||||
ctx.beginPath();
|
||||
@@ -312,7 +380,7 @@
|
||||
ctx.translate(masterKey.x, masterKey.y);
|
||||
const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8);
|
||||
ctx.shadowColor = '#00ffff';
|
||||
ctx.shadowBlur = masterKey.caught ? 5 : 12;
|
||||
ctx.shadowBlur = masterKey.caught ? 6 : 14;
|
||||
|
||||
// Right intact wing
|
||||
ctx.fillStyle = 'rgba(180, 225, 255, 0.85)';
|
||||
@@ -320,12 +388,12 @@
|
||||
ctx.ellipse(masterKey.size * 0.7, mWingY, masterKey.size * 0.9, masterKey.size * 0.4, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Left BENT wing (as in the book!)
|
||||
// Left BENT wing
|
||||
ctx.fillStyle = 'rgba(140, 190, 230, 0.75)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(-masterKey.size * 0.5, mWingY * 0.6);
|
||||
ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3); // bent angle!
|
||||
ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3);
|
||||
ctx.lineTo(-masterKey.size * 0.4, mWingY * 0.2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
@@ -366,7 +434,7 @@
|
||||
// Wizard silhouette (Cloak & Hat)
|
||||
ctx.fillStyle = '#1c172a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, -12, 7, 0, Math.PI * 2); // head
|
||||
ctx.arc(5, -12, 7, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Hat
|
||||
ctx.fillStyle = '#120e1e';
|
||||
@@ -377,7 +445,7 @@
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
// Cloak fluttering
|
||||
ctx.fillStyle = '#7a1f28'; // Gryffindor red cloak
|
||||
ctx.fillStyle = '#7a1f28';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -6);
|
||||
ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3);
|
||||
@@ -421,9 +489,9 @@
|
||||
modalText.innerHTML = `Mit waghalsigem Flug hast du den großen Silberschlüssel mit dem geknickten Flügel erwischt und das schwere Schloss entriegelt!<br><br>
|
||||
<strong>Flugzeit:</strong> ${timeTaken} Sekunden<br>
|
||||
<strong>Punkte:</strong> ${score}<br><br>
|
||||
Die Tür schwingt knarrend auf... Vor dir lodern violette und schwarze Flammen in Snapes Prüfung!`;
|
||||
modalBtn.innerText = "Weiter zur Flammenkammer 🧪";
|
||||
modalBtn.onclick = () => { window.location.href = '/bottles'; };
|
||||
Die Tür schwingt knarrend auf... Vor dir liegt McGonagalls gigantisches Zauberschach!`;
|
||||
modalBtn.innerText = "Weiter zu McGonagalls Zauberschach ♟️";
|
||||
modalBtn.onclick = () => { window.location.href = '/chess'; };
|
||||
modalBackdrop.classList.add('show');
|
||||
}
|
||||
})();
|
||||
|
||||
Executable
+247
@@ -0,0 +1,247 @@
|
||||
// McGonagall's Wizard Chess (Zauberschach) Mini-Game
|
||||
(function() {
|
||||
const boardEl = document.getElementById('chessBoard');
|
||||
if (!boardEl) return;
|
||||
|
||||
const statusMsg = document.getElementById('chessStatus');
|
||||
const moveHistoryEl = document.getElementById('moveHistory');
|
||||
const modalBackdrop = document.getElementById('modalBackdrop');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalText = document.getElementById('modalText');
|
||||
const modalBtn = document.getElementById('modalBtn');
|
||||
const btnResetChess = document.getElementById('btnResetChess');
|
||||
|
||||
// Piece Unicode Symbols
|
||||
const PIECES = {
|
||||
'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
|
||||
'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟'
|
||||
};
|
||||
|
||||
// Initial Board State for "Rons Meisterzug" (Smothered Mate / Zauberschach-Taktik)
|
||||
// Black to move: Knight on g4 and Queen on h4 attack White's castled King on h1!
|
||||
let initialPosition = {
|
||||
'h1': 'wK', 'g2': 'wP', 'h2': 'wP', 'f1': 'wR', 'd2': 'wQ', 'b2': 'wP', 'a1': 'wR',
|
||||
'g8': 'bK', 'g7': 'bP', 'h7': 'bP', 'f8': 'bR', 'h4': 'bQ', 'g4': 'bN', 'c6': 'bP'
|
||||
};
|
||||
|
||||
let boardState = {};
|
||||
let selectedSquare = null;
|
||||
let isSolved = false;
|
||||
|
||||
function initBoard() {
|
||||
boardState = Object.assign({}, initialPosition);
|
||||
selectedSquare = null;
|
||||
isSolved = false;
|
||||
if (statusMsg) {
|
||||
statusMsg.innerHTML = "🧙♂️ <strong>Du führst Schwarz (Rons Springer & Hermines Figuren)!</strong> Finde den vernichtenden Zug gegen den weißen König!";
|
||||
statusMsg.style.color = "var(--gold)";
|
||||
}
|
||||
if (moveHistoryEl) moveHistoryEl.innerHTML = "<li>Partie eröffnet: Schwarz am Zug!</li>";
|
||||
renderBoard();
|
||||
}
|
||||
|
||||
// Valid moves for Black pieces in this tactical puzzle
|
||||
function getValidMoves(square) {
|
||||
const piece = boardState[square];
|
||||
if (!piece || !piece.startsWith('b')) return [];
|
||||
|
||||
const file = square.charCodeAt(0) - 97; // 0 to 7 (a-h)
|
||||
const rank = parseInt(square[1], 10) - 1; // 0 to 7 (1-8)
|
||||
const moves = [];
|
||||
|
||||
if (piece === 'bN') {
|
||||
// Knight moves: L-shape
|
||||
const offsets = [
|
||||
[-2, -1], [-2, 1], [-1, -2], [-1, 2],
|
||||
[1, -2], [1, 2], [2, -1], [2, 1]
|
||||
];
|
||||
offsets.forEach(([df, dr]) => {
|
||||
const nf = file + df;
|
||||
const nr = rank + dr;
|
||||
if (nf >= 0 && nf < 8 && nr >= 0 && nr < 8) {
|
||||
const targetSq = String.fromCharCode(97 + nf) + (nr + 1);
|
||||
const targetPiece = boardState[targetSq];
|
||||
if (!targetPiece || targetPiece.startsWith('w')) {
|
||||
moves.push(targetSq);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (piece === 'bQ') {
|
||||
// Queen moves: orthogonal + diagonal
|
||||
const directions = [
|
||||
[0, 1], [0, -1], [1, 0], [-1, 0],
|
||||
[1, 1], [1, -1], [-1, 1], [-1, -1]
|
||||
];
|
||||
directions.forEach(([df, dr]) => {
|
||||
let step = 1;
|
||||
while (true) {
|
||||
const nf = file + df * step;
|
||||
const nr = rank + dr * step;
|
||||
if (nf < 0 || nf >= 8 || nr < 0 || nr >= 8) break;
|
||||
const targetSq = String.fromCharCode(97 + nf) + (nr + 1);
|
||||
const targetPiece = boardState[targetSq];
|
||||
if (!targetPiece) {
|
||||
moves.push(targetSq);
|
||||
} else {
|
||||
if (targetPiece.startsWith('w')) moves.push(targetSq);
|
||||
break;
|
||||
}
|
||||
step++;
|
||||
}
|
||||
});
|
||||
}
|
||||
return moves;
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
boardEl.innerHTML = '';
|
||||
// 8 ranks (from 8 down to 1), 8 files (a to h)
|
||||
for (let r = 8; r >= 1; r--) {
|
||||
for (let f = 0; f < 8; f++) {
|
||||
const sq = String.fromCharCode(97 + f) + r;
|
||||
const isLight = (f + r) % 2 !== 0;
|
||||
|
||||
const tile = document.createElement('div');
|
||||
tile.className = `chess-sq ${isLight ? 'light' : 'dark'}`;
|
||||
tile.dataset.sq = sq;
|
||||
|
||||
if (selectedSquare === sq) {
|
||||
tile.classList.add('selected');
|
||||
}
|
||||
|
||||
const piece = boardState[sq];
|
||||
if (piece) {
|
||||
const pSpan = document.createElement('span');
|
||||
pSpan.className = `chess-piece ${piece.startsWith('w') ? 'white-piece' : 'black-piece'}`;
|
||||
pSpan.innerText = PIECES[piece] || '';
|
||||
tile.appendChild(pSpan);
|
||||
}
|
||||
|
||||
// Show valid moves
|
||||
if (selectedSquare) {
|
||||
const validMoves = getValidMoves(selectedSquare);
|
||||
if (validMoves.includes(sq)) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = piece ? 'move-capture-ring' : 'move-dot';
|
||||
tile.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
tile.addEventListener('click', () => onSquareClick(sq));
|
||||
boardEl.appendChild(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSquareClick(sq) {
|
||||
if (isSolved) return;
|
||||
|
||||
const piece = boardState[sq];
|
||||
|
||||
if (selectedSquare) {
|
||||
const validMoves = getValidMoves(selectedSquare);
|
||||
if (validMoves.includes(sq)) {
|
||||
// Execute Move!
|
||||
executeMove(selectedSquare, sq);
|
||||
selectedSquare = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Select new piece if black
|
||||
if (piece && piece.startsWith('b')) {
|
||||
selectedSquare = sq;
|
||||
if (window.soundFX) window.soundFX.playStoneSlide();
|
||||
renderBoard();
|
||||
} else {
|
||||
selectedSquare = null;
|
||||
renderBoard();
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMove(fromSq, toSq) {
|
||||
const piece = boardState[fromSq];
|
||||
const capturedPiece = boardState[toSq];
|
||||
|
||||
delete boardState[fromSq];
|
||||
boardState[toSq] = piece;
|
||||
|
||||
if (window.soundFX) {
|
||||
window.soundFX.playStoneSlide();
|
||||
if (capturedPiece) {
|
||||
window.soundFX.playPoisonSizzle(); // Crushing sound
|
||||
}
|
||||
}
|
||||
|
||||
renderBoard();
|
||||
|
||||
// Check if winning move:
|
||||
// Winning move 1: Knight moves to f2+ (Smothered mate / Royal fork!)
|
||||
// Winning move 2: Queen captures h2# (Schachmatt!)
|
||||
if (piece === 'bN' && toSq === 'f2') {
|
||||
if (moveHistoryEl) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<strong>1... ♞f2+</strong> (Springer-Schach! Weißer König erstarrt!)`;
|
||||
moveHistoryEl.appendChild(li);
|
||||
}
|
||||
triggerWin("♞f2+", "Rons Springer zerschellt die weiße Verteidigung! Der weiße König hat kein Feld mehr und die weiße Königin muss kapitulieren!");
|
||||
} else if (piece === 'bQ' && toSq === 'h2') {
|
||||
if (moveHistoryEl) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<strong>1... ♛xh2#</strong> (Schachmatt! Die schwarze Dame schlägt zu!)`;
|
||||
moveHistoryEl.appendChild(li);
|
||||
}
|
||||
triggerWin("♛xh2#", "Schachmatt! Mit kaltblütiger Präzision fällt der weiße König in Trümmer!");
|
||||
} else {
|
||||
if (moveHistoryEl) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `Zug: ${piece} nach ${toSq}. Weiß pariert den Angriff. Versuche es erneut!`;
|
||||
moveHistoryEl.appendChild(li);
|
||||
}
|
||||
if (statusMsg) {
|
||||
statusMsg.innerHTML = "⚠️ <em>Kein Schachmatt! Die weißen Figuren schlagen zurück. Drücke auf 'Zurücksetzen' und versuche es erneut!</em>";
|
||||
statusMsg.style.color = "#e67e22";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerWin(moveNotation, explanation) {
|
||||
isSolved = true;
|
||||
if (window.soundFX) window.soundFX.playMagicSuccess();
|
||||
|
||||
if (statusMsg) {
|
||||
statusMsg.innerHTML = `🏆 <strong>${moveNotation} — SCHACHMATT!</strong> Die gigantischen weißen Steinfiguren senken ihre Schwerter!`;
|
||||
statusMsg.style.color = "#2ecc71";
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/chess/complete', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.new_item && window.notifyNewItem) {
|
||||
window.notifyNewItem(data.new_item);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
modalTitle.innerText = "♟️ Zauberschach Gemeistert!";
|
||||
modalText.innerHTML = `
|
||||
<strong>${moveNotation} — Grandios kombiniert!</strong><br><br>
|
||||
${explanation}<br><br>
|
||||
Die weiße Königin wirft ihr Schwert zu Boden. Die Riesen-Schachfiguren weichen zur Seite und die Tür zur <strong>Flammenkammer (Snapes Flaschenrätsel)</strong> steht weit offen!<br><br>
|
||||
🎒 <em>Ein massiver schwarzer Springer wurde deinem Zauberbeutel hinzugefügt!</em>
|
||||
`;
|
||||
modalBtn.innerText = "Weiter zu Snapes Flammenkammer 🧪";
|
||||
modalBtn.onclick = () => { window.location.href = '/bottles'; };
|
||||
modalBackdrop.classList.add('show');
|
||||
}
|
||||
|
||||
if (btnResetChess) {
|
||||
btnResetChess.addEventListener('click', () => {
|
||||
if (window.soundFX) window.soundFX.playStoneSlide();
|
||||
initBoard();
|
||||
});
|
||||
}
|
||||
|
||||
initBoard();
|
||||
})();
|
||||
+13
-1
@@ -122,6 +122,10 @@
|
||||
<span>🧹 Besenflug:</span>
|
||||
<span>{% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>♟️ Zauberschach:</span>
|
||||
<span>{% if state.chess_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>
|
||||
@@ -150,13 +154,21 @@
|
||||
<small>{% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %}</small>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/chess" class="room-nav-item {% if request.url.path == '/chess' %}current{% endif %}">
|
||||
<span class="room-icon">♟️</span>
|
||||
<div class="room-info">
|
||||
<strong>Kammer II: McGonagalls Schach</strong>
|
||||
<small>{% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Riesen-Zauberschach{% 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>
|
||||
<strong>Kammer III: 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">
|
||||
|
||||
+25
-10
@@ -16,33 +16,48 @@
|
||||
<div class="game-hud">
|
||||
<div>⏱️ Zeit: <strong id="timerVal">0s</strong></div>
|
||||
<div>✨ Punkte: <strong id="scoreVal">0</strong></div>
|
||||
<div>🎯 Mission: <span id="missionText">Fange den Schlüssel mit geknicktem Flügel!</span></div>
|
||||
<div>🎯 Ziel: <span id="missionText">Schlüssel fangen!</span></div>
|
||||
</div>
|
||||
|
||||
<canvas id="broomCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Touch Controls -->
|
||||
<div class="mobile-controls">
|
||||
<button id="mBtnUp" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▲ Hoch</button>
|
||||
<button id="mBtnBoost" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem; background: var(--gold); color: #000;">⚡ Turbo</button>
|
||||
<button id="mBtnDown" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▼ Runter</button>
|
||||
<!-- Mobile Flight Cockpit & D-Pad -->
|
||||
<div class="mobile-flight-cockpit">
|
||||
<div class="dpad-box">
|
||||
<button type="button" id="mBtnUp" class="dpad-btn dpad-up" aria-label="Hoch">▲</button>
|
||||
<div class="dpad-middle">
|
||||
<button type="button" id="mBtnLeft" class="dpad-btn dpad-left" aria-label="Links">◀</button>
|
||||
<div class="dpad-core">✨</div>
|
||||
<button type="button" id="mBtnRight" class="dpad-btn dpad-right" aria-label="Rechts">▶</button>
|
||||
</div>
|
||||
<button type="button" id="mBtnDown" class="dpad-btn dpad-down" aria-label="Runter">▼</button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-flight-actions">
|
||||
<button type="button" id="mBtnBoost" class="dpad-boost-btn">
|
||||
⚡ TURBO
|
||||
</button>
|
||||
<div class="touch-hint">
|
||||
💡 <em>Tipp: Du kannst auch direkt mit dem Finger über das Spielfeld wischen!</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dungeon-card" style="margin-top: 1.5rem;">
|
||||
<p id="statusMsg" style="text-align: center; font-size: 1.15rem; color: var(--gold); margin-bottom: 0.8rem;">
|
||||
🎮 <strong>Steuerung:</strong> Pfeiltasten / WASD / Mausbewegung zum Fliegen · <strong>Leertaste / Klick</strong> für Besen-Turbo!
|
||||
🎮 <strong>Steuerung:</strong> Pfeiltasten / WASD / Touch-Wischen zum Fliegen · <strong>Leertaste / Turbo</strong> für Besen-Beschleunigung!
|
||||
</p>
|
||||
<p style="text-align: center; color: var(--text-dim); font-size: 0.95rem;">
|
||||
Fliege durch den Raum, jage den ausweichenden Silberschlüssel mit dem verletzten Flügel und fliege nach dem Fang zur Tür ganz rechts!
|
||||
Fange den großen Silberschlüssel mit dem geknickten Flügel und fliege zur schweren Holztür ganz rechts!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 1rem;">
|
||||
<a href="/bottles" class="nav-link" style="color: var(--text-dim);">Direkt weiter zu Snapes Flaschenkammer 🧪 ➔</a>
|
||||
<a href="/chess" class="nav-link" style="color: var(--text-dim);">Direkt weiter zu McGonagalls Zauberschach ♟️ ➔</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/broom.js?v=1.0"></script>
|
||||
<script src="/static/js/broom.js?v=2.0"></script>
|
||||
{% endblock %}
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Kammer II: McGonagalls Zauberschach · Hogwarts Escape{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="parchment-scroll">
|
||||
<h1 class="parchment-title">♟️ Kammer II: McGonagalls Zauberschach ⚔️</h1>
|
||||
<p class="riddle-poem" style="font-size: 1.25rem; margin: 0.5rem 0;">
|
||||
"Wir müssen uns den Weg quer durch das Zimmer freispielen!", rief Ron.<br>
|
||||
Hinter den weißen Riesenfiguren liegt die schwere Eichentür zur Flammenkammer.<br>
|
||||
Übernimm die Kontrolle über die schwarzen Steine und setze den weißen König schachmatt!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dungeon-card" style="text-align: center;">
|
||||
<div id="chessStatus" style="font-size: 1.15rem; color: var(--gold); margin-bottom: 1rem; min-height: 28px;">
|
||||
🧙♂️ Wähle deinen schwarzen Springer oder deine Dame und führe den Siegeszug aus!
|
||||
</div>
|
||||
|
||||
<!-- 8x8 Chess Board Container -->
|
||||
<div class="chess-board-wrapper">
|
||||
<div id="chessBoard" class="chess-board">
|
||||
<!-- Rendered dynamically by chess.js -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.25rem; display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<button type="button" id="btnResetChess" class="btn-control" style="font-size: 0.95rem; padding: 0.6rem 1.2rem;">
|
||||
🔄 Stellung zurücksetzen
|
||||
</button>
|
||||
<a href="/bottles" class="btn-magical">
|
||||
🧪 Zu Snapes Flaschenkammer ➔
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Move History Log -->
|
||||
<div style="margin-top: 1.25rem; text-align: left; background: rgba(15, 12, 22, 0.7); border: 1px solid rgba(229, 185, 72, 0.25); border-radius: 8px; padding: 0.8rem 1rem;">
|
||||
<strong style="color: var(--gold); font-size: 0.9rem;">📜 Zug-Protokoll:</strong>
|
||||
<ul id="moveHistory" style="list-style: none; margin-top: 0.4rem; font-size: 0.85rem; color: var(--text-dim); line-height: 1.6;">
|
||||
<li>Partie eröffnet: Schwarz am Zug!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tactical Hint Accordion -->
|
||||
<div class="dungeon-card">
|
||||
<details>
|
||||
<summary style="cursor: pointer; font-size: 1.15rem; color: var(--gold); font-weight: bold;">
|
||||
💡 Rons taktischer Schach-Tipp (Hier klicken)
|
||||
</summary>
|
||||
<div style="margin-top: 0.75rem; line-height: 1.7; color: var(--text-light); font-size: 0.95rem;">
|
||||
<p>
|
||||
Der weiße König steht in der Ecke auf <strong>h1</strong>, eingeklemmt von seinen eigenen Bauern auf g2 und h2.
|
||||
</p>
|
||||
<p style="margin-top: 0.5rem;">
|
||||
🎯 <strong>Möglichkeit 1:</strong> Führe mit dem schwarzen Springer auf <strong>g4</strong> ein verheerendes Schachgebot auf <strong>f2</strong> aus (♞f2+) – Rons klassisches Springer-Opfer!
|
||||
</p>
|
||||
<p style="margin-top: 0.5rem;">
|
||||
🎯 <strong>Möglichkeit 2:</strong> Schlage mit der schwarzen Dame auf <strong>h4</strong> direkt den Bauern auf <strong>h2</strong> (♛xh2#) – augenblickliches Schachmatt mit Springer-Deckung!
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/chess.js?v=1.0"></script>
|
||||
{% endblock %}
|
||||
@@ -35,13 +35,27 @@
|
||||
<a href="/broom" class="btn-magical">Auf den Besen schwingen 🧹</a>
|
||||
</div>
|
||||
|
||||
<!-- Chamber 2: Snape's Potions -->
|
||||
<!-- Chamber 2: McGonagall's Wizard Chess (NEW!) -->
|
||||
<div class="chamber-card">
|
||||
<span class="chamber-badge {% if state.chess_solved %}solved{% endif %}">
|
||||
{% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Bereit{% endif %}
|
||||
</span>
|
||||
<div class="chamber-icon">♟️⚔️</div>
|
||||
<h3 class="chamber-title">Kammer II: McGonagalls Schach</h3>
|
||||
<p class="chamber-desc">
|
||||
Das gigantische, lebendige Zauberschach. Übernimm Rons Rolle als schwarzer Springer,
|
||||
überliste die weiße Königin und setze den feindlichen König schachmatt!
|
||||
</p>
|
||||
<a href="/chess" class="btn-magical">Schachbrett betreten ♟️</a>
|
||||
</div>
|
||||
|
||||
<!-- Chamber 3: Snape's Potions -->
|
||||
<div class="chamber-card">
|
||||
<span class="chamber-badge {% if state.bottles_solved %}solved{% endif %}">
|
||||
{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
|
||||
</span>
|
||||
<div class="chamber-icon">🧪🔥</div>
|
||||
<h3 class="chamber-title">Kammer II: Snapes Flaschenrätsel</h3>
|
||||
<h3 class="chamber-title">Kammer III: Snapes Flaschen</h3>
|
||||
<p class="chamber-desc">
|
||||
Schwarze Flammen versperren den Weg zum Stein, violette Flammen den Rückweg.
|
||||
Auf dem Tisch stehen 7 Flaschen und Snapes unerbittliches Logik-Gedicht.
|
||||
@@ -50,6 +64,7 @@
|
||||
<a href="/bottles" class="btn-magical">Flammenkammer betreten 🧪</a>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Bonus: Alchemy Sliding Puzzle -->
|
||||
<div class="chamber-card">
|
||||
<span class="chamber-badge {% if state.sliding_solved %}solved{% endif %}">
|
||||
|
||||
+13
-1
@@ -15,6 +15,10 @@ def test_endpoints():
|
||||
assert res.status_code == 200
|
||||
assert "Der Besenflug" in res.text
|
||||
|
||||
res = client.get("/chess")
|
||||
assert res.status_code == 200
|
||||
assert "McGonagalls Zauberschach" in res.text
|
||||
|
||||
res = client.get("/bottles")
|
||||
assert res.status_code == 200
|
||||
assert "Snapes Flaschenrätsel" in res.text
|
||||
@@ -50,6 +54,14 @@ def test_endpoints():
|
||||
assert data["success"] is True
|
||||
assert "silver_key" in data["state"]["inventory"]
|
||||
|
||||
# Test API Chess complete (adds chess_knight)
|
||||
res = client.post("/api/chess/complete")
|
||||
assert res.status_code == 200
|
||||
chess_data = res.json()
|
||||
assert chess_data["success"] is True
|
||||
assert chess_data["new_item"]["id"] == "chess_knight"
|
||||
assert "chess_knight" in chess_data["state"]["inventory"]
|
||||
|
||||
# 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
|
||||
@@ -63,7 +75,7 @@ def test_endpoints():
|
||||
assert res.status_code == 200
|
||||
assert res.json()["success"] is True
|
||||
|
||||
print("ALL INVENTORY & GAMEPLAY TESTS PASSED! 🎒✨")
|
||||
print("ALL TESTS (INCLUDING CHESS & MOBILE FLIGHT) PASSED! ♟️🧹✨")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_endpoints()
|
||||
|
||||
Reference in New Issue
Block a user