Login-Frontend + Lehrer-Dashboard + Stadt-Simulation komplett

- login.html: Schueler-Login (Klassencode+User+PW) + Lehrer-Login/Register
- teacher.html: Vollstaendiges Dashboard (Klassen, Schueler, Module, Stats)
- stadt.html: Komplette Simulation mit 27 Kachel-Typen + Kennwerten
- tile-test.html: Spital-Sackgassen gefixt
- PHP-Pages fuer alle neuen HTML-Seiten

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 08:37:08 +02:00
parent 33fae37838
commit b06aa025c1
8 changed files with 720 additions and 404 deletions
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Anmelden</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#fff;border-radius:16px;padding:2rem;max-width:400px;width:90%;box-shadow:0 8px 30px rgba(0,0,0,.08)}
.logo{text-align:center;margin-bottom:1.2rem}
.logo h1{font-size:1.3rem;font-weight:800;color:#4a7c8a;letter-spacing:-.02em}
.logo p{font-size:.78rem;color:#8a8a8a;margin-top:.2rem}
.tabs{display:flex;gap:0;margin-bottom:1.2rem;border-radius:8px;overflow:hidden;border:1px solid rgba(0,0,0,.08)}
.tab{flex:1;padding:.5rem;text-align:center;font-size:.75rem;font-weight:600;cursor:pointer;background:#f7f6f3;color:#8a8a8a;transition:all .15s}
.tab.on{background:#4a7c8a;color:#fff}
label{display:block;font-size:.72rem;font-weight:600;color:#4a4a4a;margin-bottom:.2rem;margin-top:.8rem}
input{width:100%;padding:.55rem .7rem;border:1.5px solid rgba(0,0,0,.1);border-radius:7px;font-size:.82rem;font-family:inherit;outline:none;transition:border .15s}
input:focus{border-color:#4a7c8a}
.btn{width:100%;padding:.6rem;border:none;border-radius:8px;background:#4a7c8a;color:#fff;font-size:.85rem;font-weight:700;cursor:pointer;margin-top:1rem;transition:background .15s}
.btn:hover{background:#3a6470}
.btn-secondary{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
.btn-secondary:hover{background:#dae8ec}
.msg{margin-top:.8rem;font-size:.75rem;padding:.4rem .6rem;border-radius:6px;display:none}
.msg.err{display:block;background:#f2ddd8;color:#c07a6b}
.msg.ok{display:block;background:#dceadd;color:#5a8a5e}
.link{text-align:center;margin-top:.8rem;font-size:.72rem;color:#8a8a8a}
.link a{color:#4a7c8a;text-decoration:none;font-weight:600}
.hidden{display:none}
</style>
</head>
<body>
<div class="card">
<div class="logo">
<h1>🌍 GeoGraSim</h1>
<p>Interaktive Simulationen für den Geografieunterricht</p>
</div>
<div class="tabs">
<div class="tab on" data-tab="student">Schüler:in</div>
<div class="tab" data-tab="teacher">Lehrkraft</div>
</div>
<!-- SCHUELER LOGIN -->
<form id="f-student">
<label>Klassencode</label>
<input type="text" id="s-code" placeholder="z.B. GEO1A" maxlength="6" autocapitalize="characters">
<label>Benutzername</label>
<input type="text" id="s-user" placeholder="z.B. schueler01">
<label>Passwort</label>
<input type="password" id="s-pass">
<button type="submit" class="btn">Anmelden</button>
</form>
<!-- LEHRER LOGIN -->
<form id="f-teacher" class="hidden">
<div id="teacher-login">
<label>E-Mail</label>
<input type="email" id="t-email" placeholder="name@schule.at">
<label>Passwort</label>
<input type="password" id="t-pass">
<button type="submit" class="btn">Anmelden</button>
<div class="link"><a href="#" id="show-register">Noch kein Konto? Jetzt registrieren</a></div>
</div>
<div id="teacher-register" class="hidden">
<label>Name</label>
<input type="text" id="r-name" placeholder="Vor- und Nachname">
<label>Schule</label>
<input type="text" id="r-school" placeholder="Name der Schule">
<label>E-Mail</label>
<input type="email" id="r-email" placeholder="name@schule.at">
<label>Passwort (mind. 8 Zeichen)</label>
<input type="password" id="r-pass" minlength="8">
<label>Passwort bestätigen</label>
<input type="password" id="r-pass2">
<button type="submit" class="btn">Registrieren</button>
<div class="link"><a href="#" id="show-login">Bereits registriert? Anmelden</a></div>
</div>
</form>
<div class="msg" id="msg"></div>
</div>
<script>
var BASE = (window.__GGS__?.baseUrl) || ''
var tabs = document.querySelectorAll('.tab')
var fStudent = document.getElementById('f-student')
var fTeacher = document.getElementById('f-teacher')
var msg = document.getElementById('msg')
var isRegister = false
tabs.forEach(function(t) {
t.addEventListener('click', function() {
tabs.forEach(function(x){x.className='tab'})
this.className='tab on'
fStudent.classList.toggle('hidden', this.dataset.tab !== 'student')
fTeacher.classList.toggle('hidden', this.dataset.tab !== 'teacher')
msg.className = 'msg'
})
})
document.getElementById('show-register').addEventListener('click', function(e) {
e.preventDefault(); isRegister = true
document.getElementById('teacher-login').classList.add('hidden')
document.getElementById('teacher-register').classList.remove('hidden')
})
document.getElementById('show-login').addEventListener('click', function(e) {
e.preventDefault(); isRegister = false
document.getElementById('teacher-login').classList.remove('hidden')
document.getElementById('teacher-register').classList.add('hidden')
})
function showMsg(text, type) {
msg.textContent = text
msg.className = 'msg ' + type
}
async function api(action, data) {
var res = await fetch(BASE + '/api/auth', {
method: 'POST', headers: {'Content-Type':'application/json'}, credentials:'same-origin',
body: JSON.stringify(Object.assign({action: action}, data))
})
return await res.json()
}
// Schueler-Login
fStudent.addEventListener('submit', async function(e) {
e.preventDefault()
var code = document.getElementById('s-code').value.trim().toUpperCase()
var user = document.getElementById('s-user').value.trim()
var pass = document.getElementById('s-pass').value
if (!code || !user || !pass) { showMsg('Alle Felder ausfüllen!', 'err'); return }
var r = await api('student_login', {classCode: code, username: user, password: pass})
if (r.error) { showMsg(r.error, 'err'); return }
showMsg('Willkommen, ' + r.displayName + '!', 'ok')
setTimeout(function() { location.href = 'index.html' }, 800)
})
// Lehrer-Login/Register
fTeacher.addEventListener('submit', async function(e) {
e.preventDefault()
if (isRegister) {
var name = document.getElementById('r-name').value.trim()
var school = document.getElementById('r-school').value.trim()
var email = document.getElementById('r-email').value.trim()
var pass = document.getElementById('r-pass').value
var pass2 = document.getElementById('r-pass2').value
if (!name || !email || !pass) { showMsg('Alle Felder ausfüllen!', 'err'); return }
if (pass !== pass2) { showMsg('Passwörter stimmen nicht überein!', 'err'); return }
if (pass.length < 8) { showMsg('Passwort muss mindestens 8 Zeichen haben!', 'err'); return }
var r = await api('register', {displayName: name, schoolName: school, email: email, password: pass})
if (r.error) { showMsg(r.error, 'err'); return }
showMsg('Registrierung erfolgreich!', 'ok')
setTimeout(function() { location.href = 'teacher.html' }, 800)
} else {
var email = document.getElementById('t-email').value.trim()
var pass = document.getElementById('t-pass').value
if (!email || !pass) { showMsg('E-Mail und Passwort ausfüllen!', 'err'); return }
var r = await api('login', {email: email, password: pass})
if (r.error) { showMsg(r.error, 'err'); return }
showMsg('Willkommen, ' + r.displayName + '!', 'ok')
setTimeout(function() { location.href = 'teacher.html' }, 800)
}
})
// Check ob schon eingeloggt
api('status', {}).then(function(r) {
if (r.role === 'teacher') location.href = 'teacher.html'
if (r.role === 'student') location.href = 'index.html'
})
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<?php renderPage('handbuch.html');
+1
View File
@@ -0,0 +1 @@
<?php renderPage('login.html');
+1
View File
@@ -0,0 +1 @@
<?php renderPage('stadt-editor.html');
+1
View File
@@ -0,0 +1 @@
<?php renderPage('teacher.html');
+250 -403
View File
@@ -5,446 +5,293 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Stadt & Raumplanung</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#f4f3ef;--white:#fff;--text:#1a1a1a;--text2:#4a4a4a;--text3:#8a8a8a;--fjord:#4a7c8a;--fjord-l:#dae8ec;--fjord-d:#3a6470;--moss:#5a8a5e;--sand:#c4a35a;--coral:#c07a6b;--coral-l:#f2ddd8}
html,body{height:100%;overflow:hidden}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
.nav{padding:.4rem .8rem;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(0,0,0,.05);background:var(--white);flex-shrink:0}
.nav-brand{display:flex;align-items:center;gap:.4rem;font-weight:800;font-size:.9rem;color:var(--fjord);text-decoration:none}
.nav-brand img{width:24px;height:24px;border-radius:5px}
.nav-btn{padding:.25rem .5rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:var(--white);color:var(--text2);font-size:.65rem;font-weight:600;cursor:pointer;text-decoration:none}
.main{flex:1;display:grid;grid-template-columns:160px 1fr 180px;gap:.3rem;padding:.3rem;min-height:0;overflow:hidden}
.panel{background:var(--white);border-radius:8px;border:1px solid rgba(0,0,0,.04);padding:.4rem;box-shadow:0 1px 3px rgba(0,0,0,.02);overflow-y:auto}
.panel-h{font-size:.54rem;font-weight:700;color:var(--fjord);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem}
.col-left,.col-right{display:flex;flex-direction:column;gap:.3rem;overflow-y:auto}
.col-center{display:flex;flex-direction:column;gap:.25rem;min-height:0}
.topbar{flex-shrink:0;display:flex;align-items:center;gap:.4rem;background:var(--white);border-radius:7px;padding:.25rem .5rem;border:1px solid rgba(0,0,0,.04)}
.topbar .stat{font-size:.65rem;font-weight:700;display:flex;align-items:center;gap:.15rem}
.topbar .v{font-size:.8rem}
.topbar .gold .v{color:var(--sand)}
.topbar .rnd{font-size:.62rem;font-weight:700;color:var(--fjord);margin-left:auto;background:var(--fjord-l);padding:.1rem .4rem;border-radius:4px}
.btn-next{padding:.25rem .6rem;border:none;border-radius:5px;background:var(--fjord);color:#fff;font-size:.65rem;font-weight:700;cursor:pointer;margin-left:.2rem}
.canvas-wrap{flex:1;background:#92aa6a;border-radius:8px;overflow:hidden;position:relative;min-height:200px;cursor:pointer}
.canvas-wrap canvas{width:100%;height:100%;display:block}
.param{display:flex;align-items:center;gap:.2rem;padding:.15rem 0;border-bottom:1px solid rgba(0,0,0,.02)}.param:last-child{border:none}
.param-i{font-size:.68rem;width:15px;text-align:center}.param-inf{flex:1}
.param-n{font-size:.5rem;color:var(--text3)}.param-bar{height:3px;background:#e8e5dc;border-radius:1.5px;margin-top:1px;overflow:hidden}
.param-bar-f{height:100%;border-radius:1.5px;transition:width .4s}.param-v{font-size:.58rem;font-weight:700;min-width:15px;text-align:right}
.tb{display:flex;align-items:center;gap:.2rem;padding:.22rem .3rem;border:1.5px solid rgba(0,0,0,.04);border-radius:5px;background:var(--white);cursor:pointer;width:100%;text-align:left;margin-bottom:.1rem;transition:all .1s;font-size:.55rem}
.tb:hover{border-color:var(--fjord);background:var(--fjord-l)}.tb.on{border-color:var(--fjord);background:var(--fjord);color:#fff}
.tb.off{opacity:.2;pointer-events:none;filter:grayscale(100%)}
.tb .te{font-size:.75rem;width:16px;text-align:center}.tb .tn{flex:1;font-weight:600}.tb .tc{font-weight:600;color:var(--sand);font-size:.48rem}
.msg{font-size:.52rem;padding:.15rem .25rem;border-radius:4px;margin-bottom:.08rem;background:var(--fjord-l);color:var(--fjord-d);line-height:1.25}
.msg.warn{background:var(--coral-l);color:var(--coral)}.msg.good{background:rgba(90,138,94,.12);color:var(--moss)}
.ov{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:200;backdrop-filter:blur(3px)}
.ov-c{background:var(--white);border-radius:12px;padding:1.2rem;max-width:380px;width:90%;text-align:center}
.ov-c h2{font-size:.95rem;font-weight:800;margin-bottom:.15rem}.ov-c p{font-size:.7rem;color:var(--text2);margin-bottom:.4rem;line-height:1.35}
.ov-btn{padding:.3rem .8rem;border:none;border-radius:5px;background:var(--fjord);color:#fff;font-size:.7rem;font-weight:700;cursor:pointer}
.hidden{display:none!important}
@media(max-width:800px){.main{grid-template-columns:1fr}}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f0efe8;color:#1a1a1a;display:flex;height:100vh;overflow:hidden}
.sidebar{width:155px;background:#fff;border-right:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column;overflow-y:auto}
.sidebar-section{padding:6px}
.sidebar h3{font-size:8px;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}
.tb{display:flex;align-items:center;gap:3px;padding:3px 4px;border:1.5px solid transparent;border-radius:4px;cursor:pointer;font-size:8px;font-weight:600;margin-bottom:1px;transition:all .1s}
.tb:hover{border-color:#4a7c8a;background:#f0f6f8}
.tb.on{border-color:#4a7c8a;background:#4a7c8a;color:#fff}
.tb.off{opacity:.2;pointer-events:none}
.tb .te{font-size:13px}
.tb .tc{margin-left:auto;font-size:7px;color:#c4a35a}
.center{flex:1;display:flex;flex-direction:column;min-width:0}
.topbar{display:flex;align-items:center;gap:6px;padding:4px 8px;background:#fff;border-bottom:1px solid rgba(0,0,0,.05);flex-shrink:0}
.topbar .stat{font-size:10px;font-weight:700}
.topbar .gold{color:#c4a35a}
.topbar .rnd{font-size:9px;color:#4a7c8a;background:#dae8ec;padding:2px 6px;border-radius:4px;font-weight:700;margin-left:auto}
.topbar .btn{padding:2px 8px;border:none;border-radius:4px;background:#4a7c8a;color:#fff;font-size:9px;font-weight:700;cursor:pointer}
.canvas-area{flex:1;display:flex;align-items:center;justify-content:center;background:#c8d0b8;overflow:hidden}
.canvas-area canvas{max-width:100%;max-height:100%}
.right{width:155px;background:#fff;border-left:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column;overflow-y:auto;padding:6px}
.right h3{font-size:8px;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px}
.param{display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid rgba(0,0,0,.02)}
.param:last-child{border:none}
.param-i{font-size:10px;width:14px;text-align:center}
.param-inf{flex:1}
.param-n{font-size:7px;color:#8a8a8a}
.param-bar{height:3px;background:#e8e5dc;border-radius:1.5px;margin-top:1px;overflow:hidden}
.param-bar-f{height:100%;border-radius:1.5px;transition:width .4s}
.param-v{font-size:8px;font-weight:700;min-width:14px;text-align:right}
.msg{font-size:7px;padding:2px 4px;border-radius:3px;margin-bottom:1px;background:#dae8ec;color:#3a6470;line-height:1.2}
.msg.warn{background:#f2ddd8;color:#c07a6b}
.msg.good{background:#dceadd;color:#5a8a5e}
.ov{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
.ov-c{background:#fff;border-radius:12px;padding:1.2rem;max-width:360px;width:90%;text-align:center}
.ov-c h2{font-size:.9rem;font-weight:800;margin-bottom:.15rem}
.ov-c p{font-size:.7rem;color:#4a4a4a;margin-bottom:.4rem}
.ov-btn{padding:.3rem .8rem;border:none;border-radius:5px;background:#4a7c8a;color:#fff;font-size:.7rem;font-weight:700;cursor:pointer}
.hidden{display:none}
@media(max-width:700px){.sidebar,.right{display:none}.center{width:100%}}
</style>
</head>
<body>
<nav class="nav">
<a href="index.html" class="nav-brand"><img src="assets/img/logo.png" alt="">Stadt & Raumplanung</a>
<div style="display:flex;gap:.15rem"><button id="btn-reset" class="nav-btn">🔄</button><a href="index.html" class="nav-btn"></a></div>
</nav>
<div id="game" class="main">
<div class="col-left">
<div class="panel"><div class="panel-h">Kennwerte</div><div id="params"></div></div>
<div class="panel"><div class="panel-h">Verlauf</div><canvas id="graph" style="width:100%;height:65px;display:block;border-radius:4px;background:#f0eeea"></canvas></div>
<div class="sidebar">
<div class="sidebar-section">
<h3>Straßen</h3>
<div id="road-tools"></div>
</div>
<div class="col-center">
<div class="topbar">
<div class="stat gold">💰 <span class="v" id="h-bud">0</span></div>
<span class="rnd" id="h-rnd">1/10</span>
<button class="btn-next" id="btn-next">▶ Runde</button>
</div>
<div class="canvas-wrap"><canvas id="cv"></canvas></div>
<div class="sidebar-section">
<h3>Gebäude</h3>
<div id="building-tools"></div>
</div>
<div class="col-right">
<div class="panel"><div class="panel-h">Bauen</div><div id="tiles"></div></div>
<div class="panel"><div class="panel-h">Nachrichten</div><div id="msgs"></div></div>
<div class="sidebar-section">
<h3>Natur</h3>
<div id="nature-tools"></div>
</div>
</div>
<div id="ov-intro" class="ov"><div class="ov-c">
<h2>🏘️ Stadt & Raumplanung</h2>
<p>Baue eine Siedlung auf isometrischen Kacheln. Jede Kachel hat Straßen eingebaut. Achte darauf, dass sie zusammenpassen!</p>
<button class="ov-btn" id="btn-go">Los geht's!</button>
</div></div>
<div id="ov-end" class="ov hidden"><div class="ov-c"><h2 id="end-t"></h2><p id="end-p"></p><button class="ov-btn" id="btn-again">Nochmal</button></div></div>
<div class="center">
<div class="topbar">
<span class="stat gold">💰 <span id="h-bud">50</span></span>
<span class="stat" style="font-size:8px;color:#5a8a5e" id="h-inc"></span>
<span class="rnd" id="h-rnd">1/10</span>
<button class="btn" id="btn-next">▶ Runde</button>
</div>
<div class="canvas-area"><canvas id="cv"></canvas></div>
</div>
<div class="right">
<h3>Kennwerte</h3>
<div id="params"></div>
<h3 style="margin-top:6px">Nachrichten</h3>
<div id="msgs"></div>
</div>
<div class="ov" id="ov-intro">
<div class="ov-c">
<h2>🏘️ Stadt & Raumplanung</h2>
<p>Baue eine Siedlung! Wähle links Kacheln und klicke ins Raster. Klicke oben-links/rechts auf platzierte Kacheln zum Drehen. Jede Runde bekommst du neues Budget.</p>
<button class="ov-btn" id="btn-go">Los geht's!</button>
</div>
</div>
<div class="ov hidden" id="ov-end">
<div class="ov-c"><h2 id="end-t"></h2><p id="end-p"></p><button class="ov-btn" id="btn-again">Nochmal</button></div>
</div>
<script>
// =====================================================================
// STADT & RAUMPLANUNG — Isometrische Kacheln (SimCity-Stil)
// Template-basiertes Kachel-System
// =====================================================================
var COLS=10, ROWS=10, TS=60
var GREEN='#b8ccaa', ROAD='#2e2e2e', WHITE='#fff'
const GW=10, GH=10
const RW = 0.30 // Strassenbreite relativ zur Kachel
// === ALLE KACHEL-TYPEN ===
// cat: road/building/nature, conn: [N,E,S,W], emoji: fuer Gebaeude
var TILES = [
// Strassen (cat=0)
{n:'Gerade │', cat:0, conn:[1,0,1,0], cost:2, emoji:'', eff:{}},
{n:'Gerade ─', cat:0, conn:[0,1,0,1], cost:2, emoji:'', eff:{}},
{n:'Kurve ╯', cat:0, conn:[1,1,0,0], cost:2, emoji:'', eff:{}},
{n:'Kurve ╮', cat:0, conn:[0,1,1,0], cost:2, emoji:'', eff:{}},
{n:'Kurve ╭', cat:0, conn:[0,0,1,1], cost:2, emoji:'', eff:{}},
{n:'Kurve ╰', cat:0, conn:[1,0,0,1], cost:2, emoji:'', eff:{}},
{n:'T ┴', cat:0, conn:[1,1,0,1], cost:3, emoji:'', eff:{}},
{n:'T ├', cat:0, conn:[1,1,1,0], cost:3, emoji:'', eff:{}},
{n:'T ┬', cat:0, conn:[0,1,1,1], cost:3, emoji:'', eff:{}},
{n:'T ┤', cat:0, conn:[1,0,1,1], cost:3, emoji:'', eff:{}},
{n:'Kreuzung', cat:0, conn:[1,1,1,1], cost:4, emoji:'', eff:{}},
// Gebaeude (cat=1) — mit Strassen + Emojis
{n:'Wohngebiet', cat:1, conn:[1,0,1,0], cost:6, emoji:'🏠🏡🏠', emojis:[{e:'🏠',x:-0.32,y:-0.2,s:0.15},{e:'🏡',x:0.32,y:0,s:0.15},{e:'🏠',x:-0.3,y:0.2,s:0.13},{e:'🏠',x:0.3,y:0.25,s:0.12}], eff:{eco:2,sat:2,env:-1,traf:1}},
{n:'Laden', cat:1, conn:[1,0,1,0], cost:7, emoji:'🏪', emojis:[{e:'🏪',x:0.3,y:0,s:0.22}], eff:{eco:4,sat:2,env:-1,traf:2,sup:5}},
{n:'Schule', cat:1, conn:[0,1,0,1], cost:9, emoji:'🏫', emojis:[{e:'🏫',x:0,y:-0.25,s:0.22}], eff:{eco:1,sat:5,traf:1,sup:4}},
{n:'Kirche', cat:1, conn:[0,0,1,0], cost:8, emoji:'⛪', emojis:[{e:'⛪',x:0,y:-0.05,s:0.4}], eff:{eco:1,sat:4,env:1,sup:2}},
{n:'Fabrik', cat:1, conn:[0,0,1,0], cost:8, emoji:'🏭', emojis:[{e:'🏭',x:0,y:-0.05,s:0.35}], eff:{eco:6,sat:-3,env:-6,traf:4}},
{n:'Spital', cat:1, conn:[1,0,0,0], cost:11,emoji:'🏥', emojis:[{e:'🏥',x:0,y:0.1,s:0.28},{e:'🚑',x:0,y:0.35,s:0.12}], eff:{eco:2,sat:5,traf:2,sup:5}},
{n:'Feuerwehr', cat:1, conn:[0,0,1,0], cost:8, emoji:'🚒', emojis:[{e:'🏠',x:0,y:-0.15,s:0.25},{e:'🚒',x:-0.15,y:0.2,s:0.18},{e:'🚒',x:0.15,y:0.2,s:0.18}], eff:{eco:1,sat:3,sup:3}},
{n:'Polizei', cat:1, conn:[1,0,0,0], cost:7, emoji:'🚔', emojis:[{e:'🏢',x:0,y:0.1,s:0.25},{e:'🚔',x:0,y:0.32,s:0.15}], eff:{eco:1,sat:3,sup:2}},
{n:'Supermarkt', cat:1, conn:[1,0,1,0], cost:9, emoji:'🏪', emojis:[{e:'🏪',x:0.3,y:-0.1,s:0.22},{e:'🚗',x:0.28,y:0.25,s:0.1},{e:'🚙',x:0.36,y:0.15,s:0.1}], eff:{eco:5,sat:3,env:-2,traf:3,sup:8}},
{n:'Hochhäuser', cat:1, conn:[1,0,1,0], cost:10,emoji:'🏢', emojis:[{e:'🏢',x:0.25,y:-0.15,s:0.16},{e:'🏢',x:0.33,y:-0.15,s:0.16},{e:'🏢',x:0.25,y:0.12,s:0.16},{e:'🏢',x:0.33,y:0.12,s:0.16}], eff:{eco:4,env:-2,traf:3}},
{n:'Rathaus', cat:1, conn:[0,0,1,0], cost:12,emoji:'🏛️', emojis:[{e:'🏛️',x:0,y:-0.05,s:0.35}], eff:{eco:3,sat:4,sup:3}},
{n:'Tankstelle', cat:1, conn:[1,0,1,0], cost:5, emoji:'⛽', emojis:[{e:'⛽',x:0.3,y:-0.15,s:0.14},{e:'⛽',x:0.3,y:0,s:0.14},{e:'⛽',x:0.3,y:0.15,s:0.14},{e:'🚗',x:0.38,y:0.05,s:0.1}], eff:{eco:2,env:-2,traf:1}},
{n:'Bauernhof', cat:1, conn:[0,1,0,0], cost:5, emoji:'🌾', emojis:[{e:'🏠',x:0,y:0,s:0.22},{e:'🌾',x:-0.3,y:-0.25,s:0.1},{e:'🌾',x:-0.15,y:-0.25,s:0.1},{e:'🌾',x:0,y:-0.25,s:0.1},{e:'🌾',x:0.15,y:-0.25,s:0.1},{e:'🐄',x:-0.2,y:0.25,s:0.13}], eff:{eco:3,sat:1,env:2,sup:3}},
// Natur (cat=2)
{n:'Park', cat:2, conn:[1,0,0,0], cost:4, emoji:'🌳', emojis:[{e:'🌳',x:-0.25,y:0.05,s:0.14},{e:'🌳',x:-0.05,y:0.05,s:0.14},{e:'🌳',x:0.15,y:0.05,s:0.14},{e:'🌳',x:0.3,y:0.05,s:0.13},{e:'🌷',x:-0.2,y:0.3,s:0.07},{e:'🌷',x:0,y:0.3,s:0.07},{e:'🌷',x:0.2,y:0.3,s:0.07}], eff:{sat:4,env:6,sup:1}},
{n:'Spielplatz', cat:2, conn:[1,0,0,0], cost:4, emoji:'🛝', emojis:[{e:'🛝',x:-0.15,y:0.08,s:0.18},{e:'🎠',x:0.18,y:0.08,s:0.15},{e:'⚽',x:0,y:0.3,s:0.08},{e:'🌳',x:-0.35,y:0.3,s:0.1},{e:'🌳',x:0.35,y:0.3,s:0.1}], eff:{sat:5,env:3}},
]
// === TILE TEMPLATE SYSTEM ===
// Jede Kachel wird durch ein Template definiert:
// conn: [N,E,S,W] 1=Strasse verbindet an dieser Kante
// stub: [N,E,S,W] 0-1, wie weit die Zufahrt reinragt (0=keine, 0.1=kurz/Parkplatz, 0.5=halb, 1=voll)
// content: Array von {emoji, ox, oy} relativ zum Kachel-Zentrum (-0.5..0.5)
// eff: Effekte auf Simulation
const TILES = {
empty: {n:'Leer', cost:0, conn:[0,0,0,0], stub:[0,0,0,0], content:[], eff:{}},
// Strassen
road_ns: {n:'Straße N-S', cost:2, conn:[1,0,1,0], stub:[1,0,1,0], content:[], eff:{traf:1,env:-1}},
road_ew: {n:'Straße O-W', cost:2, conn:[0,1,0,1], stub:[0,1,0,1], content:[], eff:{traf:1,env:-1}},
road_cross: {n:'Kreuzung', cost:3, conn:[1,1,1,1], stub:[1,1,1,1], content:[], eff:{traf:2,env:-1}},
road_t_ews: {n:'T offen S', cost:3, conn:[0,1,1,1], stub:[0,1,1,1], content:[], eff:{traf:1,env:-1}},
road_t_new: {n:'T offen W', cost:3, conn:[1,1,0,1], stub:[1,1,0,1], content:[], eff:{traf:1,env:-1}},
road_t_nes: {n:'T offen O', cost:3, conn:[1,1,1,0], stub:[1,1,1,0], content:[], eff:{traf:1,env:-1}},
road_t_nsw: {n:'T offen N', cost:3, conn:[1,0,1,1], stub:[1,0,1,1], content:[], eff:{traf:1,env:-1}},
road_ne: {n:'Kurve N→O', cost:2, conn:[1,1,0,0], stub:[1,1,0,0], content:[], eff:{traf:1,env:-1}},
road_es: {n:'Kurve O→S', cost:2, conn:[0,1,1,0], stub:[0,1,1,0], content:[], eff:{traf:1,env:-1}},
road_sw: {n:'Kurve S→W', cost:2, conn:[0,0,1,1], stub:[0,0,1,1], content:[], eff:{traf:1,env:-1}},
road_wn: {n:'Kurve W→N', cost:2, conn:[1,0,0,1], stub:[1,0,0,1], content:[], eff:{traf:1,env:-1}},
// Gebaeude — Strasse geht durch, Gebaeude am Rand
housing_ns: {n:'Wohngebiet │', cost:6, conn:[1,0,1,0], stub:[1,0,1,0],
content:[{e:'🏠',ox:-0.3,oy:-0.1},{e:'🏡',ox:0.3,oy:0.1},{e:'🏠',ox:0.3,oy:-0.15}], eff:{eco:2,sat:2,env:-1,traf:1}},
housing_ew: {n:'Wohngebiet ─', cost:6, conn:[0,1,0,1], stub:[0,1,0,1],
content:[{e:'🏠',ox:-0.1,oy:-0.3},{e:'🏡',ox:0.1,oy:0.3}], eff:{eco:2,sat:2,env:-1,traf:1}},
shop_s: {n:'Laden ↓', cost:7, conn:[0,0,1,0], stub:[0,0,0.3,0],
content:[{e:'🏪',ox:0,oy:-0.15}], eff:{eco:4,sat:2,env:-1,traf:2,sup:5}},
shop_n: {n:'Laden ↑', cost:7, conn:[1,0,0,0], stub:[0.3,0,0,0],
content:[{e:'🏪',ox:0,oy:0.1}], eff:{eco:4,sat:2,env:-1,traf:2,sup:5}},
park_n: {n:'Park ↑', cost:4, conn:[1,0,0,0], stub:[0.1,0,0,0],
content:[{e:'🌳',ox:-0.15,oy:0.05},{e:'🌲',ox:0.2,oy:-0.1},{e:'🌷',ox:0,oy:0.2,s:0.6}], eff:{sat:4,env:6,sup:1}},
park_s: {n:'Park ↓', cost:4, conn:[0,0,1,0], stub:[0,0,0.1,0],
content:[{e:'🌳',ox:-0.15,oy:-0.1},{e:'🌲',ox:0.2,oy:0.05},{e:'🌷',ox:0,oy:-0.2,s:0.6}], eff:{sat:4,env:6,sup:1}},
school_es: {n:'Schule O↓', cost:9, conn:[0,1,1,0], stub:[0,0.3,0.3,0],
content:[{e:'🏫',ox:-0.15,oy:-0.15}], eff:{eco:1,sat:5,traf:1,sup:4}},
industry_sw: {n:'Industrie S←', cost:8, conn:[0,0,1,1], stub:[0,0,0.5,0.5],
content:[{e:'🏭',ox:0.1,oy:-0.15}], eff:{eco:6,sat:-3,env:-6,traf:4}},
var grid = []
for(var r=0;r<ROWS;r++){grid[r]=[];for(var c=0;c<COLS;c++)grid[r][c]=null}
// Start: ein paar Strassen + 2 Haeuser
grid[4][4]={t:1,r:0};grid[4][5]={t:1,r:0};grid[4][6]={t:1,r:0} // Gerade E-W
grid[3][5]={t:11,r:0} // Wohngebiet
var sel=-1, budget=50, round=1, maxRounds=10, income=6, gameOver=false
var scores={eco:40,sat:40,env:55,traf:15,sup:15,reach:25}
var messages=[{text:'Willkommen! Baue Straßen und Gebäude.',type:''}]
function rotConn(c,n){var r=c.slice();for(var i=0;i<n;i++)r=[r[3],r[0],r[1],r[2]];return r}
// === DRAW TILE ===
var cv=document.getElementById('cv'), ctx=cv.getContext('2d')
cv.width=COLS*TS;cv.height=ROWS*TS
function drawTile(px,py,conn,alpha){
var S=TS,RW=S*0.4,M=S/2,HR=RW/2,lw=Math.max(1,S/50)
var N=conn[0],E=conn[1],So=conn[2],W=conn[3],sum=N+E+So+W
if(alpha<1)ctx.globalAlpha=alpha
ctx.fillStyle=GREEN;ctx.fillRect(px,py,S,S)
if(sum===1){ctx.fillStyle=ROAD;var st=Math.max(2,S*0.02)
if(N){ctx.fillRect(px+M-HR,py,RW,st);ctx.beginPath();ctx.arc(px+M,py+st,HR,0,Math.PI);ctx.fill()}
if(So){ctx.fillRect(px+M-HR,py+S-st,RW,st);ctx.beginPath();ctx.arc(px+M,py+S-st,HR,Math.PI,0);ctx.fill()}
if(E){ctx.fillRect(px+S-st,py+M-HR,st,RW);ctx.beginPath();ctx.arc(px+S-st,py+M,HR,Math.PI*1.5,Math.PI*0.5);ctx.fill()}
if(W){ctx.fillRect(px,py+M-HR,st,RW);ctx.beginPath();ctx.arc(px+st,py+M,HR,Math.PI*0.5,Math.PI*1.5);ctx.fill()}
ctx.strokeStyle=WHITE;ctx.lineWidth=lw
if(N){ctx.beginPath();ctx.moveTo(px+M-HR,py);ctx.lineTo(px+M-HR,py+st);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py);ctx.lineTo(px+M+HR,py+st);ctx.stroke();ctx.beginPath();ctx.arc(px+M,py+st,HR,0,Math.PI);ctx.stroke()}
if(So){ctx.beginPath();ctx.moveTo(px+M-HR,py+S);ctx.lineTo(px+M-HR,py+S-st);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py+S);ctx.lineTo(px+M+HR,py+S-st);ctx.stroke();ctx.beginPath();ctx.arc(px+M,py+S-st,HR,Math.PI,0);ctx.stroke()}
if(E){ctx.beginPath();ctx.moveTo(px+S,py+M-HR);ctx.lineTo(px+S-st,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px+S,py+M+HR);ctx.lineTo(px+S-st,py+M+HR);ctx.stroke();ctx.beginPath();ctx.arc(px+S-st,py+M,HR,Math.PI*1.5,Math.PI*0.5);ctx.stroke()}
if(W){ctx.beginPath();ctx.moveTo(px,py+M-HR);ctx.lineTo(px+st,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M+HR);ctx.lineTo(px+st,py+M+HR);ctx.stroke();ctx.beginPath();ctx.arc(px+st,py+M,HR,Math.PI*0.5,Math.PI*1.5);ctx.stroke()}
ctx.globalAlpha=1;return}
var isCurve=sum===2&&((N&&E)||(E&&So)||(So&&W)||(W&&N))
if(isCurve){var cx_,cy_,sa,ea
if(N&&E){cx_=px+M+HR;cy_=py+M-HR;sa=Math.PI;ea=Math.PI*0.5}
if(E&&So){cx_=px+M+HR;cy_=py+M+HR;sa=Math.PI*1.5;ea=Math.PI}
if(So&&W){cx_=px+M-HR;cy_=py+M+HR;sa=0;ea=Math.PI*1.5}
if(W&&N){cx_=px+M-HR;cy_=py+M-HR;sa=Math.PI*0.5;ea=0}
ctx.fillStyle=ROAD
if(N)ctx.fillRect(px+M-HR,py,RW,M-HR);if(So)ctx.fillRect(px+M-HR,py+M+HR,RW,M-HR)
if(E)ctx.fillRect(px+M+HR,py+M-HR,M-HR,RW);if(W)ctx.fillRect(px,py+M-HR,M-HR,RW)
ctx.beginPath();ctx.moveTo(cx_,cy_);ctx.arc(cx_,cy_,RW,sa,ea,true);ctx.closePath();ctx.fill()
ctx.strokeStyle=WHITE;ctx.lineWidth=lw;ctx.beginPath();ctx.arc(cx_,cy_,RW,sa,ea,true);ctx.stroke()
if(N){ctx.beginPath();ctx.moveTo(px+M-HR,py);ctx.lineTo(px+M-HR,cy_);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py);ctx.lineTo(px+M+HR,cy_);ctx.stroke()}
if(So){ctx.beginPath();ctx.moveTo(px+M-HR,cy_);ctx.lineTo(px+M-HR,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,cy_);ctx.lineTo(px+M+HR,py+S);ctx.stroke()}
if(E){ctx.beginPath();ctx.moveTo(cx_,py+M-HR);ctx.lineTo(px+S,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(cx_,py+M+HR);ctx.lineTo(px+S,py+M+HR);ctx.stroke()}
if(W){ctx.beginPath();ctx.moveTo(px,py+M-HR);ctx.lineTo(cx_,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M+HR);ctx.lineTo(cx_,py+M+HR);ctx.stroke()}
ctx.setLineDash([S*0.07,S*0.05]);ctx.lineDashOffset=-S*0.035;ctx.beginPath()
if(N&&E){ctx.moveTo(px+M,py);ctx.lineTo(px+M,cy_);ctx.arc(cx_,cy_,HR,Math.PI,Math.PI*0.5,true);ctx.lineTo(px+S,py+M)}
if(E&&So){ctx.moveTo(px+S,py+M);ctx.lineTo(cx_,py+M);ctx.arc(cx_,cy_,HR,Math.PI*1.5,Math.PI,true);ctx.lineTo(px+M,py+S)}
if(So&&W){ctx.moveTo(px+M,py+S);ctx.lineTo(px+M,cy_);ctx.arc(cx_,cy_,HR,0,Math.PI*1.5,true);ctx.lineTo(px,py+M)}
if(W&&N){ctx.moveTo(px,py+M);ctx.lineTo(cx_,py+M);ctx.arc(cx_,cy_,HR,Math.PI*0.5,0,true);ctx.lineTo(px+M,py)}
ctx.stroke();ctx.setLineDash([]);ctx.lineDashOffset=0;ctx.globalAlpha=1;return}
ctx.fillStyle=ROAD
if(N)ctx.fillRect(px+M-HR,py,RW,M+HR);if(So)ctx.fillRect(px+M-HR,py+M-HR,RW,M+HR)
if(E)ctx.fillRect(px+M-HR,py+M-HR,M+HR,RW);if(W)ctx.fillRect(px,py+M-HR,M+HR,RW)
ctx.strokeStyle=WHITE;ctx.lineWidth=lw
if(N||So){ctx.beginPath();ctx.moveTo(px+M-HR,N?py:py+M-HR);ctx.lineTo(px+M-HR,So?py+S:py+M+HR);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,N?py:py+M-HR);ctx.lineTo(px+M+HR,So?py+S:py+M+HR);ctx.stroke()}
if(E||W){ctx.beginPath();ctx.moveTo(W?px:px+M-HR,py+M-HR);ctx.lineTo(E?px+S:px+M+HR,py+M-HR);ctx.stroke();ctx.beginPath();ctx.moveTo(W?px:px+M-HR,py+M+HR);ctx.lineTo(E?px+S:px+M+HR,py+M+HR);ctx.stroke()}
if((N||So)&&(E||W)){ctx.fillStyle=ROAD;ctx.fillRect(px+M-HR+1,py+M-HR-1,RW-2,lw+2);ctx.fillRect(px+M-HR+1,py+M+HR-lw,RW-2,lw+2);ctx.fillRect(px+M-HR-1,py+M-HR+1,lw+2,RW-2);ctx.fillRect(px+M+HR-lw,py+M-HR+1,lw+2,RW-2)}
ctx.strokeStyle=WHITE;ctx.lineWidth=lw;ctx.setLineDash([S*0.07,S*0.05]);ctx.lineDashOffset=-S*0.035
if(sum===2){if(N||So){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke()};if(E||W){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke()}}
else if(sum===3){if(!N){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M,py+M+HR);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.setLineDash([]);ctx.lineWidth=lw*2;ctx.beginPath();ctx.moveTo(px+M-HR+3,py+M+HR+6);ctx.lineTo(px+M,py+M+HR+6);ctx.stroke();ctx.lineWidth=lw}
if(!So){ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+M-HR);ctx.stroke();ctx.setLineDash([]);ctx.lineWidth=lw*2;ctx.beginPath();ctx.moveTo(px+M,py+M-HR-6);ctx.lineTo(px+M+HR-3,py+M-HR-6);ctx.stroke();ctx.lineWidth=lw}
if(!W){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.setLineDash([]);ctx.lineWidth=lw*2;ctx.beginPath();ctx.moveTo(px+M+HR+6,py+M);ctx.lineTo(px+M+HR+6,py+M+HR-3);ctx.stroke();ctx.lineWidth=lw}
if(!E){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+M-HR,py+M);ctx.stroke();ctx.setLineDash([]);ctx.lineWidth=lw*2;ctx.beginPath();ctx.moveTo(px+M-HR-6,py+M-HR+3);ctx.lineTo(px+M-HR-6,py+M);ctx.stroke();ctx.lineWidth=lw}}
else if(sum===4){ctx.beginPath();ctx.moveTo(px+M,py);ctx.lineTo(px+M,py+S);ctx.stroke();ctx.beginPath();ctx.moveTo(px,py+M);ctx.lineTo(px+M-HR,py+M);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR,py+M);ctx.lineTo(px+S,py+M);ctx.stroke();ctx.setLineDash([]);ctx.lineWidth=lw*2;ctx.beginPath();ctx.moveTo(px+M-HR+6,py+M);ctx.lineTo(px+M-HR+6,py+M+HR-3);ctx.stroke();ctx.beginPath();ctx.moveTo(px+M+HR-6,py+M-HR+3);ctx.lineTo(px+M+HR-6,py+M);ctx.stroke();ctx.lineWidth=lw}
ctx.setLineDash([]);ctx.lineDashOffset=0;ctx.globalAlpha=1
}
function getConn(type){return TILES[type]?.conn||[0,0,0,0]}
// === GAME STATE ===
let grid=[] // GH x GW tile type strings
let level=null,round=0,budget=0,gameOver=false,selType=null
let scores={eco:40,sat:40,env:55,traf:15,sup:15,reach:25}
let messages=[],history=[],ghostR=-1,ghostC=-1
const LEVELS=[{
id:'L1',rounds:10,budget:40,income:6,
unlocked:['road_ns','road_ew','road_cross','road_t_ews','road_t_new','road_t_nes','road_t_nsw','road_ne','road_es','road_sw','road_wn','housing_ns','housing_ew','shop_s','shop_n','park_n','park_s'],
blockBad:false,
initial:[{x:4,y:4,t:'road_ew'},{x:5,y:4,t:'road_ew'},{x:6,y:4,t:'road_ew'},{x:4,y:3,t:'housing_ns'},{x:6,y:5,t:'housing_ns'}],
events:[{r:1,m:'Deine Siedlung braucht einen Laden!'},{r:4,m:'Mehr Wohnraum nötig!'},{r:7,m:'Ein Park erhöht die Zufriedenheit.'},{r:9,m:'Letzte Runde!'}],
win:50
}]
function initGame(lvl){
level=lvl;round=1;budget=lvl.budget;gameOver=false;selType=null;messages=[];history=[]
grid=[];for(let r=0;r<GH;r++){grid[r]=[];for(let c=0;c<GW;c++)grid[r][c]='empty'}
for(const t of lvl.initial)grid[t.y][t.x]=t.t
recalc();const ev=lvl.events.find(e=>e.r===1);if(ev)addMsg(ev.m,'')
function drawEmojis(px,py,tile){
if(!tile.emojis)return
ctx.textAlign='center';ctx.textBaseline='middle'
tile.emojis.forEach(function(em){
ctx.font=Math.round(TS*em.s)+'px serif'
ctx.fillText(em.e, px+TS/2+em.x*TS, py+TS/2+em.y*TS)
})
}
// === SIM ===
// === SIMULATION ===
function recalc(){
let eco=30,sat=35,env=55,traf=10,sup=10,reach=20,bad=0,iso=0
for(let r=0;r<GH;r++)for(let c=0;c<GW;c++){
const type=grid[r][c];if(type==='empty')continue
const def=TILES[type],ef=def.eff||{}
var eco=30,sat=35,env=55,traf=10,sup=10,reach=20
for(var r=0;r<ROWS;r++)for(var c=0;c<COLS;c++){
if(!grid[r][c])continue
var tile=TILES[grid[r][c].t],ef=tile.eff||{}
eco+=ef.eco||0;sat+=ef.sat||0;env+=ef.env||0;traf+=ef.traf||0;sup+=ef.sup||0
const conn=def.conn
const dirs=[[-1,0,0,2],[0,1,1,3],[1,0,2,0],[0,-1,3,1]]
let anyGood=false
for(const[dr,dc,myD,thD]of dirs){
if(!conn[myD])continue
const nr=r+dr,nc=c+dc
if(nr<0||nr>=GH||nc<0||nc>=GW){bad++;continue}
const nb=grid[nr][nc];if(nb==='empty'){bad++;continue}
if(TILES[nb].conn[thD])anyGood=true;else bad++
}
if(def.content.length>0&&!anyGood){iso++;reach-=4}else if(def.content.length>0)reach+=3
// Connection check
var conn=rotConn(tile.conn,grid[r][c].r)
var dirs=[[-1,0,0,2],[0,1,1,3],[1,0,2,0],[0,-1,3,1]],any=false
for(var d=0;d<dirs.length;d++){if(!conn[dirs[d][2]])continue;var nr=r+dirs[d][0],nc=c+dirs[d][1]
if(nr>=0&&nr<ROWS&&nc>=0&&nc<COLS&&grid[nr][nc]){var nb=rotConn(TILES[grid[nr][nc].t].conn,grid[nr][nc].r);if(nb[dirs[d][3]])any=true;else traf++}else traf++}
if(tile.cat===1&&!any){reach-=4}else if(tile.cat===1)reach+=3
// Neighbors
if(type.startsWith('housing')){const nb=nbT(r,c);if(nb.some(n=>n.startsWith('park')))sat+=2;if(nb.some(n=>n.startsWith('industry'))){sat-=4;env-=3};if(nb.some(n=>n.startsWith('shop')))sup+=2}
if(tile.n==='Wohngebiet'||tile.n==='Hochhäuser'){var nb2=[];if(r>0&&grid[r-1][c])nb2.push(TILES[grid[r-1][c].t]);if(c<COLS-1&&grid[r][c+1])nb2.push(TILES[grid[r][c+1].t]);if(r<ROWS-1&&grid[r+1][c])nb2.push(TILES[grid[r+1][c].t]);if(c>0&&grid[r][c-1])nb2.push(TILES[grid[r][c-1].t])
nb2.forEach(function(n){if(n.n==='Park'||n.n==='Spielplatz'){sat+=2;env+=1};if(n.n==='Fabrik'){sat-=4;env-=3};if(n.n.indexOf('Laden')>=0||n.n.indexOf('Supermarkt')>=0){sup+=2;sat+=1}})}
}
traf+=bad;reach-=iso*3
const cl=v=>Math.min(100,Math.max(0,Math.round(v)))
var cl=function(v){return Math.min(100,Math.max(0,Math.round(v)))}
scores={eco:cl(eco),sat:cl(sat),env:cl(env),traf:cl(traf),sup:cl(sup),reach:cl(reach)}
}
function nbT(r,c){const res=[];if(r>0)res.push(grid[r-1][c]);if(c<GW-1)res.push(grid[r][c+1]);if(r<GH-1)res.push(grid[r+1][c]);if(c>0)res.push(grid[r][c-1]);return res}
function place(r,c){
if(gameOver||!selType)return
if(grid[r][c]!=='empty'){addMsg('❌ Belegt!','warn');return}
if(budget<TILES[selType].cost){addMsg('❌ Kein Budget!','warn');return}
if(level.blockBad){
const conn=TILES[selType].conn,dirs=[[-1,0,0,2],[0,1,1,3],[1,0,2,0],[0,-1,3,1]]
for(const[dr,dc,myD,thD]of dirs){
if(!conn[myD])continue;const nr=r+dr,nc=c+dc
if(nr>=0&&nr<GH&&nc>=0&&nc<GW&&grid[nr][nc]!=='empty'&&!TILES[grid[nr][nc]].conn[thD]){addMsg('❌ Straße passt nicht!','warn');return}
}
}
grid[r][c]=selType;budget-=TILES[selType].cost;recalc();renderAll()
}
function addMsg(t,type){messages.unshift({text:t,type});if(messages.length>10)messages.pop()}
function nextRound(){
if(gameOver)return;history.push({round,...scores});round++;budget+=level.income
if(round>level.rounds){
gameOver=true;const total=Math.round(0.22*scores.eco+0.24*scores.sat+0.18*scores.env+0.16*scores.sup+0.12*scores.reach+0.08*(100-scores.traf))
document.getElementById('end-t').textContent=total>=level.win?'🎉 Geschafft!':'😔 Nicht bestanden'
document.getElementById('end-p').textContent=`Score: ${total}`;document.getElementById('ov-end').classList.remove('hidden');return
}
const ev=level.events.find(e=>e.r===round);if(ev)addMsg(ev.m,'');renderAll()
if(gameOver)return;round++;budget+=income;recalc()
if(round>maxRounds){gameOver=true
var total=Math.round(0.22*scores.eco+0.24*scores.sat+0.18*scores.env+0.16*scores.sup+0.12*scores.reach+0.08*(100-scores.traf))
document.getElementById('end-t').textContent=total>=50?'🎉 Geschafft!':'😔'
document.getElementById('end-p').textContent='Score: '+total
document.getElementById('ov-end').classList.remove('hidden')
return}
renderUI()
}
// === ISOMETRIC RENDERER ===
const $cv=document.getElementById('cv'),ctx=$cv.getContext('2d')
let W=0,H=0,TW=0,TH=0,mapOx=0,mapOy=0,animT=0
// === RENDER ===
var hoverR=-1,hoverC=-1
// Vehicles array
let cars=[]
function isoX(c,r){return(c-r)*(TW/2)}
function isoY(c,r){return(c+r)*(TH/2)}
function screenToGrid(sx,sy){
const rx=sx-mapOx,ry=sy-mapOy
const c=Math.floor((rx/(TW/2)+ry/(TH/2))/2)
const r=Math.floor((ry/(TH/2)-rx/(TW/2))/2)
return{r,c}
function render(){
ctx.fillStyle='#c8d0b8';ctx.fillRect(0,0,cv.width,cv.height)
for(var r=0;r<ROWS;r++)for(var c=0;c<COLS;c++){
var px=c*TS,py=r*TS,cell=grid[r][c]
if(cell){var conn=rotConn(TILES[cell.t].conn,cell.r);drawTile(px,py,conn,1);drawEmojis(px,py,TILES[cell.t])}
else{ctx.fillStyle=GREEN;ctx.fillRect(px,py,TS,TS)}
ctx.strokeStyle='rgba(0,0,0,0.06)';ctx.lineWidth=0.4;ctx.strokeRect(px,py,TS,TS)
if(r===hoverR&&c===hoverC&&sel>=0&&!cell){drawTile(px,py,TILES[sel].conn,0.3)}
}
requestAnimationFrame(render)
}
function resize(){
const rect=$cv.parentElement.getBoundingClientRect(),dpr=devicePixelRatio||1
$cv.width=rect.width*dpr;$cv.height=rect.height*dpr
ctx.setTransform(dpr,0,0,dpr,0,0)
W=rect.width;H=rect.height
const n=GW+GH
const fitW=(W*0.9)/(n/2),fitH=(H*0.85)/(n/2)
TW=Math.min(fitW,fitH*2);TH=TW/2
mapOx=(W-(GW+GH)*(TW/2)/2)/2+GH*(TW/2)/2+TW/4
mapOy=(H-(GW+GH)*(TH/2)/2)/2+TH
}
function updateCars(){
cars=cars.filter(v=>v.t<1)
if(cars.length<6&&Math.random()<0.03){
const tiles=[]
for(let r=0;r<GH;r++)for(let c=0;c<GW;c++)if(TILES[grid[r][c]]?.conn.some(v=>v))tiles.push({r,c})
if(tiles.length){
const spot=tiles[Math.floor(Math.random()*tiles.length)]
const conn=TILES[grid[spot.r][spot.c]].conn
const dirs=[];conn.forEach((v,i)=>{if(v)dirs.push(i)})
if(dirs.length){
const d=dirs[Math.floor(Math.random()*dirs.length)]
const emojis=['🚗','🚙','🚕','🚌']
cars.push({r:spot.r,c:spot.c,dir:d,t:0,e:emojis[Math.floor(Math.random()*emojis.length)]})
}
}
}
for(const v of cars)v.t+=0.006
}
function draw(){
animT+=0.01
updateCars()
// Sky/grass background
ctx.fillStyle='#92aa6a';ctx.fillRect(0,0,W,H)
// Draw tiles back-to-front
for(let r=0;r<GH;r++)for(let c=0;c<GW;c++){
const type=grid[r][c]
const sx=mapOx+isoX(c,r),sy=mapOy+isoY(c,r)
const isGhost=r===ghostR&&c===ghostC&&type==='empty'&&selType
drawIsoTile(sx,sy,type,1)
if(isGhost){
ctx.globalAlpha=0.4
drawIsoTile(sx,sy,selType,0.4)
ctx.globalAlpha=1
}
}
// Draw cars
const cfs=Math.max(6,TW*0.12)
ctx.font=`${cfs}px serif`;ctx.textAlign='center';ctx.textBaseline='middle'
for(const v of cars){
const sx=mapOx+isoX(v.c,v.r),sy=mapOy+isoY(v.c,v.r)
// Move along road direction in iso space
const off=v.t*TW*0.4
let dx=0,dy=0
if(v.dir===0){dx=off*0.5;dy=-off*0.25} // N
if(v.dir===1){dx=off*0.5;dy=off*0.25} // E
if(v.dir===2){dx=-off*0.5;dy=off*0.25} // S
if(v.dir===3){dx=-off*0.5;dy=-off*0.25}// W
// Slight offset to right side of road
const rOff=TW*0.04
if(v.dir===0||v.dir===2){dx+=rOff}else{dy+=rOff}
ctx.fillText(v.e,sx+dx,sy+dy)
}
requestAnimationFrame(draw)
}
function drawIsoTile(sx,sy,type,alpha){
const def=TILES[type]
if(!def)return
const hw=TW/2,hh=TH/2
const conn=def.conn
// === DIAMOND BASE (green grass) ===
ctx.fillStyle='#7da84e'
ctx.beginPath()
ctx.moveTo(sx,sy-hh) // top
ctx.lineTo(sx+hw,sy) // right
ctx.lineTo(sx,sy+hh) // bottom
ctx.lineTo(sx-hw,sy) // left
ctx.closePath()
ctx.fill()
// Subtle edge
ctx.strokeStyle='rgba(0,0,0,0.06)';ctx.lineWidth=0.5;ctx.stroke()
// === ROADS as iso parallelograms ===
const rw=TW*RW // road width in pixels
const rhw=rw/2
ctx.fillStyle='#3a3a3a'
// N: from center toward top-left edge
if(conn[0]) drawIsoRoad(sx,sy,hw,hh,rhw,0)
// E: from center toward top-right edge
if(conn[1]) drawIsoRoad(sx,sy,hw,hh,rhw,1)
// S: from center toward bottom-right edge
if(conn[2]) drawIsoRoad(sx,sy,hw,hh,rhw,2)
// W: from center toward bottom-left edge
if(conn[3]) drawIsoRoad(sx,sy,hw,hh,rhw,3)
// Center junction
if(conn.filter(v=>v).length>=2){
ctx.fillStyle='#3a3a3a'
const jr=rhw*0.7
ctx.beginPath();ctx.arc(sx,sy,jr,0,Math.PI*2);ctx.fill()
}
// Road markings (center dashed line + edge lines)
for(let d=0;d<4;d++){
if(!conn[d])continue
drawIsoRoadMarkings(sx,sy,hw,hh,rhw,d)
}
// === BUILDING CONTENT ===
if(def.content.length>0){
const fs=Math.max(8,TW*0.18)
ctx.font=`${fs}px serif`;ctx.textAlign='center';ctx.textBaseline='middle'
for(const item of def.content){
// Convert content position to iso coordinates
const scale=item.s||1
ctx.font=`${fs*scale}px serif`
// ox,oy are in tile-local coords (-0.5..0.5)
// Convert to iso: ox goes along E direction, oy goes along S direction
const px=sx + item.ox*TW*0.45 + item.oy*TW*0.05
const py=sy + item.ox*TH*0.15 + item.oy*TH*0.4
ctx.fillText(item.e,px,py)
}
ctx.textBaseline='alphabetic'
}
}
function drawIsoRoad(sx,sy,hw,hh,rhw,dir){
// Draw road segment from center to edge in direction dir
// In iso: N=top-left, E=top-right, S=bottom-right, W=bottom-left
const steps=6
ctx.beginPath()
for(let i=0;i<=steps;i++){
const t=i/steps*0.5 // 0..0.5 (center to edge)
let px,py
if(dir===0){px=sx-t*hw;py=sy-t*hh} // N (toward top-left)
if(dir===1){px=sx+t*hw;py=sy-t*hh} // E (toward top-right)
if(dir===2){px=sx+t*hw;py=sy+t*hh} // S (toward bottom-right)
if(dir===3){px=sx-t*hw;py=sy+t*hh} // W (toward bottom-left)
// Perpendicular offset for road width
let nx,ny // normal direction
if(dir===0||dir===2){nx=1/(hw/hh);ny=1} else{nx=-1/(hw/hh);ny=1}
const len=Math.sqrt(nx*nx+ny*ny);nx/=len;ny/=len
if(i===0){
ctx.moveTo(px+nx*rhw,py+ny*rhw*0.5)
}
ctx.lineTo(px+nx*rhw,py+ny*rhw*0.5)
}
// Go back on the other side
for(let i=steps;i>=0;i--){
const t=i/steps*0.5
let px,py
if(dir===0){px=sx-t*hw;py=sy-t*hh}
if(dir===1){px=sx+t*hw;py=sy-t*hh}
if(dir===2){px=sx+t*hw;py=sy+t*hh}
if(dir===3){px=sx-t*hw;py=sy+t*hh}
let nx,ny
if(dir===0||dir===2){nx=1/(hw/hh);ny=1}else{nx=-1/(hw/hh);ny=1}
const len=Math.sqrt(nx*nx+ny*ny);nx/=len;ny/=len
ctx.lineTo(px-nx*rhw,py-ny*rhw*0.5)
}
ctx.closePath()
ctx.fill()
}
function drawIsoRoadMarkings(sx,sy,hw,hh,rhw,dir){
// Center dashed line
ctx.strokeStyle='rgba(255,255,200,0.5)';ctx.lineWidth=Math.max(1.2,TW*0.015)
ctx.setLineDash([TW*0.06,TW*0.04])
ctx.beginPath()
let ex,ey
if(dir===0){ex=sx-hw*0.48;ey=sy-hh*0.48}
if(dir===1){ex=sx+hw*0.48;ey=sy-hh*0.48}
if(dir===2){ex=sx+hw*0.48;ey=sy+hh*0.48}
if(dir===3){ex=sx-hw*0.48;ey=sy+hh*0.48}
ctx.moveTo(sx,sy);ctx.lineTo(ex,ey);ctx.stroke()
ctx.setLineDash([])
// Edge lines (solid, thicker)
ctx.strokeStyle='rgba(255,255,255,0.3)';ctx.lineWidth=Math.max(0.8,TW*0.01)
let nx,ny
if(dir===0||dir===2){nx=1/(hw/hh);ny=1}else{nx=-1/(hw/hh);ny=1}
const len=Math.sqrt(nx*nx+ny*ny);nx/=len;ny/=len
for(const side of [1,-1]){
ctx.beginPath()
ctx.moveTo(sx+nx*rhw*side,sy+ny*rhw*0.5*side)
ctx.lineTo(ex+nx*rhw*side,ey+ny*rhw*0.5*side)
ctx.stroke()
}
}
// === UI ===
function renderAll(){
function renderUI(){
document.getElementById('h-bud').textContent=budget
document.getElementById('h-rnd').textContent=`${round}/${level.rounds}`
if(selType&&budget<TILES[selType].cost)selType=null
const ps=[{n:'Wirtschaft',i:'💰',v:scores.eco},{n:'Zufriedenheit',i:'😊',v:scores.sat},{n:'Umwelt',i:'🌿',v:scores.env},{n:'Verkehr',i:'🚗',v:scores.traf,inv:1},{n:'Versorgung',i:'🛒',v:scores.sup},{n:'Erreichbarkeit',i:'🔗',v:scores.reach}]
document.getElementById('params').innerHTML=ps.map(p=>{const g=p.inv?p.v<40:p.v>55,b=p.inv?p.v>65:p.v<30,col=b?'var(--coral)':g?'var(--moss)':'var(--sand)';return`<div class="param"><div class="param-i">${p.i}</div><div class="param-inf"><div class="param-n">${p.n}</div><div class="param-bar"><div class="param-bar-f" style="width:${p.v}%;background:${col}"></div></div></div><div class="param-v" style="color:${col}">${p.v}</div></div>`}).join('')
document.getElementById('tiles').innerHTML=level.unlocked.map(id=>{const d=TILES[id],af=budget>=d.cost;return`<div class="tb ${selType===id?'on':''} ${!af?'off':''}" data-t="${id}"><span class="te">${d.content[0]?.e||'🛣️'}</span><span class="tn">${d.n}</span><span class="tc">💰${d.cost}</span></div>`}).join('')
document.querySelectorAll('.tb').forEach(b=>b.addEventListener('click',()=>{selType=selType===b.dataset.t?null:b.dataset.t;renderAll()}))
document.getElementById('msgs').innerHTML=messages.slice(0,7).map(m=>`<div class="msg ${m.type}">${m.text}</div>`).join('')
drawGraph()
document.getElementById('h-rnd').textContent=round+'/'+maxRounds
document.getElementById('h-inc').textContent='+'+income+'/Runde'
if(sel>=0&&budget<TILES[sel].cost)sel=-1
// Params
var ps=[{n:'Wirtschaft',i:'💰',v:scores.eco},{n:'Zufriedenheit',i:'😊',v:scores.sat},{n:'Umwelt',i:'🌿',v:scores.env},{n:'Verkehr',i:'🚗',v:scores.traf,inv:1},{n:'Versorgung',i:'🛒',v:scores.sup},{n:'Erreichbarkeit',i:'🔗',v:scores.reach}]
document.getElementById('params').innerHTML=ps.map(function(p){var g=p.inv?p.v<40:p.v>55,b=p.inv?p.v>65:p.v<30,col=b?'#c07a6b':g?'#5a8a5e':'#c4a35a';return'<div class="param"><div class="param-i">'+p.i+'</div><div class="param-inf"><div class="param-n">'+p.n+'</div><div class="param-bar"><div class="param-bar-f" style="width:'+p.v+'%;background:'+col+'"></div></div></div><div class="param-v" style="color:'+col+'">'+p.v+'</div></div>'}).join('')
document.getElementById('msgs').innerHTML=messages.slice(-5).reverse().map(function(m){return'<div class="msg '+m.type+'">'+m.text+'</div>'}).join('')
// Tools
;[['road-tools',0],['building-tools',1],['nature-tools',2]].forEach(function(pair){
var html='';TILES.forEach(function(t,i){if(t.cat!==pair[1])return;var af=budget>=t.cost
html+='<div class="tb'+(sel===i?' on':'')+(af?'':' off')+'" data-t="'+i+'"><span class="te">'+(t.emoji||'🛣️')+'</span>'+t.n+'<span class="tc">💰'+t.cost+'</span></div>'})
document.getElementById(pair[0]).innerHTML=html})
document.querySelectorAll('.tb').forEach(function(b){b.addEventListener('click',function(){var i=parseInt(this.dataset.t);sel=sel===i?-1:i;renderUI()})})
}
function drawGraph(){const gc=document.getElementById('graph');if(!gc||history.length<1)return;const gx=gc.getContext('2d'),rect=gc.parentElement.getBoundingClientRect(),dpr=devicePixelRatio||1;gc.width=rect.width*dpr;gc.height=65*dpr;gx.setTransform(dpr,0,0,dpr,0,0);const gW=rect.width,gH=65;gx.clearRect(0,0,gW,gH);const lines=[{k:'eco',c:'#c4a35a'},{k:'sat',c:'#5a8a5e'},{k:'env',c:'#4a8a4a'},{k:'sup',c:'#4a7c8a'},{k:'reach',c:'#8a6a8a'}];for(const l of lines){gx.strokeStyle=l.c;gx.lineWidth=1;gx.beginPath();history.forEach((h,i)=>{const x=2+(i/Math.max(1,level.rounds-1))*(gW-4),y=2+(gH-4)-(h[l.k]/100)*(gH-4);i===0?gx.moveTo(x,y):gx.lineTo(x,y)});gx.stroke()}}
// === INPUT ===
$cv.addEventListener('mousemove',e=>{const r=$cv.getBoundingClientRect();const g=screenToGrid(e.clientX-r.left,e.clientY-r.top);ghostR=g.r;ghostC=g.c;if(ghostR<0||ghostR>=GH||ghostC<0||ghostC>=GW){ghostR=-1;ghostC=-1}})
$cv.addEventListener('click',e=>{
if(gameOver||!selType)return;const r=$cv.getBoundingClientRect()
const g=screenToGrid(e.clientX-r.left,e.clientY-r.top)
if(g.r>=0&&g.r<GH&&g.c>=0&&g.c<GW)place(g.r,g.c)
cv.addEventListener('click',function(e){
if(gameOver)return;var rect=cv.getBoundingClientRect(),mx=(e.clientX-rect.left)*(cv.width/rect.width),my=(e.clientY-rect.top)*(cv.height/rect.height)
var gc=Math.floor(mx/TS),gr=Math.floor(my/TS);if(gr<0||gr>=ROWS||gc<0||gc>=COLS)return
var cell=grid[gr][gc]
if(cell){var rx=mx-gc*TS,ry=my-gr*TS;if(ry<TS*0.35){if(rx<TS*0.4){cell.r=(cell.r+3)%4;return};if(rx>TS*0.6){cell.r=(cell.r+1)%4;return}};return}
if(sel>=0&&budget>=TILES[sel].cost){grid[gr][gc]={t:sel,r:0};budget-=TILES[sel].cost;recalc();renderUI()}
})
$cv.addEventListener('mouseleave',()=>{ghostR=-1;ghostC=-1})
document.addEventListener('keydown',e=>{if(selType){selType=null;renderAll()}})
cv.addEventListener('mousemove',function(e){var rect=cv.getBoundingClientRect();hoverC=Math.floor((e.clientX-rect.left)*(cv.width/rect.width)/TS);hoverR=Math.floor((e.clientY-rect.top)*(cv.height/rect.height)/TS);if(hoverR<0||hoverR>=ROWS||hoverC<0||hoverC>=COLS){hoverR=-1;hoverC=-1}})
cv.addEventListener('mouseleave',function(){hoverR=-1;hoverC=-1})
document.addEventListener('keydown',function(e){if(e.key==='Escape'){sel=-1;renderUI()}})
document.getElementById('btn-next').addEventListener('click',nextRound)
document.getElementById('btn-go').addEventListener('click',()=>{document.getElementById('ov-intro').classList.add('hidden');renderAll()})
document.getElementById('btn-again').addEventListener('click',()=>{document.getElementById('ov-end').classList.add('hidden');initGame(LEVELS[0]);renderAll()})
document.getElementById('btn-reset').addEventListener('click',()=>{if(confirm('Neu?')){initGame(LEVELS[0]);renderAll()}})
document.getElementById('btn-go').addEventListener('click',function(){document.getElementById('ov-intro').classList.add('hidden');renderUI()})
document.getElementById('btn-again').addEventListener('click',function(){document.getElementById('ov-end').classList.add('hidden');location.reload()})
initGame(LEVELS[0]);resize();window.addEventListener('resize',resize);renderAll();draw()
recalc();renderUI();render()
</script>
</body>
</html>
+290
View File
@@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Lehrkraft-Dashboard</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;color:#1a1a1a;min-height:100vh}
.topbar{background:#fff;padding:.6rem 1.2rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:10}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1}
.topbar .user{font-size:.72rem;color:#8a8a8a}
.topbar .btn-sm{padding:.3rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.68rem;font-weight:600;cursor:pointer;color:#4a4a4a}
.topbar .btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1000px;margin:0 auto;padding:1rem}
.grid{display:grid;grid-template-columns:280px 1fr;gap:1rem;min-height:calc(100vh - 60px)}
.sidebar{display:flex;flex-direction:column;gap:.6rem}
.main{display:flex;flex-direction:column;gap:.8rem}
.card{background:#fff;border-radius:10px;padding:.8rem;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04)}
.card-h{font-size:.6rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem}
.class-item{padding:.5rem;border:1.5px solid rgba(0,0,0,.05);border-radius:7px;cursor:pointer;margin-bottom:.3rem;transition:all .1s}
.class-item:hover{border-color:#4a7c8a;background:#f0f6f8}
.class-item.active{border-color:#4a7c8a;background:#dae8ec}
.class-item h4{font-size:.78rem;font-weight:700}
.class-item p{font-size:.62rem;color:#8a8a8a}
.btn{padding:.4rem .8rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer}
.btn:hover{background:#3a6470}
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
.btn-danger{background:#c07a6b}
table{width:100%;border-collapse:collapse;font-size:.75rem}
th{background:#f7f6f3;padding:.4rem .5rem;text-align:left;font-weight:600;font-size:.65rem;color:#8a8a8a}
td{padding:.4rem .5rem;border-bottom:1px solid rgba(0,0,0,.03)}
.module-row{display:flex;align-items:center;gap:.5rem;padding:.4rem .2rem;border-bottom:1px solid rgba(0,0,0,.03)}
.module-row:last-child{border:none}
.module-name{flex:1;font-size:.75rem;font-weight:600}
.module-desc{font-size:.6rem;color:#8a8a8a}
.mode-btn{padding:.2rem .5rem;border:1px solid rgba(0,0,0,.08);border-radius:4px;font-size:.6rem;font-weight:600;cursor:pointer;background:#fff}
.mode-btn.active{background:#4a7c8a;color:#fff;border-color:#4a7c8a}
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
.modal-card{background:#fff;border-radius:12px;padding:1.2rem;max-width:380px;width:90%}
.modal-card h3{font-size:.9rem;font-weight:700;margin-bottom:.5rem}
.modal-card label{display:block;font-size:.68rem;font-weight:600;margin-top:.5rem;color:#4a4a4a}
.modal-card input{width:100%;padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;margin-top:.15rem}
.hidden{display:none}
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem;margin-bottom:.5rem}
.stat{text-align:center;background:#f7f6f3;border-radius:6px;padding:.4rem}
.stat .n{font-size:1.1rem;font-weight:800;color:#4a7c8a}
.stat .l{font-size:.55rem;color:#8a8a8a}
@media(max-width:700px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="topbar">
<h1>🌍 GeoGraSim — Lehrkraft</h1>
<span class="user" id="user-info"></span>
<button class="btn-sm" onclick="logout()">Abmelden</button>
<a href="index.html" class="btn-sm">← App</a>
</div>
<div class="wrap">
<div class="grid">
<!-- SIDEBAR: Klassen -->
<div class="sidebar">
<div class="card">
<div class="card-h">Meine Klassen</div>
<div id="class-list"></div>
<button class="btn" style="width:100%;margin-top:.4rem" onclick="showCreateClass()">+ Neue Klasse</button>
</div>
<div class="card">
<div class="card-h">Handbuch</div>
<a href="handbuch.html" style="font-size:.72rem;color:#4a7c8a">📖 Didaktisches Handbuch öffnen</a>
</div>
</div>
<!-- MAIN -->
<div class="main" id="main-content">
<div class="card" style="text-align:center;padding:2rem;color:#8a8a8a">
<p>Wähle links eine Klasse aus oder erstelle eine neue.</p>
</div>
</div>
</div>
</div>
<!-- Modal: Klasse erstellen -->
<div class="modal hidden" id="modal-class">
<div class="modal-card">
<h3 id="modal-class-title">Neue Klasse</h3>
<label>Klassenname</label>
<input type="text" id="mc-name" placeholder="z.B. 1A">
<label>Schuljahr</label>
<input type="text" id="mc-year" placeholder="z.B. 2025/26">
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="saveClass()">Speichern</button>
<button class="btn btn-outline" onclick="hideModal('modal-class')">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal: Schueler erstellen -->
<div class="modal hidden" id="modal-student">
<div class="modal-card">
<h3>Schüler:in erstellen</h3>
<label>Benutzername</label>
<input type="text" id="ms-user" placeholder="z.B. max01">
<label>Anzeigename (optional)</label>
<input type="text" id="ms-name" placeholder="z.B. Max M.">
<label>Passwort</label>
<input type="text" id="ms-pass" placeholder="mind. 4 Zeichen">
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="saveStudent()">Erstellen</button>
<button class="btn btn-outline" onclick="hideModal('modal-student')">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal: Batch-Schueler -->
<div class="modal hidden" id="modal-batch">
<div class="modal-card">
<h3>Mehrere Schüler:innen erstellen</h3>
<label>Prefix</label>
<input type="text" id="mb-prefix" value="schueler" placeholder="z.B. schueler">
<label>Anzahl</label>
<input type="number" id="mb-count" value="25" min="1" max="50">
<p style="font-size:.65rem;color:#8a8a8a;margin-top:.3rem">Erstellt z.B. schueler01, schueler02, ... mit zufälligen 4-stelligen Passwörtern.</p>
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="batchCreate()">Erstellen</button>
<button class="btn btn-outline" onclick="hideModal('modal-batch')">Abbrechen</button>
</div>
<div id="batch-result" style="margin-top:.5rem;font-size:.65rem;max-height:200px;overflow-y:auto"></div>
</div>
</div>
<script>
var BASE = (window.__GGS__?.baseUrl) || ''
var currentClassId = null
async function api(endpoint, data) {
var opts = data ? {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(data)} : {credentials:'same-origin'}
var r = await fetch(BASE + '/api/' + endpoint, opts)
return await r.json()
}
// Auth check
api('auth', {action:'status'}).then(function(r) {
if (r.role !== 'teacher') { location.href = 'login.html'; return }
document.getElementById('user-info').textContent = r.user?.display_name || 'Lehrkraft'
loadClasses()
})
function logout() { api('auth', {action:'logout'}).then(function() { location.href = 'login.html' }) }
// === KLASSEN ===
async function loadClasses() {
var classes = await api('classes')
if (classes.error) return
var html = ''
classes.forEach(function(c) {
html += '<div class="class-item'+(c.id===currentClassId?' active':'')+'" onclick="selectClass('+c.id+')">'
html += '<h4>'+c.name+'</h4>'
html += '<p>'+c.school_year+' · Code: <strong>'+c.join_code+'</strong> · '+c.student_count+' Schüler</p>'
html += '</div>'
})
document.getElementById('class-list').innerHTML = html || '<p style="font-size:.7rem;color:#aaa">Noch keine Klassen.</p>'
}
function selectClass(id) {
currentClassId = id
loadClasses()
loadClassDetail(id)
}
async function loadClassDetail(id) {
var students = await api('students?class_id=' + id)
var modules = await api('modules?class_id=' + id)
if (students.error) students = []
if (modules.error) modules = []
var html = ''
// Stats
html += '<div class="card"><div class="stat-grid">'
html += '<div class="stat"><div class="n">'+students.length+'</div><div class="l">Schüler:innen</div></div>'
var avgScore = 0; var completions = 0
students.forEach(function(s) { if(s.total_score?.avg_score){avgScore+=parseFloat(s.total_score.avg_score);completions+=parseInt(s.total_score.count)} })
if(students.length>0) avgScore = Math.round(avgScore/students.length)
html += '<div class="stat"><div class="n">'+avgScore+'</div><div class="l">Ø Score</div></div>'
html += '<div class="stat"><div class="n">'+completions+'</div><div class="l">Abschlüsse</div></div>'
html += '</div></div>'
// Modulfreigabe
html += '<div class="card"><div class="card-h">Module</div>'
modules.forEach(function(m) {
html += '<div class="module-row">'
html += '<div><div class="module-name">'+m.name+'</div><div class="module-desc">'+m.desc+'</div></div>'
html += '<button class="mode-btn'+(m.mode==='locked'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'locked\')">🔒</button>'
html += '<button class="mode-btn'+(m.mode==='free'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'free\')">🆓</button>'
html += '<button class="mode-btn'+(m.mode==='teacher_started'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'teacher_started\')">▶ Start</button>'
html += '</div>'
})
html += '</div>'
// Schuelerliste
html += '<div class="card"><div class="card-h">Schüler:innen</div>'
html += '<div style="display:flex;gap:.3rem;margin-bottom:.5rem">'
html += '<button class="btn" onclick="showModal(\'modal-student\')">+ Einzeln</button>'
html += '<button class="btn btn-outline" onclick="showModal(\'modal-batch\')">+ Mehrere</button>'
html += '</div>'
if (students.length > 0) {
html += '<table><tr><th>Benutzer</th><th>Name</th><th>Letzter Login</th><th>Ø Score</th><th></th></tr>'
students.forEach(function(s) {
var avg = s.total_score?.avg_score ? Math.round(parseFloat(s.total_score.avg_score)) : '—'
var last = s.last_login ? new Date(s.last_login).toLocaleDateString('de-AT') : '—'
html += '<tr><td>'+s.username+'</td><td>'+(s.display_name||'—')+'</td><td>'+last+'</td><td>'+avg+'</td>'
html += '<td><button class="btn-sm" onclick="deleteStudent('+s.id+')">🗑</button></td></tr>'
})
html += '</table>'
} else {
html += '<p style="font-size:.7rem;color:#aaa">Noch keine Schüler:innen in dieser Klasse.</p>'
}
html += '</div>'
document.getElementById('main-content').innerHTML = html
}
// === MODALS ===
function showModal(id) { document.getElementById(id).classList.remove('hidden') }
function hideModal(id) { document.getElementById(id).classList.add('hidden') }
function showCreateClass() { document.getElementById('mc-name').value=''; document.getElementById('mc-year').value=''; showModal('modal-class') }
async function saveClass() {
var name = document.getElementById('mc-name').value.trim()
var year = document.getElementById('mc-year').value.trim()
if (!name) return
await api('classes', {action:'create', name:name, schoolYear:year})
hideModal('modal-class')
loadClasses()
}
async function saveStudent() {
var user = document.getElementById('ms-user').value.trim()
var name = document.getElementById('ms-name').value.trim()
var pass = document.getElementById('ms-pass').value
if (!user || !pass) return
var r = await api('students', {action:'create', classId:currentClassId, username:user, displayName:name, password:pass})
if (r.error) { alert(r.error); return }
hideModal('modal-student')
loadClassDetail(currentClassId)
}
async function batchCreate() {
var prefix = document.getElementById('mb-prefix').value.trim() || 'schueler'
var count = parseInt(document.getElementById('mb-count').value) || 25
var r = await api('students', {action:'create_batch', classId:currentClassId, prefix:prefix, count:count})
if (r.error) { alert(r.error); return }
// Zugangsdaten anzeigen
var html = '<strong>Erstellt! Zugangsdaten (bitte notieren):</strong><br>'
html += '<table style="font-size:.6rem"><tr><th>Benutzer</th><th>Passwort</th></tr>'
r.created.forEach(function(s) { html += '<tr><td>'+s.username+'</td><td><code>'+s.password+'</code></td></tr>' })
html += '</table>'
document.getElementById('batch-result').innerHTML = html
loadClassDetail(currentClassId)
}
async function deleteStudent(id) {
if (!confirm('Schüler:in wirklich löschen?')) return
await api('students', {action:'delete', studentId:id})
loadClassDetail(currentClassId)
}
async function setMode(classId, moduleId, mode) {
await api('modules', {action:'set_mode', classId:classId, moduleId:moduleId, mode:mode})
loadClassDetail(classId)
}
</script>
</body>
</html>
+1 -1
View File
@@ -402,7 +402,7 @@ var emojiTiles = [
{e:'🏠',x:-0.32,y:-0.3,s:32},{e:'🏡',x:-0.3,y:0,s:30},{e:'🏠',x:-0.32,y:0.28,s:28},
{e:'🏘️',x:0.32,y:-0.25,s:30},{e:'🏠',x:0.3,y:0.05,s:32},{e:'🏡',x:0.32,y:0.3,s:28}]},
{id:'e04', label:'Fabrik', stub:'S', emoji:'🏭', size:85},
{id:'e05', label:'Spital', stub:'EW', emoji:'🏥', size:75, extra:[{e:'🚑',x:0,y:0.3,s:32}]},
{id:'e05', label:'Spital', stub:'EW', emoji:'🏥', size:65, yoff:-0.12, extra:[{e:'🚑',x:0,y:0.3,s:30}]},
{id:'e06', label:'Feuerwehr', stub:'S', bg:'rgba(200,50,40,0.12)', emojis:[
{e:'🏠',x:0,y:-0.15,s:60},{e:'🚒',x:-0.2,y:0.22,s:40},{e:'🚒',x:0.2,y:0.22,s:40}]},
{id:'e07', label:'Polizei', stub:'N', emoji:'🚔', size:45, extra:[{e:'🏢',x:0,y:0.15,s:55}]},