Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0555a2d245 | |||
| 5f603cb193 | |||
| 913f161502 |
@@ -0,0 +1,73 @@
|
||||
// Headless-Balance-Messung (temporär). node sim-harness.mjs
|
||||
import { createState, place, upgrade, callNextWave, tick, GOALS } from '../../App/sims/tourismusregion/js/model.js';
|
||||
import { BUILDINGS, GAME_LEN } from '../../App/sims/tourismusregion/js/data.js';
|
||||
|
||||
const ALL_TYPES = Object.keys(BUILDINGS);
|
||||
function upgradeAllMax(s) {
|
||||
for (const b of s.buildings) {
|
||||
const def = BUILDINGS[b.type];
|
||||
let guard = 0;
|
||||
while (b.level < def.cost.length && s.budget >= def.cost[b.level] && guard++ < 6) {
|
||||
const r = upgrade(s, b);
|
||||
if (!r || !r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run(strategy, mapId = 'talschleife') {
|
||||
const s = createState({ mapId, seed: 12345 });
|
||||
const dt = 0.5;
|
||||
let t = 0;
|
||||
|
||||
function tryBuildAll() {
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
const slot = s.slots[i];
|
||||
if (slot.building || slot.blocked) continue;
|
||||
for (const type of ALL_TYPES) {
|
||||
const def = BUILDINGS[type];
|
||||
if (!def || s.budget < def.cost[0]) continue;
|
||||
const r = place(s, type, i);
|
||||
if (r && r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function buildOneCafe() {
|
||||
// Minimal-Strategie: genau EIN Café (Thomas' „5-Sterne-Café macht alles grün")
|
||||
if (s.buildings.length) return;
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
if (s.slots[i].building || s.slots[i].blocked) continue;
|
||||
const r = place(s, 'cafe', i);
|
||||
if (r && r.ok) return;
|
||||
}
|
||||
}
|
||||
|
||||
let guard = 0;
|
||||
while (t < GAME_LEN && s.phase < 8 && guard++ < 2000) {
|
||||
if (strategy === 'buildAll' || strategy === 'maxedAll') tryBuildAll();
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
if (strategy === 'oneCafe') buildOneCafe();
|
||||
try { callNextWave(s); } catch (_) {}
|
||||
for (let k = 0; k < 40 && t < GAME_LEN; k++) { tick(s, dt); t += dt; }
|
||||
}
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
// Restzeit auslaufen lassen
|
||||
while (t < GAME_LEN && guard++ < 6000) { tick(s, dt); t += dt; }
|
||||
|
||||
const stars = s.stats.all.count ? s.stats.all.stars / s.stats.all.count : 0;
|
||||
return {
|
||||
strategy,
|
||||
env: Math.round(s.env),
|
||||
avgStars: +stars.toFixed(2),
|
||||
visitorsExited: s.stats.all.count,
|
||||
budget: Math.round(s.budget),
|
||||
buildings: s.buildings.length,
|
||||
traffic: Math.round(s.traffic || 0),
|
||||
goalsMet: GOALS.filter(g => g.check(s)).length + '/' + GOALS.length,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('=== Tourismusregion Balance-Messung (talschleife) ===');
|
||||
for (const strat of ['maxedAll', 'buildAll', 'oneCafe']) {
|
||||
const r = run(strat);
|
||||
console.log(`${strat.padEnd(9)} → env ${String(r.env).padStart(3)} · Ø${r.avgStars}★ · ${r.buildings} Geb. · Verkehr ${r.traffic} · Ziele ${r.goalsMet} · Budget ${r.budget}`);
|
||||
}
|
||||
@@ -44,6 +44,7 @@
|
||||
<a href="admin-levels.html" class="btn-sm">🎮 Level</a>
|
||||
<a href="admin-styleguide.html" class="btn-sm">🎨 Styleguide</a>
|
||||
<a href="admin-accounts.html" class="btn-sm">👤 Admins</a>
|
||||
<a href="admin-weltkueche.html" class="btn-sm">🍽️ Weltküche-Herkunft</a>
|
||||
<a href="admin.html" class="btn-sm">Logout</a>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +56,7 @@
|
||||
<a class="btn btn-outline" href="sims/tourismustal/editor.php">🏠 Tourismustal-Sprite-Editor</a>
|
||||
<a class="btn btn-outline" href="stadt-editor">🏙️ Stadt-Raumplanung-Editor</a>
|
||||
<a class="btn btn-outline" href="foto-upload">📷 Schulbuch-Bildupload</a>
|
||||
<a class="btn btn-outline" href="admin-weltkueche.html">🍽️ Weltküche-Herkunftsübersicht (QA)</a>
|
||||
</div>
|
||||
<div style="font-size:.66rem;color:#8a8a8a;margin-top:.55rem">Alle nur mit Admin-Login erreichbar. Schulbuch-Upload zusätzlich per PIN (0815). Heli-Audio-Werkzeuge liegen nur lokal (<code>.LocalDeveloperTools</code>) und sind nicht auf dem Server.</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GeoGraSim — Weltküche-Herkunftsübersicht (Admin)</title>
|
||||
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96"><link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="assets/fonts/inter.css">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Inter',sans-serif;background:#f7f6f3;min-height:100vh;color:#2a2a2a}
|
||||
.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:20}
|
||||
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem}
|
||||
.topbar h1 img{height:28px}
|
||||
.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;text-decoration:none}
|
||||
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
|
||||
.wrap{max-width:1100px;margin:1rem auto;padding:0 1rem}
|
||||
.lead{font-size:.82rem;color:#6a6a6a;margin-bottom:.8rem;line-height:1.5}
|
||||
.lead b{color:#3a3a3a}
|
||||
.filters{display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
|
||||
.filters input{padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.12);border-radius:7px;font-size:.8rem;font-family:inherit;min-width:220px}
|
||||
.chip{padding:.3rem .7rem;border-radius:99px;border:1.5px solid rgba(0,0,0,.12);background:#fff;font-size:.72rem;font-weight:600;cursor:pointer;color:#4a4a4a}
|
||||
.chip.on{background:#4a7c8a;color:#fff;border-color:#4a7c8a}
|
||||
.dish{background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.04);border:1px solid rgba(0,0,0,.05);margin-bottom:1rem;overflow:hidden}
|
||||
.dish-h{padding:.7rem 1rem;background:#f0f5f6;border-bottom:1px solid rgba(0,0,0,.05);display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
|
||||
.dish-h .nm{font-size:1rem;font-weight:800;color:#2a4a52}
|
||||
.dish-h .heimat{font-size:.72rem;color:#6a6a6a}
|
||||
.dish-h .cat{font-size:.6rem;text-transform:uppercase;letter-spacing:.05em;background:#e8e4d8;color:#7a6540;padding:.1rem .5rem;border-radius:99px;font-weight:700}
|
||||
table{width:100%;border-collapse:collapse;font-size:.78rem}
|
||||
th{text-align:left;padding:.45rem .7rem;font-size:.62rem;text-transform:uppercase;letter-spacing:.04em;color:#8a8a8a;background:#fafaf7;border-bottom:1px solid rgba(0,0,0,.05)}
|
||||
td{padding:.5rem .7rem;border-bottom:1px solid rgba(0,0,0,.04);vertical-align:top}
|
||||
tr:last-child td{border-bottom:none}
|
||||
.ing-nm{font-weight:700;color:#3a3a3a;white-space:nowrap}
|
||||
.ing-nm small{display:block;color:#9a9a9a;font-weight:400;font-size:.66rem}
|
||||
.cty{display:inline-flex;align-items:center;gap:.25rem;padding:.12rem .45rem;border-radius:99px;font-size:.72rem;font-weight:600;margin:.1rem .2rem .1rem 0;white-space:nowrap}
|
||||
.cty.ok{background:#dceadd;color:#2f6b3a;border:1px solid #b6d8bd}
|
||||
.cty.dist{background:#f6e2dc;color:#a3472f;border:1px solid #e6c3b4}
|
||||
.cty.other{background:#eee;color:#7a7a7a;border:1px solid #ddd}
|
||||
.cty .g{opacity:.6;font-weight:400;font-size:.64rem}
|
||||
.warn{background:#fff5e6;border-left:3px solid #e0a020;padding:.35rem .6rem;border-radius:5px;font-size:.72rem;color:#8a6510;margin-top:.35rem}
|
||||
.colhead{font-size:.6rem;color:#8a8a8a;text-transform:uppercase;letter-spacing:.04em;margin-bottom:.2rem}
|
||||
.legend{font-size:.72rem;color:#6a6a6a;margin:.5rem 0 1rem;display:flex;gap:1rem;flex-wrap:wrap}
|
||||
#status{padding:2rem;text-align:center;color:#8a8a8a}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> Weltküche — Herkunftsübersicht (Admin)</h1>
|
||||
<a href="admin-licenses.html" class="btn-sm">← Admin</a>
|
||||
<a href="admin.html" class="btn-sm">Logout</a>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<p class="lead">QA-Werkzeug: spiegelt die <b>Spiel-Logik</b> (Heimat + Handels-Gravitation Menge÷Entfernung, Markt-Tags). Pro Gericht & Zutat siehst du, welche Länder das Spiel als <b>richtig</b> akzeptiert und welche als <b>Distraktoren</b> im Topf sind. Ziel: kein „Warum Frankreich?", keine „Bananen aus Deutschland".</p>
|
||||
<div class="legend">
|
||||
<span><span class="cty ok">🟢 richtig</span> akzeptierte Herkunft</span>
|
||||
<span><span class="cty dist">🔴 Distraktor</span> falsch (fern/klimafremd/kein Lieferant)</span>
|
||||
<span><span class="cty other">⚪ nicht angeboten</span> Produzent, aber nicht im Spiel</span>
|
||||
<span><b>g</b> = Relevanz (Menge÷Entfernung zur Heimat)</span>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<input id="q" placeholder="Suche Gericht oder Zutat…" oninput="render()">
|
||||
<span class="chip on" data-cat="" onclick="setCat(this,'')">Alle</span>
|
||||
<span class="chip" data-cat="regional-at" onclick="setCat(this,'regional-at')">AT-regional</span>
|
||||
<span class="chip" data-cat="dach" onclick="setCat(this,'dach')">DACH</span>
|
||||
<span class="chip" data-cat="europa" onclick="setCat(this,'europa')">Europa</span>
|
||||
<span class="chip" data-cat="exotisch" onclick="setCat(this,'exotisch')">Exotisch</span>
|
||||
<label style="font-size:.72rem;color:#6a6a6a;margin-left:.5rem"><input type="checkbox" id="onlyWarn" onchange="render()"> nur Auffällige ⚠</label>
|
||||
</div>
|
||||
<div id="status">Lade Daten…</div>
|
||||
<div id="list"></div>
|
||||
</div>
|
||||
<script>
|
||||
var BASE = location.pathname.replace(/\/admin-weltkueche\.html$/, '');
|
||||
var D = 'sims/weltkueche/assets/data/';
|
||||
var C = {}, PM = {}, DISHES = [];
|
||||
var _cat = '';
|
||||
|
||||
// ---- Spiel-Logik gespiegelt (siehe game.html buildTaskFor) ----
|
||||
function mrank(m){ return m === 'eu' ? 0 : (m === 'world-far' || m === 'local') ? 2 : 1; }
|
||||
function wkDist(a, b){
|
||||
var A = C[a], B = C[b];
|
||||
if (!A || !B || A.lat == null || B.lat == null) return 3000;
|
||||
var R = 6371, toR = Math.PI/180;
|
||||
var dLat = (B.lat-A.lat)*toR, dLng = (B.lng-A.lng)*toR;
|
||||
var s = Math.pow(Math.sin(dLat/2),2) + Math.cos(A.lat*toR)*Math.cos(B.lat*toR)*Math.pow(Math.sin(dLng/2),2);
|
||||
return Math.max(60, 2*R*Math.asin(Math.min(1, Math.sqrt(s))));
|
||||
}
|
||||
// Klassifiziert alle Produzenten einer Zutat für ein Gericht.
|
||||
// Gibt { correct:[keys], distractors:[keys], branch } zurück (Pools, ohne Zufalls-Ziehung).
|
||||
function classify(ingKey, heimat){
|
||||
var ing = PM[ingKey]; if (!ing) return null;
|
||||
var producers = ing.producers || {};
|
||||
var keys = Object.keys(producers).filter(function(c){ return C[c]; });
|
||||
// Heimat-Zweig: Heimat erzeugt die Zutat selbst
|
||||
if (heimat && C[heimat] && producers[heimat]) {
|
||||
var grav = function(c){ return (producers[c].t || 0) / wkDist(heimat, c); };
|
||||
var realistic = keys.filter(function(c){ return c !== heimat && mrank(producers[c].market) < 2; })
|
||||
.sort(function(a,b){ return (mrank(producers[a].market)-mrank(producers[b].market)) || (grav(b)-grav(a)); });
|
||||
var correct = [heimat]; if (realistic[0]) correct.push(realistic[0]);
|
||||
if (correct.length < 2){ var any = keys.filter(function(c){ return c!==heimat && correct.indexOf(c)<0; })
|
||||
.sort(function(a,b){ return (producers[b].t||0)-(producers[a].t||0); }); if(any[0]) correct.push(any[0]); }
|
||||
var farP = keys.filter(function(c){ return correct.indexOf(c)<0 && mrank(producers[c].market)===2; });
|
||||
var climate = (ing.climate_possible||[]).filter(function(c){ return C[c] && correct.indexOf(c)<0; });
|
||||
return { correct: correct, distractors: farP.concat(climate), branch:'heimat', grav: grav };
|
||||
}
|
||||
// Normal-Zweig (exotisch / Heimat kein Produzent)
|
||||
var TIER = { eu:0, global:1, 'world-far':2 };
|
||||
var pool = keys.filter(function(c){ var m = producers[c].market || 'global'; return m==='eu'||m==='global'||m==='world-far'; })
|
||||
.sort(function(a,b){ var ma=producers[a].market||'global', mb=producers[b].market||'global';
|
||||
return ((TIER[ma]!=null?TIER[ma]:2)-(TIER[mb]!=null?TIER[mb]:2)) || ((producers[b].t||0)-(producers[a].t||0)); });
|
||||
var correct2 = pool.slice(0, Math.min(2, pool.length));
|
||||
var climate2 = (ing.climate_possible||[]).filter(function(c){ return C[c] && correct2.indexOf(c)<0; });
|
||||
var localFar = keys.filter(function(c){ var m=producers[c].market; return (m==='local'||m==='world-far') && correct2.indexOf(c)<0; });
|
||||
return { correct: correct2, distractors: localFar.concat(climate2), branch:'welt', grav: function(c){ return producers[c].t||0; } };
|
||||
}
|
||||
|
||||
function fmtT(t){ if(!t) return ''; if(t>=1e6) return (t/1e6).toFixed(t>=1e7?0:1)+' Mio t'; if(t>=1e3) return Math.round(t/1e3)+' Tsd t'; return t+' t'; }
|
||||
function ctyChip(key, cls, ing, heimat, showG){
|
||||
var c = C[key]; if(!c) return '';
|
||||
var g = '';
|
||||
if (showG && heimat && C[heimat] && key!==heimat){
|
||||
var t = (PM[ing].producers[key]||{}).t||0; var d = Math.round(wkDist(heimat,key));
|
||||
g = ' <span class="g">'+Math.round(t/Math.max(1,d))+'</span>';
|
||||
}
|
||||
return '<span class="cty '+cls+'" title="'+(c.n)+(PM[ing].producers[key]?(' · '+fmtT(PM[ing].producers[key].t)+' · Markt: '+(PM[ing].producers[key].market||'global')):'')+(heimat&&key!==heimat?(' · '+Math.round(wkDist(heimat,key))+' km'):'')+'">'+c.f+' '+c.n+g+'</span>';
|
||||
}
|
||||
|
||||
function render(){
|
||||
var q = (document.getElementById('q').value||'').toLowerCase().trim();
|
||||
var onlyWarn = document.getElementById('onlyWarn').checked;
|
||||
var host = document.getElementById('list'); var html = '';
|
||||
var shown = 0;
|
||||
DISHES.forEach(function(d){
|
||||
if (_cat && d.category !== _cat) return;
|
||||
var heimat = d.heimat || null;
|
||||
var ings = (d.ings||[]).filter(function(k){ return PM[k]; });
|
||||
// Filter Suche
|
||||
var matchDish = !q || (d.name||'').toLowerCase().indexOf(q)>=0 || ings.some(function(k){ return (PM[k].nm||k).toLowerCase().indexOf(q)>=0; });
|
||||
if (!matchDish) return;
|
||||
var rows = ''; var dishHasWarn = false;
|
||||
ings.forEach(function(k){
|
||||
if (q && (d.name||'').toLowerCase().indexOf(q)<0 && (PM[k].nm||k).toLowerCase().indexOf(q)<0) return;
|
||||
var cl = classify(k, heimat); if(!cl) return;
|
||||
// Auffällig NUR im Heimat-Zweig: die Zutat ist lokal erzeugbar (Heimat baut
|
||||
// sie an), trotzdem ist ein „richtiges" Land > 1500 km weg → echter Ausreißer.
|
||||
// Welt-Zutaten (Pfeffer aus Vietnam etc.) sind zu Recht fern → keine Warnung.
|
||||
var warn = '';
|
||||
if (heimat && C[heimat] && cl.branch === 'heimat'){
|
||||
var farCorrect = cl.correct.filter(function(c){ return c!==heimat && wkDist(heimat,c) > 1500; });
|
||||
if (farCorrect.length){ warn = '<div class="warn">⚠ „richtig" trotz > 1500 km von '+C[heimat].n+': '+farCorrect.map(function(c){return C[c].n+' ('+Math.round(wkDist(heimat,c))+' km)';}).join(', ')+'</div>'; dishHasWarn=true; }
|
||||
}
|
||||
if (onlyWarn && !warn) return;
|
||||
var okChips = cl.correct.map(function(c){ return ctyChip(c,'ok',k,heimat,true); }).join('');
|
||||
var distChips = cl.distractors.slice(0,6).map(function(c){ return ctyChip(c,'dist',k,heimat,false); }).join('');
|
||||
rows += '<tr><td class="ing-nm">'+(PM[k].ic||'')+' '+(PM[k].nm||k)+'<small>'+(cl.branch==='heimat'?'Heimat-Regel':'Welt-Zutat')+'</small></td>'
|
||||
+ '<td><div class="colhead">richtig</div>'+(okChips||'—')+warn+'</td>'
|
||||
+ '<td><div class="colhead">Distraktoren (Pool)</div>'+(distChips||'—')+'</td></tr>';
|
||||
});
|
||||
if (!rows) return;
|
||||
if (onlyWarn && !dishHasWarn) return;
|
||||
shown++;
|
||||
html += '<div class="dish"><div class="dish-h"><span class="nm">'+(d.name||d.id)+'</span>'
|
||||
+ (heimat&&C[heimat]?'<span class="heimat">Heimat: '+C[heimat].f+' '+C[heimat].n+'</span>':'<span class="heimat">ohne Heimat (Welt-Gericht)</span>')
|
||||
+ (d.category?'<span class="cat">'+d.category+'</span>':'')+'</div>'
|
||||
+ '<table><thead><tr><th style="width:26%">Zutat</th><th style="width:37%">🟢 Richtig</th><th style="width:37%">🔴 Distraktoren</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
||||
});
|
||||
document.getElementById('status').style.display = 'none';
|
||||
host.innerHTML = html || '<div id="status" style="display:block">Keine Treffer.</div>';
|
||||
}
|
||||
function setCat(el, cat){ _cat = cat; document.querySelectorAll('.chip').forEach(function(c){ c.classList.toggle('on', c===el); }); render(); }
|
||||
|
||||
// Auth + Daten laden
|
||||
fetch(BASE + '/api/admin', {method:'POST', headers:{'Content-Type':'application/json'}, credentials:'same-origin', body:JSON.stringify({action:'status'})})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
if (!r.authenticated){ location.href = 'admin.html'; return; }
|
||||
return Promise.all([
|
||||
fetch(D+'countries-full.json').then(function(r){return r.json();}),
|
||||
fetch(D+'production-matrix.json').then(function(r){return r.json();}),
|
||||
fetch(D+'dishes.json').then(function(r){return r.json();}),
|
||||
]).then(function(a){
|
||||
C = a[0]; PM = a[1];
|
||||
DISHES = Object.keys(a[2]).filter(function(k){return k[0]!=='_';}).map(function(id){ return Object.assign({id:id}, a[2][id]); });
|
||||
render();
|
||||
});
|
||||
})
|
||||
.catch(function(e){ document.getElementById('status').textContent = 'Fehler: '+e.message; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -283,7 +283,7 @@ export const BUILDINGS = {
|
||||
},
|
||||
naturschutz: {
|
||||
name: 'Naturschutzgebiet', emoji: '🌿', cat: 'nature', unlockPhase: 4, maxLevel: 1, fx: '🦋', sound: 'pling',
|
||||
desc: 'Umwelt +20, Naturerlebnis +15 % – sperrt aber Bauplätze im Umkreis.',
|
||||
desc: 'Hebt die Umwelt sofort um +20 und macht Natur-Attraktionen 15 % attraktiver. So rettest du die Landschaft, wenn zu viel Bauen die Umwelt gedrückt hat – Wanderer und Naturfreunde danken es dir. Preis: sperrt umliegende Bauplätze (Naturschutz vor Ausbau).',
|
||||
cost: [300],
|
||||
sat: [6],
|
||||
slow: [0.05],
|
||||
|
||||
@@ -112,6 +112,10 @@ function startGame() {
|
||||
+ 'See, Berg – und drei Wege, auf denen Gäste ankommen.\n\n'
|
||||
+ 'Ziel: zufriedene Gäste (Ø 3,5 ★), regionale Wertschöpfung (2500 €), '
|
||||
+ 'intakte Umwelt (≥ 45) und ein solides Budget.\n\n'
|
||||
+ '🌲 Die Umwelt ist deine Landschaft: Wanderer und Naturfreunde lieben sie intakt. '
|
||||
+ 'Zu viel Bauen und Verkehr lässt die Umwelt sinken – fällt sie zu tief, vergraulst '
|
||||
+ 'du genau diese Gäste. Ein 🌿 Naturschutzgebiet hebt die Umwelt wieder und zieht '
|
||||
+ 'Naturgäste an, sperrt aber Bauplätze in der Nähe. Abwägen ist alles!\n\n'
|
||||
+ 'Gebaut wird auf den markierten Bauplätzen am Weg – starte mit einem Wirtshaus!',
|
||||
});
|
||||
if (!localStorage.getItem('tourismusregion_seen')) {
|
||||
|
||||
@@ -119,6 +119,30 @@ export function tick(state, dt) {
|
||||
cost /= 6; // pro Minute → pro 10 s
|
||||
state.budget -= cost;
|
||||
state.opCostTotal += cost;
|
||||
|
||||
// Übertourismus (§10 Raumnutzungskonflikt): dichte Bebauung + viel Verkehr
|
||||
// zehren die Umwelt langsam aus. Ab ~6 fertigen Bauwerken beginnt der Druck.
|
||||
// „Alles zubauen" drückt die Umwelt so über die Partie klar unter die
|
||||
// 45er-Schwelle — vorher blieb sie fast konstant. Naturschutzgebiet (+20)
|
||||
// und maßvolles Bauen sind die Gegenmittel.
|
||||
const builtCount = state.buildings.reduce((n, b) => n + (b.construction <= 0 ? 1 : 0), 0);
|
||||
const overbuild = Math.max(0, builtCount - 6);
|
||||
const envErode = overbuild * 0.06 + state.traffic * 0.012; // pro 10 s
|
||||
if (envErode > 0) state.env = clampEnv(state.env - envErode);
|
||||
|
||||
// Einmalige Lern-Warnung, sobald die Umwelt kippt (didaktischer Hinweis).
|
||||
if (state.env < 50 && !state.envWarned) {
|
||||
state.envWarned = true;
|
||||
state.pendingCards.push({
|
||||
kind: 'card', icon: '🌲', title: 'Die Umwelt leidet!', sound: 'card',
|
||||
text: 'Deine Umwelt ist unter 50 gefallen. Umweltbewusste Gäste – '
|
||||
+ 'Wanderer, Naturfreunde, Radgruppen – werden unzufrieden, wenn sie '
|
||||
+ 'unter 45 sinkt, und bleiben irgendwann ganz weg.\n\n'
|
||||
+ 'Was hilft: maßvoller bauen, auf sanfte Anreise (Bus, Rad, Bahn) '
|
||||
+ 'setzen – oder ein 🌿 Naturschutzgebiet bauen, das die Umwelt sofort '
|
||||
+ 'um +20 hebt. Naturschutz gegen Ausbau abwägen ist der Kern des Spiels.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Verkehr: Parkplätze + aktive Auto-/Busgruppen
|
||||
@@ -495,7 +519,14 @@ function exitVisitor(state, v) {
|
||||
const tPen = state.traffic * 0.08 * g.trafficSens;
|
||||
if (tPen > 4) malus.traffic = true;
|
||||
sat -= tPen;
|
||||
if (state.env < 45 && g.envSens >= 0.7) malus.env = true;
|
||||
// Umwelt-Malus WIRKLICH anwenden (war zuvor nur ein toter Flag ohne Wirkung):
|
||||
// eine kaputte Landschaft vergrault Gäste — je umweltbewusster, desto stärker,
|
||||
// und je tiefer die Umwelt fällt, desto heftiger. Ab env<60 spürbar, <40 hart.
|
||||
// Das ist der Raumnutzungskonflikt: zu viel zubauen → Umwelt kippt → Rating fällt.
|
||||
if (state.env < 60 && g.envSens >= 0.4) {
|
||||
sat -= g.envSens * (60 - state.env) * 0.5; // env 40 → ~6, env 20 → ~12 (× envSens)
|
||||
if (state.env < 45) malus.env = true;
|
||||
}
|
||||
if (v.timeIn > g.maxTime) { sat *= 0.92; malus.time = true; }
|
||||
|
||||
const ratio = Math.max(0, sat) / g.satTarget;
|
||||
|
||||
+37
-25
@@ -1487,6 +1487,13 @@ var SIM_LABELS = {
|
||||
};
|
||||
function simWord(id){ return (SIM_LABELS[id] && SIM_LABELS[id].w) || id; }
|
||||
function simCode(id){ return (SIM_LABELS[id] && SIM_LABELS[id].c) || String(id||'').slice(0,3).toUpperCase(); }
|
||||
// Stabile, unterscheidbare Farbe je Sim (eine Farbe pro Simulation) — Hash→Farbton.
|
||||
function simColor(id){
|
||||
if (SIM_LABELS[id] && SIM_LABELS[id].col) return SIM_LABELS[id].col;
|
||||
var s = String(id||''), h = 0;
|
||||
for (var i=0;i<s.length;i++) h = (h*33 + s.charCodeAt(i)) % 360;
|
||||
return 'hsl('+h+',48%,42%)';
|
||||
}
|
||||
// Rückwärtskompatibel: LIVE_SIM_SHORT[id] = Wort je Sim.
|
||||
var LIVE_SIM_SHORT = Object.keys(SIM_LABELS).reduce(function(o,k){ o[k]=SIM_LABELS[k].w; return o; }, {});
|
||||
function _liveFmtMin(sec) {
|
||||
@@ -2049,44 +2056,49 @@ async function renderLiveSummary() {
|
||||
var pctL = _msToPct(sess.startMs);
|
||||
var pctR = _msToPct(endMs);
|
||||
var pctW = Math.max(0.8, pctR - pctL); // mindestens 0.8 % sichtbar
|
||||
var simW = simWord(sess.simId);
|
||||
var simC = simCode(sess.simId);
|
||||
var lbl = simW + (sess.lessonLabel ? ' '+sess.lessonLabel : '');
|
||||
var color = _liveSuccessColor(sess.success);
|
||||
var simW = simWord(sess.simId), simC = simCode(sess.simId);
|
||||
var col = simColor(sess.simId); // eine Farbe je Sim
|
||||
var stars = (sess.stars != null) ? Math.max(0, Math.min(5, sess.stars)) : null;
|
||||
var aborted = sess.success === 'aborted';
|
||||
var completed = !aborted && stars != null; // fertig gespielt
|
||||
var inProgress = !aborted && stars == null; // noch dran / kein Ergebnis
|
||||
// Balkenhöhe = Maß für den Erfolg (mehr Sterne → höher). Abbruch/läuft: flach.
|
||||
var barH = completed ? (9 + Math.round(stars / 5 * 15)) : (aborted ? 9 : 12);
|
||||
// Rahmen kodiert den Zustand: fertig = geschlossen · noch dran = rechts offen ·
|
||||
// Abbruch = gestrichelt (+ ✖ am Ende).
|
||||
var border = completed ? 'border:1.5px solid rgba(20,20,20,.45);'
|
||||
: inProgress ? 'border:1.5px solid rgba(20,20,20,.4);border-right:0;'
|
||||
: 'border:1.5px dashed rgba(20,20,20,.45);';
|
||||
var stateWord = completed ? 'fertig' : aborted ? 'abgebrochen' : 'läuft noch';
|
||||
var tip = (sess.simTitle||sess.simId) + (sess.lessonLabel?' · '+sess.lessonLabel:'')
|
||||
+ ' · ' + _fmtHM(sess.startMs) + '–' + _fmtHM(endMs)
|
||||
+ ' · ' + _liveFmtMin(realDurSec)
|
||||
+ (sess.stars!=null?' · '+sess.stars+'/5★':'');
|
||||
// enger Balken → 3-Buchstaben-Kürzel, mittel → Wort, breit → Wort + Lektion
|
||||
var inner = (pctW < 2.5) ? '' : (pctW < 6) ? simC : (pctW < 11) ? simW : lbl;
|
||||
+ ' · ' + _liveFmtMin(realDurSec) + ' · ' + stateWord
|
||||
+ (stars!=null?' · '+stars+'/5★':'');
|
||||
// enger Balken → Kürzel, mittel → Wort, breit → Wort + Lektion
|
||||
var inner = (pctW < 2.5) ? '' : (pctW < 6) ? simC : (pctW < 11) ? simW : (simW + (sess.lessonLabel ? ' '+sess.lessonLabel : ''));
|
||||
var sessKey = s.studentId + ':' + sess.simId + ':' + (sess.lessonLabel||'_') + ':' + sess.startMs;
|
||||
html += '<div onclick="openLiveSessionDetail(\''+sessKey+'\')" '
|
||||
+ 'title="'+esc(tip)+'" '
|
||||
+ 'style="position:absolute;left:'+pctL+'%;width:'+pctW+'%;top:2px;height:24px;'
|
||||
+ 'background:'+color+';color:#fff;border-radius:3px;'
|
||||
+ 'padding:0 .35rem;font-size:.62rem;font-weight:700;line-height:24px;'
|
||||
+ 'style="position:absolute;left:'+pctL+'%;width:'+pctW+'%;bottom:2px;height:'+barH+'px;'
|
||||
+ 'background:'+col+';color:#fff;border-radius:3px;'+border
|
||||
+ 'padding:0 .3rem;font-size:.6rem;font-weight:700;line-height:'+Math.max(9,barH-3)+'px;'
|
||||
+ 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;'
|
||||
+ 'box-shadow:0 1px 2px rgba(0,0,0,.1)">'
|
||||
+ 'text-shadow:0 1px 1px rgba(0,0,0,.35);box-shadow:0 1px 2px rgba(0,0,0,.12)">'
|
||||
+ esc(inner) + '</div>';
|
||||
// Nach Abschluss immer die Sterne (gold) am Balkenende zeigen
|
||||
if (sess.stars != null) {
|
||||
var _sc = Math.max(0, Math.min(5, sess.stars));
|
||||
var _starTxt = '★'.repeat(_sc) + '<span style="color:#d9d2be">' + '★'.repeat(5-_sc) + '</span>';
|
||||
var _rightEdge = pctR > 82;
|
||||
html += '<span style="position:absolute;'
|
||||
+ (_rightEdge ? 'right:'+(100-pctR)+'%;padding-right:4px;' : 'left:'+pctR+'%;padding-left:3px;')
|
||||
+ 'top:0;height:28px;line-height:28px;font-size:.62rem;color:#e0a838;font-weight:700;'
|
||||
+ 'letter-spacing:.5px;white-space:nowrap;pointer-events:none">'+_starTxt+'</span>';
|
||||
if (aborted) {
|
||||
html += '<span style="position:absolute;left:'+pctR+'%;bottom:-1px;height:28px;line-height:28px;'
|
||||
+ 'font-size:.74rem;color:#b03a2a;font-weight:800;padding-left:1px;pointer-events:none">✖</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:.8rem;display:flex;gap:.6rem;flex-wrap:wrap;font-size:.65rem;color:#5a5a5a">';
|
||||
[['good','★ ≥ 4 — gut'],['mid','★ 2-3 — mittel'],['bad','★ ≤ 1 — schwach'],['aborted','abgebrochen'],['running','läuft noch']].forEach(function(p){
|
||||
html += '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:'+_liveSuccessColor(p[0])+';vertical-align:middle;margin-right:.25rem"></span>'+p[1]+'</span>';
|
||||
});
|
||||
html += '<div style="margin-top:.8rem;display:flex;gap:1rem;flex-wrap:wrap;font-size:.66rem;color:#5a5a5a;align-items:center">';
|
||||
html += '<span><b>Farbe</b> = Simulation · <b>Höhe</b> = Erfolg (höher = besser)</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px solid rgba(20,20,20,.45);vertical-align:middle;margin-right:.25rem"></span>fertig</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px solid rgba(20,20,20,.4);border-right:0;vertical-align:middle;margin-right:.25rem"></span>läuft noch</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px dashed rgba(20,20,20,.45);vertical-align:middle;margin-right:.15rem"></span><span style="color:#b03a2a;font-weight:800">✖</span> Abbruch</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
Reference in New Issue
Block a user