|
|
|
@@ -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>
|
|
|
|
|