f22c5ebbfe
- PHP/MySQL Backend (XAMPP + Produktionsserver) - Front-Controller, API-Endpunkte, Session-Management - Flussmanagement-Simulation (Echtzeit, Punkt-basierter Fluss) - Stadt & Raumplanung (Prototyp, Top-Down Kachelsystem) - Klimawaechter 3D: Deiche kleiner, Baeume kippen, Budget angepasst - persistence.ts: Dualer Speicher (localStorage + Server-API) - 6 Unit-Test-Dateien fuer bestehende Simulationen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
546 lines
22 KiB
HTML
546 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GeoGraSim — Lieferketten-Planer*in</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">
|
||
<style>
|
||
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
|
||
:root {
|
||
--bg: #f7f6f3;
|
||
--text: #1a1a1a; --text2: #4a4a4a; --text3: #8a8a8a;
|
||
--fjord: #4a7c8a; --fjord-l: #dae8ec; --fjord-d: #3a6470;
|
||
--moss: #5a8a5e; --moss-l: #dceadd;
|
||
--sand: #c4a35a; --sand-l: #f2eacc;
|
||
--coral: #c07a6b; --coral-l: #f2ddd8;
|
||
--danger: #b04a3a;
|
||
--glass-bg: rgba(255,255,255,.85);
|
||
--glass-bg2: rgba(255,255,255,.92);
|
||
--glass-bord: rgba(255,255,255,.55);
|
||
--glass-shadow: 0 8px 32px rgba(20,40,60,.18);
|
||
--topbar-h: 2.8rem;
|
||
--side-w: 220px;
|
||
--pad: .65rem;
|
||
}
|
||
html, body { height:100%; overflow:hidden; }
|
||
body { font-family:'Inter',system-ui,sans-serif; background:#dceaf0; color:var(--text); line-height:1.5; }
|
||
|
||
#canvas-wrap { position:fixed; inset:0; z-index:0; }
|
||
|
||
/* === FLOATING PANELS === */
|
||
.floating {
|
||
position:fixed; z-index:5;
|
||
backdrop-filter:blur(14px) saturate(140%); -webkit-backdrop-filter:blur(14px) saturate(140%);
|
||
background:var(--glass-bg); border:1px solid var(--glass-bord);
|
||
box-shadow:var(--glass-shadow); border-radius:14px;
|
||
}
|
||
|
||
/* === TOPBAR === */
|
||
.topbar {
|
||
top:0; left:0; right:0;
|
||
padding:.35rem .7rem;
|
||
display:flex; justify-content:space-between; align-items:center; gap:.8rem;
|
||
border-radius:0; border-bottom:1px solid rgba(0,0,0,.08);
|
||
}
|
||
.topbar .brand {
|
||
display:flex; align-items:center; gap:.5rem; font-weight:800; font-size:.92rem;
|
||
color:var(--fjord); text-decoration:none; white-space:nowrap;
|
||
}
|
||
.topbar .brand img { width:24px; height:24px; border-radius:6px; }
|
||
.topbar-controls { display:flex; gap:.5rem; align-items:center; }
|
||
.icon-btn {
|
||
width:32px; height:32px; border-radius:8px;
|
||
border:1px solid rgba(0,0,0,.08); background:rgba(255,255,255,.55);
|
||
color:var(--text2); font-size:.95rem; cursor:pointer;
|
||
display:flex; align-items:center; justify-content:center; padding:0;
|
||
text-decoration:none; transition:all .15s;
|
||
}
|
||
.icon-btn:hover { background:var(--fjord-l); border-color:var(--fjord); color:var(--fjord); }
|
||
.ctrl-group {
|
||
display:flex; gap:.2rem; padding:.2rem;
|
||
background:rgba(255,255,255,.45); border-radius:9px;
|
||
border:1px solid rgba(0,0,0,.06);
|
||
}
|
||
.ctrl-group .icon-btn {
|
||
border:none; background:transparent;
|
||
width:30px; height:30px; font-size:.8rem; font-weight:700;
|
||
}
|
||
.ctrl-group .icon-btn:hover { background:rgba(0,0,0,.06); }
|
||
.ctrl-group .icon-btn.active { background:var(--fjord); color:#fff; }
|
||
.topbar-sep { width:1px; height:24px; background:rgba(0,0,0,.08); }
|
||
|
||
/* === STATUS-PANEL (LINKS) === */
|
||
.panel-h {
|
||
font-size:.58rem; font-weight:800; color:var(--fjord);
|
||
text-transform:uppercase; letter-spacing:.07em; margin-bottom:.4rem;
|
||
}
|
||
.panel-h-sub { margin-top:.9rem; padding-top:.6rem; border-top:1px solid rgba(0,0,0,.08); }
|
||
.panel-status {
|
||
top: calc(var(--topbar-h) + var(--pad));
|
||
left: var(--pad);
|
||
width: var(--side-w);
|
||
padding: .6rem .75rem;
|
||
bottom: var(--pad);
|
||
overflow-y: auto;
|
||
}
|
||
.resource {
|
||
display:flex; align-items:flex-start; gap:.5rem; padding:.3rem .1rem;
|
||
border-bottom:1px solid rgba(0,0,0,.06);
|
||
}
|
||
.resource:last-child { border-bottom:none; }
|
||
.resource-icon { font-size:1.15rem; width:22px; text-align:center; padding-top:.1rem; }
|
||
.resource-info { flex:1; min-width:0; }
|
||
.resource-name { font-size:.58rem; color:var(--text3); font-weight:500; line-height:1.1; }
|
||
.resource-val { font-size:.85rem; font-weight:800; color:var(--text); line-height:1.2; }
|
||
|
||
/* === GOALS === */
|
||
.goal { padding:.3rem 0; border-bottom:1px solid rgba(0,0,0,.06); }
|
||
.goal:last-child { border:0; }
|
||
.goal-row { display:flex; align-items:center; gap:.35rem; font-size:.7rem; }
|
||
.goal-check {
|
||
width:12px; height:12px; border:1.5px solid var(--text3);
|
||
border-radius:3px; display:flex; align-items:center; justify-content:center;
|
||
font-size:.52rem; flex-shrink:0;
|
||
}
|
||
.goal-check.done { background:var(--moss); border-color:var(--moss); color:#fff; }
|
||
.goal-bar { height:3px; background:rgba(0,0,0,.08); border-radius:2px; margin-top:.2rem; overflow:hidden; }
|
||
.goal-bar-fill { height:100%; background:var(--moss); transition:width .3s; }
|
||
|
||
/* === BESTELL-FORMULAR (RECHTS) === */
|
||
.panel-order {
|
||
top: calc(var(--topbar-h) + var(--pad));
|
||
right: var(--pad);
|
||
bottom: var(--pad);
|
||
width: 300px;
|
||
padding: .85rem 1rem 1rem;
|
||
overflow-y: auto;
|
||
}
|
||
.order-section { margin-bottom: 1rem; }
|
||
.order-section h4 {
|
||
font-size: .65rem; font-weight: 800; color: var(--fjord);
|
||
text-transform: uppercase; letter-spacing: .07em;
|
||
margin-bottom: .35rem;
|
||
}
|
||
.order-options { display: flex; flex-direction: column; gap: .25rem; }
|
||
.order-option {
|
||
display: flex; align-items: center; gap: .4rem;
|
||
padding: .35rem .5rem; border-radius: 8px;
|
||
background: rgba(255,255,255,.55);
|
||
border: 1.5px solid rgba(0,0,0,.06);
|
||
cursor: pointer; transition: all .12s;
|
||
font-family: inherit; text-align: left;
|
||
}
|
||
.order-option:hover { border-color: var(--fjord); background: var(--fjord-l); }
|
||
.order-option.selected { border-color: var(--moss); background: var(--moss-l); }
|
||
.oo-flag { font-size: .9rem; flex-shrink: 0; }
|
||
.oo-info { flex: 1; min-width: 0; }
|
||
.oo-name { font-size: .68rem; font-weight: 700; line-height: 1.15; }
|
||
.oo-stats {
|
||
font-size: .55rem; color: var(--text3); line-height: 1.2;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.oo-stats .oo-stat { display: inline-block; margin-right: .35rem; }
|
||
.oo-stat-good { color: var(--moss); }
|
||
.oo-stat-bad { color: var(--danger); }
|
||
|
||
.transport-row { display: flex; gap: .25rem; }
|
||
.transport-btn {
|
||
flex: 1; padding: .4rem .25rem; border-radius: 8px;
|
||
background: rgba(255,255,255,.55); border: 1.5px solid rgba(0,0,0,.06);
|
||
cursor: pointer; font-family: inherit;
|
||
display: flex; flex-direction: column; align-items: center; gap: .1rem;
|
||
transition: all .12s;
|
||
}
|
||
.transport-btn:hover { border-color: var(--fjord); background: var(--fjord-l); }
|
||
.transport-btn.selected { border-color: var(--moss); background: var(--moss-l); }
|
||
.transport-btn .tb-emoji { font-size: 1.1rem; }
|
||
.transport-btn .tb-name { font-size: .55rem; font-weight: 700; }
|
||
|
||
.order-summary {
|
||
margin-top: .8rem; padding: .65rem .75rem;
|
||
background: rgba(0,0,0,.04); border-radius: 9px;
|
||
font-size: .68rem; line-height: 1.5;
|
||
}
|
||
.order-summary .os-row {
|
||
display: flex; justify-content: space-between;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.order-summary .os-row strong { font-weight: 800; }
|
||
.os-good { color: var(--moss); }
|
||
.os-bad { color: var(--danger); }
|
||
.order-btn {
|
||
margin-top: .7rem; width: 100%;
|
||
padding: .65rem; border: none; border-radius: 9px;
|
||
background: var(--fjord); color: #fff;
|
||
font-weight: 800; font-size: .85rem; cursor: pointer;
|
||
font-family: inherit; transition: all .15s;
|
||
}
|
||
.order-btn:hover { background: var(--fjord-d); }
|
||
.order-btn:disabled { background: #c0c0c0; cursor: not-allowed; }
|
||
|
||
/* === EVENTS-STRIP (unten links neben dem Status) === */
|
||
.panel-events-strip {
|
||
position: fixed;
|
||
left: calc(var(--side-w) + var(--pad) * 2);
|
||
bottom: var(--pad);
|
||
width: 280px;
|
||
max-height: 90px;
|
||
padding: .35rem .55rem .45rem;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: max-height .38s cubic-bezier(.22,1,.36,1);
|
||
z-index: 12;
|
||
display: flex; flex-direction: column; gap: .25rem;
|
||
}
|
||
.panel-events-strip.open {
|
||
max-height: calc(100vh - var(--topbar-h) - 4 * var(--pad));
|
||
box-shadow: 0 18px 50px rgba(20,40,60,.28);
|
||
cursor: zoom-out;
|
||
}
|
||
.events-strip-header {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
font-size: .58rem; font-weight: 800; color: var(--fjord);
|
||
text-transform: uppercase; letter-spacing: .07em;
|
||
flex-shrink: 0;
|
||
}
|
||
.esh-toggle { transition: transform .35s ease-out; font-size: .65rem; color: var(--text3); }
|
||
.panel-events-strip.open .esh-toggle { transform: rotate(180deg); }
|
||
.events-strip-list {
|
||
overflow-y: auto;
|
||
flex: 1; min-height: 0;
|
||
}
|
||
.panel-events-strip:not(.open) .events-strip-list .event:not(:first-child) {
|
||
display: none;
|
||
}
|
||
.panel-events-strip:not(.open) .events-strip-list .event {
|
||
margin-bottom: 0;
|
||
}
|
||
.event {
|
||
padding:.3rem .45rem; border-radius:7px; margin-bottom:.25rem;
|
||
font-size:.65rem; line-height:1.3;
|
||
display:flex; gap:.3rem; align-items:flex-start;
|
||
}
|
||
.event-text { flex:1; }
|
||
.event-info { background:rgba(0,0,0,.05); color:var(--text2); }
|
||
.event-success { background:var(--moss-l); color:var(--moss); }
|
||
.event-warning { background:var(--sand-l); color:var(--sand); }
|
||
.event-danger { background:var(--coral-l); color:var(--danger); }
|
||
|
||
/* === TUTORIAL === */
|
||
.tutorial-overlay {
|
||
position:fixed; inset:0; background:rgba(15,30,40,.85);
|
||
backdrop-filter:blur(8px); display:flex; align-items:center;
|
||
justify-content:center; z-index:50; padding:1.5rem;
|
||
}
|
||
.tutorial-card {
|
||
background:#fff; border-radius:18px; padding:1.6rem 1.9rem;
|
||
max-width:520px; box-shadow:0 30px 80px rgba(0,0,0,.4);
|
||
}
|
||
.tutorial-card h3 { font-size:1.25rem; font-weight:800; margin-bottom:.6rem; color:var(--fjord); }
|
||
.tutorial-card p { font-size:.9rem; color:var(--text2); white-space:pre-line; margin-bottom:1rem; }
|
||
.progress-dots { display:flex; gap:.3rem; justify-content:center; margin:.8rem 0; }
|
||
.dot { width:8px; height:8px; border-radius:50%; background:#dadad0; }
|
||
.dot.active { background:var(--fjord); }
|
||
.tut-btn {
|
||
width:100%; padding:.75rem; background:var(--fjord); color:#fff;
|
||
border:none; border-radius:10px; font-weight:700; font-size:.95rem; cursor:pointer;
|
||
}
|
||
.tut-btn:hover { background:var(--fjord-d); }
|
||
|
||
/* === END SCREEN === */
|
||
.end-screen {
|
||
position:fixed; inset:0; background:rgba(15,30,40,.9);
|
||
backdrop-filter:blur(10px); display:flex; align-items:center;
|
||
justify-content:center; z-index:100; padding:2rem;
|
||
}
|
||
.end-card { background:#fff; border-radius:20px; padding:2rem 2.5rem; max-width:500px; text-align:center; box-shadow:0 30px 80px rgba(0,0,0,.4); }
|
||
.end-icon { font-size:4rem; margin-bottom:.5rem; }
|
||
.end-card h2 { font-size:1.8rem; font-weight:900; letter-spacing:-.02em; margin-bottom:.3rem; }
|
||
.end-card .end-summary { color:var(--text2); margin-bottom:1rem; }
|
||
.end-stats { display:grid; grid-template-columns:1fr 1fr; gap:.8rem; margin:1rem 0; }
|
||
.end-stat { background:#f7f6f3; border-radius:10px; padding:.8rem; }
|
||
.end-stat-val { font-size:1.4rem; font-weight:800; color:var(--fjord); }
|
||
.end-stat-lbl { font-size:.7rem; color:var(--text3); text-transform:uppercase; }
|
||
.end-goals { display:flex; flex-direction:column; gap:.3rem; margin:.8rem 0 1rem; text-align:left; }
|
||
.end-goal { display:flex; align-items:center; gap:.55rem; font-size:.82rem; padding:.35rem .55rem; border-radius:7px; background:#f7f6f3; }
|
||
.end-goal.done { color:var(--moss); background:var(--moss-l); }
|
||
.end-goal.fail { color:var(--danger); background:var(--coral-l); }
|
||
.end-goal-icon { font-weight:900; font-size:.95rem; width:1rem; text-align:center; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="canvas-wrap"></div>
|
||
|
||
<!-- TOP BAR -->
|
||
<nav class="floating topbar">
|
||
<a href="index.html" class="brand">
|
||
<img src="assets/img/logo.png" alt="">
|
||
Lieferketten-Planer*in
|
||
</a>
|
||
<div class="topbar-controls">
|
||
<div class="ctrl-group" title="Simulationsgeschwindigkeit">
|
||
<button class="icon-btn" data-speed="0" title="Pause">⏸</button>
|
||
<button class="icon-btn active" data-speed="1" title="Normal (×1)">×1</button>
|
||
<button class="icon-btn" data-speed="2" title="Schnell (×2)">×2</button>
|
||
<button class="icon-btn" data-speed="4" title="Sehr schnell (×4)">×4</button>
|
||
</div>
|
||
<div class="topbar-sep"></div>
|
||
<button class="icon-btn" id="btn-save" title="Stand speichern">💾</button>
|
||
<button class="icon-btn" id="btn-load" title="Stand laden">📂</button>
|
||
<button class="icon-btn" id="btn-finish" title="Simulation beenden">⏹</button>
|
||
<button class="icon-btn" id="btn-reset" title="Zurücksetzen">🔄</button>
|
||
<a class="icon-btn" href="index.html" title="Zurück zur Übersicht">🏠</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- LINKS: Status + Ziele -->
|
||
<aside class="floating panel-status">
|
||
<div class="panel-h">📊 Status</div>
|
||
<div id="resources"></div>
|
||
<div class="panel-h panel-h-sub">🎯 Ziele</div>
|
||
<div id="status-goals"></div>
|
||
</aside>
|
||
|
||
<!-- LINKS DANEBEN: Ereignisse-Strip -->
|
||
<aside class="floating panel-events-strip" id="events-strip">
|
||
<div class="events-strip-header">
|
||
<span>📰 Ereignisse</span>
|
||
<span class="esh-toggle">▲</span>
|
||
</div>
|
||
<div id="events" class="events-strip-list"></div>
|
||
</aside>
|
||
|
||
<!-- RECHTS: Bestell-Formular -->
|
||
<aside class="floating panel-order">
|
||
<div class="order-section">
|
||
<h4>🌾 Baumwolle</h4>
|
||
<div class="order-options" id="cotton-options"></div>
|
||
</div>
|
||
<div class="order-section">
|
||
<h4>🚚 Transport zur Näherei</h4>
|
||
<div class="transport-row" id="transport1-row"></div>
|
||
</div>
|
||
<div class="order-section">
|
||
<h4>🏭 Näherei</h4>
|
||
<div class="order-options" id="factory-options"></div>
|
||
</div>
|
||
<div class="order-section">
|
||
<h4>🚚 Transport nach Wien</h4>
|
||
<div class="transport-row" id="transport2-row"></div>
|
||
</div>
|
||
<div class="order-summary" id="order-summary"></div>
|
||
<button class="order-btn" id="order-btn" disabled>Bestellen</button>
|
||
</aside>
|
||
|
||
<!-- TUTORIAL -->
|
||
<div class="tutorial-overlay" id="tutorial">
|
||
<div class="tutorial-card">
|
||
<h3 id="tut-title">…</h3>
|
||
<p id="tut-text">…</p>
|
||
<div class="progress-dots" id="tut-dots"></div>
|
||
<button class="tut-btn" id="tut-next">Weiter →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="end-screen-host"></div>
|
||
<div id="citizen-event-host"></div>
|
||
<div id="info-overlay-host"></div>
|
||
|
||
<script type="module">
|
||
import {
|
||
LieferkettenGame, COTTON_SOURCES, FACTORIES, TRANSPORT_MODES, DEFAULT_CONFIG,
|
||
} from './src/sims/sim-10-lieferketten/game.ts'
|
||
import { LieferkettenRenderer } from './src/sims/sim-10-lieferketten/renderer.ts'
|
||
import { GameUI } from './src/ui/game-ui.ts'
|
||
import { installTooltips } from './src/ui/info-overlay.ts'
|
||
|
||
installTooltips()
|
||
|
||
// === Lehrer-Konfiguration via URL-Parameter ===
|
||
// ?config=<json> erlaubt der Lehrperson, die drei Hauptparameter zu setzen.
|
||
function readConfig() {
|
||
try {
|
||
const raw = new URLSearchParams(location.search).get('config')
|
||
if (raw) return JSON.parse(decodeURIComponent(raw))
|
||
} catch {}
|
||
return {}
|
||
}
|
||
const userConfig = readConfig()
|
||
const game = new LieferkettenGame(userConfig)
|
||
|
||
// === GameUI ohne Shop (wir bauen unser eigenes Bestell-Formular) ===
|
||
// Wir geben einen leeren shopItems-Array, damit GameUI nichts rendert.
|
||
const ui = new GameUI({
|
||
game,
|
||
Renderer: LieferkettenRenderer,
|
||
shopItems: [],
|
||
onBuy: () => false,
|
||
graphs: [],
|
||
saveKey: 'lieferketten-save',
|
||
yearOffset: 2025,
|
||
})
|
||
|
||
// === Bestell-Formular bauen ===
|
||
const filterByConfig = (list, allowed) => list.filter(x => allowed.includes(x.id))
|
||
const cottonAvailable = filterByConfig(COTTON_SOURCES, game.getConfig().enabledOptions.cotton)
|
||
const factoryAvailable = filterByConfig(FACTORIES, game.getConfig().enabledOptions.factory)
|
||
const transportAvailable = TRANSPORT_MODES.filter(t => game.getConfig().enabledOptions.transport.includes(t.id))
|
||
|
||
const currentOrder = {
|
||
cottonId: null,
|
||
factoryId: null,
|
||
transport1: null,
|
||
transport2: null,
|
||
}
|
||
|
||
function ratingClass(value, goodAbove, badBelow) {
|
||
if (value >= goodAbove) return 'oo-stat-good'
|
||
if (value < badBelow) return 'oo-stat-bad'
|
||
return ''
|
||
}
|
||
|
||
function renderCottonOptions() {
|
||
const el = document.getElementById('cotton-options')
|
||
el.innerHTML = cottonAvailable.map(c => `
|
||
<button class="order-option ${currentOrder.cottonId === c.id ? 'selected' : ''}" data-id="${c.id}">
|
||
<span class="oo-flag">${c.flag}</span>
|
||
<div class="oo-info">
|
||
<div class="oo-name">${c.country}</div>
|
||
<div class="oo-stats">
|
||
<span class="oo-stat">€${c.pricePerShirt.toFixed(2)}</span>
|
||
<span class="oo-stat ${ratingClass(-c.co2PerShirt, -1.5, -2.4)}">${c.co2PerShirt.toFixed(1)} kg CO₂</span>
|
||
<span class="oo-stat ${ratingClass(c.ethicsScore, 7, 5)}">⚖️ ${c.ethicsScore}</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
`).join('')
|
||
el.querySelectorAll('.order-option').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
currentOrder.cottonId = btn.dataset.id
|
||
renderCottonOptions(); updatePreview()
|
||
})
|
||
})
|
||
}
|
||
|
||
function renderFactoryOptions() {
|
||
const el = document.getElementById('factory-options')
|
||
el.innerHTML = factoryAvailable.map(f => `
|
||
<button class="order-option ${currentOrder.factoryId === f.id ? 'selected' : ''}" data-id="${f.id}">
|
||
<span class="oo-flag">${f.flag}</span>
|
||
<div class="oo-info">
|
||
<div class="oo-name">${f.country}</div>
|
||
<div class="oo-stats">
|
||
<span class="oo-stat">€${f.pricePerShirt.toFixed(2)}</span>
|
||
<span class="oo-stat ${ratingClass(-f.co2PerShirt, -1.0, -1.5)}">${f.co2PerShirt.toFixed(1)} kg CO₂</span>
|
||
<span class="oo-stat ${ratingClass(f.ethicsScore, 7, 5)}">⚖️ ${f.ethicsScore}</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
`).join('')
|
||
el.querySelectorAll('.order-option').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
currentOrder.factoryId = btn.dataset.id
|
||
renderFactoryOptions(); updatePreview()
|
||
})
|
||
})
|
||
}
|
||
|
||
function renderTransportRow(rowId, key) {
|
||
const el = document.getElementById(rowId)
|
||
el.innerHTML = transportAvailable.map(t => `
|
||
<button class="transport-btn ${currentOrder[key] === t.id ? 'selected' : ''}" data-id="${t.id}">
|
||
<span class="tb-emoji">${t.emoji}</span>
|
||
<span class="tb-name">${t.name.split(' ')[0]}</span>
|
||
</button>
|
||
`).join('')
|
||
el.querySelectorAll('.transport-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
currentOrder[key] = btn.dataset.id
|
||
renderTransportRow(rowId, key); updatePreview()
|
||
})
|
||
})
|
||
}
|
||
|
||
function updatePreview() {
|
||
// Renderer-Vorschau aktualisieren
|
||
const r = ui.getRenderer()
|
||
if (r && typeof r.setPreviewOrder === 'function') {
|
||
r.setPreviewOrder(currentOrder)
|
||
}
|
||
|
||
// Order-Summary berechnen
|
||
const summary = document.getElementById('order-summary')
|
||
const btn = document.getElementById('order-btn')
|
||
const complete = currentOrder.cottonId && currentOrder.factoryId && currentOrder.transport1 && currentOrder.transport2
|
||
|
||
if (!complete) {
|
||
summary.innerHTML = '<div style="color:var(--text3);text-align:center;font-size:.7rem">Wähle alle 4 Stationen aus.</div>'
|
||
btn.disabled = true
|
||
return
|
||
}
|
||
|
||
// Vorschau-Berechnung lokal (mirror der placeOrder-Logik)
|
||
const c = COTTON_SOURCES.find(x => x.id === currentOrder.cottonId)
|
||
const f = FACTORIES.find(x => x.id === currentOrder.factoryId)
|
||
const t1 = TRANSPORT_MODES.find(x => x.id === currentOrder.transport1)
|
||
const t2 = TRANSPORT_MODES.find(x => x.id === currentOrder.transport2)
|
||
|
||
// Sehr grobe Schätzung — wir nutzen die Distanz-Mittelwerte (nicht die Effekte)
|
||
const dist1 = 5 // ~5000 km Schätzwert für Vorschau
|
||
const dist2 = 3 // ~3000 km Schätzwert
|
||
const t1Cost = t1.costPer1000km * dist1
|
||
const t2Cost = t2.costPer1000km * dist2
|
||
const t1CO2 = t1.co2Per1000km * dist1
|
||
const t2CO2 = t2.co2Per1000km * dist2
|
||
const costPerShirt = c.pricePerShirt + f.pricePerShirt + t1Cost + t2Cost
|
||
const co2PerShirt = c.co2PerShirt + f.co2PerShirt + t1CO2 + t2CO2
|
||
const ethics = (c.ethicsScore + f.ethicsScore) / 2
|
||
const profitPerShirt = 12 - costPerShirt
|
||
const profitMio = (5000 * profitPerShirt) / 1_000_000
|
||
|
||
const profitClass = profitMio >= 0 ? 'os-good' : 'os-bad'
|
||
const co2Class = co2PerShirt < 6 ? 'os-good' : 'os-bad'
|
||
const ethicsClass = ethics >= 5 ? 'os-good' : 'os-bad'
|
||
|
||
summary.innerHTML = `
|
||
<div class="os-row"><span>Kosten/Stück</span><strong>€${costPerShirt.toFixed(2)}</strong></div>
|
||
<div class="os-row"><span>Gewinn/Charge</span><strong class="${profitClass}">${profitMio >= 0 ? '+' : ''}${profitMio.toFixed(2)} Mio €</strong></div>
|
||
<div class="os-row"><span>CO₂/Stück</span><strong class="${co2Class}">${co2PerShirt.toFixed(1)} kg</strong></div>
|
||
<div class="os-row"><span>Ø Ethik</span><strong class="${ethicsClass}">${ethics.toFixed(1)} / 10</strong></div>
|
||
`
|
||
btn.disabled = false
|
||
}
|
||
|
||
document.getElementById('order-btn').addEventListener('click', () => {
|
||
if (!currentOrder.cottonId || !currentOrder.factoryId || !currentOrder.transport1 || !currentOrder.transport2) return
|
||
game.placeOrder({ ...currentOrder })
|
||
// Sprite-Animation auf der Karte
|
||
const r = ui.getRenderer()
|
||
if (r && typeof r.animateOrder === 'function') r.animateOrder(currentOrder)
|
||
})
|
||
|
||
// Events-Strip toggle
|
||
const eventsStrip = document.getElementById('events-strip')
|
||
eventsStrip.addEventListener('click', (ev) => {
|
||
ev.stopPropagation()
|
||
eventsStrip.classList.toggle('open')
|
||
})
|
||
document.addEventListener('click', (ev) => {
|
||
if (!eventsStrip.classList.contains('open')) return
|
||
if (eventsStrip.contains(ev.target)) return
|
||
eventsStrip.classList.remove('open')
|
||
})
|
||
|
||
// Initial render
|
||
renderCottonOptions()
|
||
renderFactoryOptions()
|
||
renderTransportRow('transport1-row', 'transport1')
|
||
renderTransportRow('transport2-row', 'transport2')
|
||
updatePreview()
|
||
</script>
|
||
</body>
|
||
</html>
|