6c2990aa5e
- Header nach oben RECHTS (raus aus dem Leaflet-Zoom oben links), Geld-Anzeige entfernt (steht in linker Sidebar), Emoji aus Titel, Jahr als Badge in die Pille (Konsistenz mit Batch 1), Inter-Font. - Jahr-Leiste + Anbau-Versuch-Spalte nach unten geschoben (unter Header). - Komponente: wiederverwendbarer Default-Musikplayer (data-ggs-music, Klima-Tracks als Plattform-Standard) + Musik-Widget-Styling fuer Sims ohne design-system.css. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1983 lines
80 KiB
HTML
1983 lines
80 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<title>🌾 Landwirtschaft · GeoGraSim</title>
|
||
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
|
||
|
||
<link rel="stylesheet" href="../../assets/fonts/inter.css">
|
||
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
|
||
<script src="../../assets/js/ggs-header.js"></script>
|
||
|
||
<style>
|
||
html, body { margin: 0; padding: 0; height: 100dvh; overflow: hidden;
|
||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
background: #f5f3ea; color: #2a2a2a;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
#map { width: 100vw; height: 100dvh; background: #cfe1ed; }
|
||
|
||
/* ===== Topbar ===== */
|
||
.ggs-topbar {
|
||
position: absolute; top: 8px; right: 8px; left: auto; z-index: 1001;
|
||
display: flex; gap: 8px; align-items: center; font-size: 13px;
|
||
}
|
||
.ggs-topbar a {
|
||
text-decoration: none; background: #f1ede2; color: #2a2a2a;
|
||
border: 1px solid #d4cfc0; border-radius: 6px;
|
||
padding: 4px 10px; font-size: 13px;
|
||
min-height: 36px; display: inline-flex; align-items: center; gap: 4px;
|
||
}
|
||
.ggs-topbar a:active { background: #e8e2d2; }
|
||
.ggs-topbar h1 { margin: 0; padding: 0 6px; font-size: 14px; font-weight: 700; color: #8b5a2b; }
|
||
.farmer-money {
|
||
margin-left: 4px; padding: 4px 10px;
|
||
background: #fff5dc; border: 1px solid #e8c547; border-radius: 6px;
|
||
font-weight: 800; color: #8b5a2b; font-size: 14px;
|
||
min-height: 36px; display: inline-flex; align-items: center;
|
||
}
|
||
.farmer-money.negative { background: #ffe8e3; border-color: #d8453a; color: #a02a20; }
|
||
|
||
/* ===== Year-Bar ===== */
|
||
.farmer-year-bar {
|
||
position: absolute; top: 60px; right: 8px; z-index: 1000;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 6px 10px; border-radius: 8px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||
display: flex; gap: 8px; align-items: center; font-size: 13px;
|
||
}
|
||
.farmer-year-label { color: #6b4f2a; font-weight: 700; }
|
||
.farmer-year-num { color: #8b5a2b; font-weight: 800; font-size: 16px; }
|
||
.farmer-harvest-btn {
|
||
background: #8b5a2b; color: #fff; border: none; border-radius: 6px;
|
||
padding: 8px 14px; font-size: 13px; font-weight: 700;
|
||
min-height: 36px; cursor: pointer; font-family: inherit;
|
||
}
|
||
.farmer-harvest-btn:active { background: #6b4520; }
|
||
.farmer-harvest-btn:disabled { background: #c8bfb0; cursor: not-allowed; }
|
||
.farmer-market {
|
||
margin-left: 4px; padding: 4px 10px;
|
||
background: #e8f0e8; border: 1px solid #4a7c4e; border-radius: 6px;
|
||
font-size: 12px; font-weight: 700; color: #1f4b37;
|
||
min-height: 36px; display: inline-flex; align-items: center; gap: 4px;
|
||
}
|
||
.farmer-market-label { font-size: 10px; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.04em; }
|
||
|
||
/* ===== KPI-Panel (links unter Topbar) — Bilanz-Statistik ===== */
|
||
.farmer-kpi {
|
||
position: absolute; left: 8px; top: 76px; z-index: 1000;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 8px; padding: 10px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||
width: 220px;
|
||
display: flex; flex-direction: column; gap: 4px;
|
||
}
|
||
.farmer-kpi-title {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.04em; color: #8b5a2b; padding: 2px 4px 4px;
|
||
}
|
||
.farmer-kpi-row {
|
||
padding: 6px 4px; border-top: 1px solid #f0e8d8;
|
||
}
|
||
.farmer-kpi-row:first-of-type { border-top: 0; padding-top: 4px; }
|
||
.farmer-kpi-label {
|
||
font-size: 10px; font-weight: 700; color: #6b6258;
|
||
text-transform: uppercase; letter-spacing: 0.04em;
|
||
}
|
||
.farmer-kpi-value {
|
||
font-size: 17px; font-weight: 800; color: #2a2a2a;
|
||
margin-top: 2px; line-height: 1.1;
|
||
}
|
||
.farmer-kpi-value-small {
|
||
font-size: 13px; font-weight: 600; color: #2a2a2a; margin-top: 2px;
|
||
}
|
||
.farmer-kpi-value.negative { color: #a02a20; }
|
||
.farmer-kpi-value.green { color: #1f7a3d; }
|
||
.farmer-kpi-bar {
|
||
height: 5px; background: #e8e2d2; border-radius: 3px;
|
||
margin-top: 5px; overflow: hidden;
|
||
}
|
||
.farmer-kpi-bar-fill {
|
||
height: 100%; background: #1f7a3d; border-radius: 3px;
|
||
transition: width 0.4s; width: 0%;
|
||
}
|
||
.farmer-kpi-bar-fill.warn { background: #e8c547; }
|
||
.farmer-kpi-bar-fill.bad { background: #d8453a; }
|
||
.farmer-kpi-spark {
|
||
margin-top: 5px; height: 56px;
|
||
background: #f8f5ec; border-radius: 4px;
|
||
border: 1px solid #efe6d3;
|
||
padding: 2px;
|
||
overflow: hidden;
|
||
}
|
||
.farmer-kpi-spark svg { width: 100%; height: 100%; display: block; }
|
||
.farmer-kpi-spark-axis {
|
||
font-size: 9px; fill: #888; font-weight: 600;
|
||
}
|
||
.farmer-kpi-spark-tick {
|
||
stroke: #d4cfc0; stroke-width: 0.5;
|
||
}
|
||
.farmer-kpi-spark-trigger {
|
||
font-size: 8px; fill: #b87800; font-weight: 700;
|
||
}
|
||
|
||
/* ===== Side-Panel (rechts unter Year-Bar) — toggelt zwischen Picker und Report ===== */
|
||
.farmer-side {
|
||
position: absolute; right: 8px; top: 112px; z-index: 1000;
|
||
max-height: calc(100dvh - 92px - env(safe-area-inset-bottom));
|
||
overflow-y: auto;
|
||
width: 240px;
|
||
}
|
||
|
||
/* Picker */
|
||
.farmer-picker {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 8px; padding: 8px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
}
|
||
.farmer-picker.hidden { display: none; }
|
||
.farmer-picker-title {
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.04em; color: #8b5a2b; padding: 2px 4px;
|
||
}
|
||
.farmer-crop-card {
|
||
display: flex; gap: 8px; align-items: center;
|
||
padding: 8px; border-radius: 6px;
|
||
background: #f8f5ec; border: 2px solid transparent;
|
||
cursor: pointer; min-height: 44px;
|
||
transition: background 0.1s, border-color 0.1s, opacity 0.1s;
|
||
}
|
||
.farmer-crop-card:active { background: #e8e2d2; }
|
||
.farmer-crop-card.active { background: #fff5dc; border-color: #8b5a2b; }
|
||
.farmer-crop-card.disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.farmer-crop-icon { font-size: 22px; line-height: 1; }
|
||
.farmer-crop-info { flex: 1; min-width: 0; }
|
||
.farmer-crop-name { font-size: 13px; font-weight: 700; }
|
||
.farmer-crop-desc { font-size: 11px; color: #6b6258; line-height: 1.3; }
|
||
.farmer-crop-counter {
|
||
background: #8b5a2b; color: #fff;
|
||
border-radius: 12px; padding: 2px 8px;
|
||
font-size: 11px; font-weight: 800;
|
||
min-width: 28px; text-align: center;
|
||
}
|
||
.farmer-crop-card.disabled .farmer-crop-counter { background: #999; }
|
||
.farmer-undo-btn {
|
||
background: #f1ede2; color: #6b4f2a;
|
||
border: 1px solid #d4cfc0; border-radius: 6px;
|
||
padding: 6px 10px; font-size: 12px; font-weight: 600;
|
||
cursor: pointer; min-height: 32px;
|
||
font-family: inherit; margin-bottom: 4px;
|
||
}
|
||
.farmer-undo-btn:active { background: #e8e2d2; }
|
||
.farmer-undo-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
/* Report-Panel */
|
||
.farmer-report-panel {
|
||
background: rgba(255, 255, 255, 0.97);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||
overflow: hidden;
|
||
display: none; flex-direction: column;
|
||
}
|
||
.farmer-report-panel.show { display: flex; }
|
||
.farmer-report-progress {
|
||
background: #8b5a2b; color: #fff;
|
||
padding: 6px 12px;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: 0.06em; display: flex; justify-content: space-between;
|
||
}
|
||
.farmer-report-body { padding: 14px; }
|
||
.farmer-report-headline {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.farmer-report-icon-big { font-size: 36px; line-height: 1; flex-shrink: 0; }
|
||
.farmer-report-headline-text {
|
||
display: flex; flex-direction: column; gap: 5px;
|
||
min-width: 0;
|
||
}
|
||
.farmer-report-crop-name {
|
||
font-size: 17px; font-weight: 800; color: #2a2a2a;
|
||
line-height: 1.2;
|
||
}
|
||
.farmer-report-suitability {
|
||
font-size: 11px; font-weight: 800; color: #fff;
|
||
padding: 4px 10px; border-radius: 12px;
|
||
text-transform: uppercase; letter-spacing: 0.04em;
|
||
white-space: nowrap;
|
||
align-self: flex-start;
|
||
}
|
||
.farmer-report-place {
|
||
font-size: 12px; color: #6b6258; margin-bottom: 12px;
|
||
padding: 6px 10px; background: #f8f5ec; border-radius: 6px;
|
||
line-height: 1.4;
|
||
}
|
||
.farmer-report-place strong { color: #2a2a2a; }
|
||
.farmer-report-narrative {
|
||
font-size: 13px; color: #2a2a2a; line-height: 1.55;
|
||
margin-bottom: 12px;
|
||
}
|
||
.farmer-report-trade {
|
||
border-top: 1px dashed #d4cfc0; padding-top: 10px;
|
||
font-size: 12px; color: #444;
|
||
}
|
||
.farmer-trade-line {
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 3px 0;
|
||
}
|
||
.farmer-trade-line .vehicle { font-size: 16px; }
|
||
.farmer-trade-line .amount { margin-left: auto; font-weight: 700; }
|
||
.farmer-trade-line.net { font-size: 14px; padding-top: 6px; border-top: 1px solid #eee; margin-top: 4px; }
|
||
.farmer-trade-line.net .amount.positive { color: #1f7a3d; }
|
||
.farmer-trade-line.net .amount.negative { color: #d8453a; }
|
||
.farmer-trade-line.totalverlust {
|
||
font-style: italic; color: #d8453a; padding: 4px 0;
|
||
}
|
||
.farmer-trade-line.co2 .amount { font-weight: 700; }
|
||
.farmer-trade-line.co2.co2-low .amount { color: #1f7a3d; }
|
||
.farmer-trade-line.co2.co2-mid .amount { color: #b87800; }
|
||
.farmer-trade-line.co2.co2-high .amount { color: #d8453a; }
|
||
.farmer-report-actions {
|
||
padding: 10px 14px;
|
||
background: #f8f5ec; border-top: 1px solid #e2ddd1;
|
||
display: flex; justify-content: flex-end; gap: 8px;
|
||
}
|
||
.farmer-report-btn {
|
||
background: #8b5a2b; color: #fff; border: none;
|
||
border-radius: 6px; padding: 10px 16px;
|
||
font-size: 13px; font-weight: 700; min-height: 40px;
|
||
cursor: pointer; font-family: inherit;
|
||
}
|
||
.farmer-report-btn:active { background: #6b4520; }
|
||
|
||
/* ===== Field-Marker mit Ring ===== */
|
||
.farmer-field-icon {
|
||
width: 40px; height: 40px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
box-shadow: 0 0 0 2px #fff;
|
||
transition: transform 0.2s ease-out;
|
||
}
|
||
.farmer-field-icon span { font-size: 22px; line-height: 1; }
|
||
.farmer-field-icon.current {
|
||
animation: farmer-pulse 1.1s ease-in-out infinite;
|
||
z-index: 1000 !important;
|
||
}
|
||
@keyframes farmer-pulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.25); }
|
||
}
|
||
|
||
/* ===== Hub-Marker ===== */
|
||
.farmer-hub-icon {
|
||
width: 28px; height: 28px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
border: 2px solid #8b5a2b;
|
||
border-radius: 6px;
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||
font-size: 16px; line-height: 1;
|
||
transition: all 0.2s;
|
||
}
|
||
.farmer-hub-icon.active {
|
||
border-color: #1f7a3d;
|
||
background: #d6f0d6;
|
||
box-shadow: 0 0 16px rgba(31, 122, 61, 0.7), 0 2px 6px rgba(0,0,0,0.2);
|
||
animation: farmer-hub-pulse 1.4s ease-in-out infinite;
|
||
z-index: 1100 !important;
|
||
}
|
||
@keyframes farmer-hub-pulse {
|
||
0%, 100% { transform: scale(1); box-shadow: 0 0 14px rgba(31,122,61,0.7), 0 2px 6px rgba(0,0,0,0.2); }
|
||
50% { transform: scale(1.18); box-shadow: 0 0 24px rgba(31,122,61,0.95), 0 2px 6px rgba(0,0,0,0.2); }
|
||
}
|
||
|
||
/* ===== Monokultur / Krankheit Boxen im Bericht ===== */
|
||
.farmer-disease-box {
|
||
background: #ffe8e3; border: 1px solid #d8453a; color: #a02a20;
|
||
padding: 10px 12px; border-radius: 6px; margin: 8px 0;
|
||
font-size: 12px; line-height: 1.45; font-weight: 600;
|
||
}
|
||
.farmer-mud-box {
|
||
background: #fff5dc; border: 1px solid #e8c547; color: #6b4f2a;
|
||
padding: 8px 12px; border-radius: 6px; margin: 8px 0;
|
||
font-size: 12px; line-height: 1.4;
|
||
}
|
||
.farmer-mud-box strong { color: #8b5a2b; }
|
||
|
||
/* ===== Vehicle / Money ===== */
|
||
.farmer-vehicle-icon {
|
||
font-size: 26px; line-height: 1;
|
||
text-shadow: 0 0 4px rgba(255,255,255,0.95), 0 0 4px rgba(255,255,255,0.95);
|
||
pointer-events: none;
|
||
}
|
||
.farmer-money-toast {
|
||
background: #1f7a3d; color: #fff;
|
||
padding: 4px 10px; border-radius: 14px;
|
||
font-size: 13px; font-weight: 800;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||
pointer-events: none; white-space: nowrap;
|
||
}
|
||
.farmer-money-toast.loss { background: #d8453a; }
|
||
|
||
/* ===== Toast (Hint) ===== */
|
||
.farmer-toast {
|
||
position: fixed; left: 50%; top: 60px;
|
||
transform: translateX(-50%);
|
||
background: rgba(40, 40, 40, 0.92); color: #fff;
|
||
padding: 10px 16px; border-radius: 8px;
|
||
font-size: 13px; font-weight: 600;
|
||
z-index: 3000; opacity: 0; pointer-events: none;
|
||
transition: opacity 0.2s; max-width: 80vw; text-align: center;
|
||
}
|
||
.farmer-toast.show { opacity: 1; }
|
||
|
||
/* ===== Welcome-Modal ===== */
|
||
.ggs-modal-overlay {
|
||
position: fixed; inset: 0;
|
||
background: rgba(20, 20, 20, 0.6);
|
||
display: none; align-items: center; justify-content: center;
|
||
z-index: 2000; padding: 12px;
|
||
}
|
||
.ggs-modal-overlay.show { display: flex; }
|
||
.ggs-modal {
|
||
background: #fff; border-radius: 12px;
|
||
max-height: 92dvh; width: min(680px, 96vw);
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||
position: relative;
|
||
}
|
||
.ggs-modal h2 { margin: 0 0 12px; color: #8b5a2b; padding-right: 36px; }
|
||
.ggs-modal-close {
|
||
position: absolute; top: 8px; right: 8px;
|
||
width: 36px; height: 36px;
|
||
border: none; background: #f1ede2; border-radius: 50%;
|
||
cursor: pointer; font-size: 18px; color: #6b4f2a;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.ggs-modal-close:active { background: #e0d9c5; }
|
||
.ggs-modal-actions {
|
||
position: sticky; bottom: 0;
|
||
background: linear-gradient(to bottom, rgba(255,255,255,0), #fff 30%);
|
||
padding-top: 12px; margin-top: 12px;
|
||
display: flex; gap: 8px; justify-content: flex-end;
|
||
}
|
||
.ggs-modal-actions button {
|
||
min-height: 44px; padding: 10px 16px;
|
||
border-radius: 6px; cursor: pointer;
|
||
font-size: 14px; font-weight: 700; font-family: inherit;
|
||
background: #8b5a2b; color: #fff; border: none;
|
||
}
|
||
.ggs-modal-actions button:active { background: #6b4520; }
|
||
.farmer-welcome-list {
|
||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px; margin: 12px 0;
|
||
}
|
||
.farmer-welcome-list > div {
|
||
text-align: center; padding: 10px 6px;
|
||
background: #f8f5ec; border-radius: 6px;
|
||
}
|
||
.farmer-welcome-list span { display: block; font-size: 28px; line-height: 1; }
|
||
.farmer-welcome-list strong { display: block; margin-top: 2px; }
|
||
.farmer-welcome-list small { display: block; font-size: 11px; color: #6b6258; margin-top: 2px; }
|
||
|
||
/* ===== Info / Glossar ===== */
|
||
.farmer-info-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.farmer-info-card {
|
||
background: #f8f5ec; border: 1px solid #e2ddd1; border-radius: 6px;
|
||
padding: 10px; cursor: pointer;
|
||
transition: background 0.1s, border-color 0.1s;
|
||
min-height: 70px; display: flex; flex-direction: column; gap: 4px;
|
||
}
|
||
.farmer-info-card:active { background: #e8e2d2; }
|
||
.farmer-info-card:hover { border-color: #8b5a2b; }
|
||
.farmer-info-card-icon { font-size: 22px; line-height: 1; }
|
||
.farmer-info-card-title { font-size: 13px; font-weight: 800; color: #2a2a2a; }
|
||
.farmer-info-card-short { font-size: 11px; color: #6b6258; line-height: 1.35; }
|
||
.farmer-info-back {
|
||
background: #f1ede2; border: 1px solid #d4cfc0; border-radius: 6px;
|
||
padding: 6px 12px; font-size: 12px; font-weight: 600; color: #6b4f2a;
|
||
cursor: pointer; margin-bottom: 12px; font-family: inherit;
|
||
}
|
||
.farmer-info-back:active { background: #e8e2d2; }
|
||
.farmer-info-detail-head {
|
||
display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
|
||
}
|
||
.farmer-info-detail-icon { font-size: 36px; line-height: 1; }
|
||
.farmer-info-detail-title { font-size: 19px; font-weight: 800; color: #2a2a2a; }
|
||
.farmer-info-detail-text {
|
||
font-size: 14px; line-height: 1.55; color: #2a2a2a;
|
||
background: #f8f5ec; padding: 12px; border-radius: 6px;
|
||
}
|
||
.farmer-info-detail-easy {
|
||
background: #e8f0e8; border-left: 3px solid #4a7c4e;
|
||
padding: 10px 12px; margin-top: 10px; border-radius: 4px;
|
||
font-size: 13px; line-height: 1.5; color: #1f4b37;
|
||
}
|
||
.farmer-info-detail-easy::before {
|
||
content: '🟢 In leichter Sprache:'; display: block;
|
||
font-size: 11px; font-weight: 800; text-transform: uppercase;
|
||
letter-spacing: 0.04em; margin-bottom: 4px;
|
||
}
|
||
|
||
/* ===== Endbericht-Modal ===== */
|
||
.farmer-end-stars {
|
||
font-size: 38px; text-align: center; margin: 6px 0 4px;
|
||
letter-spacing: 6px; line-height: 1;
|
||
}
|
||
.farmer-end-score {
|
||
text-align: center; font-size: 14px; color: #6b4f2a;
|
||
margin-bottom: 14px;
|
||
}
|
||
.farmer-end-score strong {
|
||
font-size: 32px; color: #8b5a2b; font-weight: 900;
|
||
}
|
||
.farmer-end-stats {
|
||
display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
|
||
margin: 12px 0;
|
||
}
|
||
.farmer-end-stat {
|
||
background: #f8f5ec; padding: 10px; border-radius: 6px;
|
||
border-left: 3px solid #c8bfb0;
|
||
}
|
||
.farmer-end-stat.good { border-left-color: #1f7a3d; }
|
||
.farmer-end-stat.warn { border-left-color: #e8c547; }
|
||
.farmer-end-stat.bad { border-left-color: #d8453a; }
|
||
.farmer-end-stat-label {
|
||
font-size: 11px; color: #6b6258; text-transform: uppercase;
|
||
font-weight: 700; letter-spacing: 0.04em;
|
||
}
|
||
.farmer-end-stat-value {
|
||
font-size: 18px; font-weight: 800; color: #2a2a2a; margin-top: 2px;
|
||
}
|
||
.farmer-end-stat-meta {
|
||
font-size: 11px; color: #888; margin-top: 2px;
|
||
}
|
||
.farmer-end-tips {
|
||
background: #fff5dc; border: 1px solid #e8c547; border-radius: 6px;
|
||
padding: 10px 12px; margin-top: 12px;
|
||
}
|
||
.farmer-end-tips.green { background: #e8f0e8; border-color: #4a7c4e; }
|
||
.farmer-end-tips h3 { margin: 0 0 6px; font-size: 13px; color: #8b5a2b; }
|
||
.farmer-end-tips.green h3 { color: #1f4b37; }
|
||
.farmer-end-tips ul {
|
||
margin: 0; padding-left: 20px; font-size: 12px;
|
||
line-height: 1.55; color: #6b4f2a;
|
||
}
|
||
.farmer-end-tips li { margin: 2px 0; }
|
||
.farmer-end-submit-status {
|
||
font-size: 11px; color: #888; text-align: center; margin-top: 8px;
|
||
}
|
||
.farmer-end-submit-status.ok { color: #1f7a3d; }
|
||
.farmer-end-submit-status.err { color: #d8453a; }
|
||
|
||
@media (max-width: 720px) {
|
||
.farmer-side { width: 200px; }
|
||
.farmer-crop-desc { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="ggs-topbar ggs-header" data-ggs-sim="farmer" data-ggs-no-glossar data-ggs-home="/schueler" data-ggs-music="default">
|
||
<h1 class="ggs-header-module">Landwirtschaft</h1>
|
||
<span class="ggs-header-badge">Jahr <span id="year-num">1</span></span>
|
||
<div class="ggs-header-spacer"></div>
|
||
<div class="ggs-header-actions">
|
||
<a href="/schueler" title="Zurück zur Schülerseite">🏠</a>
|
||
<button id="audio-toggle" title="Ton an/aus" onclick="toggleAudio()">🔊</button>
|
||
<button id="btn-info" title="Anleitung nochmal zeigen" onclick="if(window.GGS_TUTORIAL&&typeof window.GGS_TUTORIAL.replay==='function')window.GGS_TUTORIAL.replay();">ℹ</button>
|
||
<button id="glossar-btn" title="Fachinfo / Glossar" onclick="openInfoModal()">📖</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="farmer-year-bar">
|
||
<span class="farmer-market" id="market-display" title="Markt-Auftrag dieses Jahres — alle Lieferungen gehen hierher">
|
||
<span class="farmer-market-label">Markt</span>
|
||
<span id="market-text">—</span>
|
||
</span>
|
||
<button class="farmer-harvest-btn" id="harvest-btn" disabled>Erntejahr starten</button>
|
||
</div>
|
||
|
||
<div class="farmer-kpi" id="kpi-panel">
|
||
<div class="farmer-kpi-title">📊 Bilanz im Verlauf</div>
|
||
<div class="farmer-kpi-row">
|
||
<div class="farmer-kpi-label">💰 Einnahmen</div>
|
||
<div class="farmer-kpi-value" id="kpi-money">0 €</div>
|
||
<div class="farmer-kpi-spark" id="kpi-money-spark"></div>
|
||
</div>
|
||
<div class="farmer-kpi-row">
|
||
<div class="farmer-kpi-label">🎯 Treffer-Quote (S1+S2)</div>
|
||
<div class="farmer-kpi-value" id="kpi-rate">— %</div>
|
||
<div class="farmer-kpi-spark" id="kpi-rate-spark"></div>
|
||
</div>
|
||
<div class="farmer-kpi-row">
|
||
<div class="farmer-kpi-label">🌫 CO₂-Fußabdruck</div>
|
||
<div class="farmer-kpi-value" id="kpi-co2">0 kg</div>
|
||
<div class="farmer-kpi-spark" id="kpi-co2-spark"></div>
|
||
</div>
|
||
<div class="farmer-kpi-row">
|
||
<div class="farmer-kpi-label">📦 Versuche · 🦠 Krankheiten</div>
|
||
<div class="farmer-kpi-value-small"><span id="kpi-attempts">0</span> · <span id="kpi-diseases">0</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="farmer-side">
|
||
<div class="farmer-picker" id="picker">
|
||
<div class="farmer-picker-title" id="picker-title">Anbau-Versuch</div>
|
||
<button class="farmer-undo-btn" id="undo-btn" disabled onclick="undoLastPlant()">↶ Letzte Pflanzung zurück</button>
|
||
</div>
|
||
|
||
<div class="farmer-report-panel" id="report-panel">
|
||
<div class="farmer-report-progress">
|
||
<span id="report-progress-text">1 / 1</span>
|
||
<span>Erntebericht</span>
|
||
</div>
|
||
<div class="farmer-report-body" id="report-body"></div>
|
||
<div class="farmer-report-actions">
|
||
<button class="farmer-report-btn" id="report-next-btn">Weiter</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="map"></div>
|
||
|
||
<div class="farmer-toast" id="toast"></div>
|
||
|
||
<!-- Welcome -->
|
||
<div class="ggs-modal-overlay" id="welcome-overlay">
|
||
<div class="ggs-modal">
|
||
<button class="ggs-modal-close" onclick="closeWelcome()" aria-label="Schließen">✕</button>
|
||
<h2>🌾 Willkommen zur Landwirtschafts-Simulation</h2>
|
||
<p>Du arbeitest auf einer Welt-Karte mit drei Sorten — jede mit eigener Einheit und realem Welthandelspreis:</p>
|
||
<div class="farmer-welcome-list">
|
||
<div><span>🌾</span><strong>Weizen</strong><small>1 Tonne · 250 €</small></div>
|
||
<div><span>🍌</span><strong>Banane</strong><small>1 Tonne · 750 € · verderblich</small></div>
|
||
<div><span>🦌</span><strong>Rentier</strong><small>1 Tier · 1500 € · Premium</small></div>
|
||
</div>
|
||
<p>
|
||
<strong>So arbeitest du:</strong><br>
|
||
1. Pro Jahr darfst du von jeder Sorte <strong>3 Stück</strong> irgendwo platzieren — auch im Wasser.<br>
|
||
2. Drücke <em>Erntejahr starten</em>, sobald du genug platziert hast.<br>
|
||
3. Die Berichte erscheinen rechts, eine nach der anderen — die Karte fliegt jeweils zur Pflanzung und zeigt die Eignungsfarbe.<br>
|
||
4. <strong>Markt-Auftrag</strong>: der aktuell aktive Welt-Hafen blinkt grün. Markt bleibt <strong>3 Jahre</strong> in derselben Region. <em>Lokal erzeugte Ware</em> (Konsumcity in < 2000 km) fährt direkt zur nächsten Stadt. <em>Importware</em> geht über den Hafen, von dort per LKW weiter zu Konsumcity.<br>
|
||
5. <strong>Verderbliche Ware</strong> (Banane) wird per <em>Reefer-Container</em> auf dem Schiff gekühlt verschickt — ~1,5× teurer als normales Schiff, aber überall hin praktikabel.<br>
|
||
6. <strong>Wirkungs-Kreise</strong>: jeder Anbau-Versuch hat einen Kreis um sich. Berühren sich zwei Kreise gleicher Sorte → <strong>Bodenmüdigkeit</strong> (Pflanzen) oder <strong>Überweidung</strong> (Tiere); bei 3+ verbundenen droht Krankheits-Ausbruch (Panama-Krankheit, Schwarzrost, Hufrehe, MERS …). Die Kreise <em>schrumpfen pro Jahr</em> und verschwinden nach 4 Jahren — dann ist der Standort erholt.<br>
|
||
7. Nach 3 Jahren wird eine <strong>weitere Sorte</strong> freigeschaltet — dafür sinkt die Anzahl Versuche pro Sorte.
|
||
</p>
|
||
<p style="font-size:12px;color:#666">
|
||
Eignungs-Farben (sichtbar nach Ernte): 🟢 sehr gut · 🟩 gut · 🟡 marginal · 🟠 schlecht · 🔴 unmöglich
|
||
</p>
|
||
<div style="font-size:11px;color:#777;background:#f8f5ec;padding:8px 10px;border-radius:6px;line-height:1.5;">
|
||
<strong>📊 Echte Werte:</strong> Frachtraten nach UNCTAD/Eurostat 2023
|
||
(LKW 0,07 €/t·km · Schiff 0,015 €/t·km · Flieger 1,5 €/t·km · Reefer +50 % Kosten und +100 % CO₂).
|
||
Klima-Eignung nach Köppen-Geiger 2018 + FAO GAEZ. CO₂-Faktoren nach
|
||
Umweltbundesamt 2022 (LKW 90 g/t·km · Schiff 15 g/t·km · Flieger 1 000 g/t·km).
|
||
Welthandelspreise: Weizen ~250 €/t · Bananen ~750 €/t (FOB) · Rentier ~1 500 €/Tier
|
||
· Kamel ~800 €/Tier.
|
||
</div>
|
||
<div class="ggs-modal-actions">
|
||
<button onclick="closeWelcome()">Los arbeiten</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Fachinfo / Glossar -->
|
||
<div class="ggs-modal-overlay" id="info-overlay">
|
||
<div class="ggs-modal" style="max-width:780px">
|
||
<button class="ggs-modal-close" onclick="closeInfoModal()" aria-label="Schließen">✕</button>
|
||
<h2 id="info-title">📚 Fachinfo & Glossar</h2>
|
||
<p id="info-intro" style="font-size:13px;color:#6b6258;margin:0 0 12px">Klicke auf einen Begriff für die ausführliche Erklärung.</p>
|
||
<div id="info-list" class="farmer-info-grid"></div>
|
||
<div id="info-detail" style="display:none">
|
||
<button class="farmer-info-back" onclick="closeInfoDetail()">← Zurück zur Liste</button>
|
||
<div id="info-detail-body"></div>
|
||
</div>
|
||
<div class="ggs-modal-actions">
|
||
<button onclick="closeInfoModal()">Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Endbericht: erscheint nach 10 abgeschlossenen Jahren (Auslöser: Jahr 11) -->
|
||
<div class="ggs-modal-overlay" id="end-report-overlay">
|
||
<div class="ggs-modal" style="max-width:720px">
|
||
<button class="ggs-modal-close" onclick="closeEndReport()" aria-label="Schließen">✕</button>
|
||
<h2>📋 Zwischenbericht — 10 Jahre Landwirtschaft</h2>
|
||
<div class="farmer-end-stars" id="end-stars">⭐⭐⭐</div>
|
||
<div class="farmer-end-score">Score: <strong id="end-score">0</strong> / 100</div>
|
||
<div class="farmer-end-stats" id="end-stats"></div>
|
||
<div class="farmer-end-tips" id="end-tips"></div>
|
||
<div class="farmer-end-submit-status" id="end-submit-status"></div>
|
||
<div class="ggs-modal-actions">
|
||
<button onclick="resetGame()" style="background:#888">🔄 Neu starten</button>
|
||
<button onclick="closeEndReport()">Weiter arbeiten</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||
<script src="engine.js"></script>
|
||
|
||
<script>
|
||
/*__FARMER_INJECTION__*/
|
||
|
||
// Karten-Provider: osm-de (FOSSGIS e.V.) — deutsche Beschriftung + Vegetations-
|
||
// Färbung (Wald grün, Wüste beige, Tundra ocker), unterstützt didaktisch das
|
||
// Klima-Verständnis. Alternative: 'carto' (heller, minimalistischer, englisch).
|
||
const TILE_URL = window.FARMER_TILE_PROXY
|
||
? window.FARMER_TILE_PROXY + '?p=osm-de&z={z}&x={x}&y={y}'
|
||
: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png';
|
||
const EASY = !!window.STUDENT_EASY;
|
||
|
||
function pickText(t) {
|
||
if (!t) return '';
|
||
if (typeof t === 'string') return t;
|
||
if (EASY && t.easy) return t.easy;
|
||
return t.de || '';
|
||
}
|
||
|
||
const CONTINENT_DE = {
|
||
'Europe': 'Europa',
|
||
'Asia': 'Asien',
|
||
'Africa': 'Afrika',
|
||
'North America': 'Nordamerika',
|
||
'South America': 'Südamerika',
|
||
'Oceania': 'Ozeanien',
|
||
'Antarctica': 'Antarktis',
|
||
'Seven seas (open ocean)': 'Offenes Meer',
|
||
};
|
||
function germanContinent(name) {
|
||
if (!name) return null;
|
||
return CONTINENT_DE[name] || name;
|
||
}
|
||
|
||
// CO2-Klasse pro Lieferung — Schwellen pragmatisch, keine offizielle Kategorie.
|
||
// Nur für Farb-Code im Bericht: grün=ok, gelb=mittel, rot=hoch.
|
||
function co2Class(kg) {
|
||
if (kg < 50) return 'co2-low';
|
||
if (kg < 200) return 'co2-mid';
|
||
return 'co2-high';
|
||
}
|
||
|
||
// ===== Audio-System =====
|
||
// Mute-State persistiert in localStorage. Sounds (ElevenLabs-generiert) +
|
||
// Hintergrundmusik (regional pro Markt) werden nur abgespielt wenn audioEnabled.
|
||
|
||
const SFX_BASE = (window.FARMER_BASE || './') + 'assets/sounds/sfx/';
|
||
const MUSIC_BASE = (window.FARMER_BASE || './') + 'assets/sounds/music/';
|
||
|
||
const MUSIC_TRACKS = {
|
||
hamburg: ['hamburg1', 'hamburg2'],
|
||
newyork: ['newyork1', 'newyork2'],
|
||
shanghai: ['shanghai1', 'shanghai2'],
|
||
saopaulo: ['saopaulo1', 'saopaulo2'],
|
||
dubai: ['dubai1', 'dubai2', 'dubai3', 'dubai4'],
|
||
};
|
||
|
||
let audioEnabled = (function () {
|
||
try {
|
||
const v = localStorage.getItem('farmer-audio');
|
||
return v === null ? true : v === '1';
|
||
} catch { return true; }
|
||
})();
|
||
let bgMusicAudio = null;
|
||
let lastMusicHubId = null;
|
||
const audioCache = {};
|
||
|
||
function preloadAudio(name) {
|
||
if (audioCache[name]) return audioCache[name];
|
||
const a = new Audio(SFX_BASE + name + '.mp3');
|
||
a.preload = 'auto';
|
||
audioCache[name] = a;
|
||
return a;
|
||
}
|
||
|
||
function preloadAllSounds() {
|
||
const names = [
|
||
'harvest_start','report_open','report_next',
|
||
'vehicle_truck','vehicle_ship','vehicle_plane','vehicle_arrive',
|
||
'disease_outbreak','monoculture_warn','year_advance','unlock_crop',
|
||
'negative_balance','end_report_open','reset',
|
||
'crop_wheat','crop_banana','crop_reindeer','crop_camel',
|
||
'crop_rice','crop_coffee','crop_dates','crop_corn',
|
||
'crop_fish','crop_olive',
|
||
];
|
||
names.forEach(n => preloadAudio(n));
|
||
}
|
||
|
||
// Pro-Sound Default-Volumes (Standard: 0.7). Schiffshupe ist deutlich
|
||
// präsenter als andere SFX, deshalb leiser.
|
||
const SOUND_VOLUMES = {
|
||
vehicle_ship: 0.35,
|
||
vehicle_plane: 0.55,
|
||
vehicle_truck: 0.55,
|
||
};
|
||
|
||
function playSound(name, opts) {
|
||
if (!audioEnabled) return;
|
||
try {
|
||
const cached = preloadAudio(name);
|
||
const a = cached.cloneNode();
|
||
a.volume = (opts && opts.volume !== undefined)
|
||
? opts.volume
|
||
: (SOUND_VOLUMES[name] !== undefined ? SOUND_VOLUMES[name] : 0.7);
|
||
a.play().catch(() => {});
|
||
} catch (err) {
|
||
console.log('[farmer:audio] failed for', name, err.message);
|
||
}
|
||
}
|
||
|
||
function toggleAudio() {
|
||
audioEnabled = !audioEnabled;
|
||
try { localStorage.setItem('farmer-audio', audioEnabled ? '1' : '0'); } catch {}
|
||
document.getElementById('audio-toggle').textContent = audioEnabled ? '🔊' : '🔇';
|
||
if (bgMusicAudio) {
|
||
if (audioEnabled) bgMusicAudio.play().catch(() => {});
|
||
else bgMusicAudio.pause();
|
||
}
|
||
}
|
||
|
||
function startBackgroundMusic() {
|
||
updateMusicForCurrentMarket();
|
||
}
|
||
|
||
/** Wechselt Musik wenn nötig (anderer Markt-Hub als aktuell läuft). */
|
||
function updateMusicForCurrentMarket() {
|
||
if (!game) return;
|
||
const hub = window.FarmerEngine.getMarketHubForYear(game.year);
|
||
if (!hub) return;
|
||
if (lastMusicHubId === hub.id) return; // selbe Region — Track läuft weiter
|
||
|
||
const tracks = MUSIC_TRACKS[hub.id] || [];
|
||
if (tracks.length === 0) return;
|
||
const pick = tracks[Math.floor(Math.random() * tracks.length)];
|
||
|
||
if (bgMusicAudio) {
|
||
bgMusicAudio.pause();
|
||
bgMusicAudio.src = '';
|
||
}
|
||
bgMusicAudio = new Audio(MUSIC_BASE + pick + '.mp3');
|
||
bgMusicAudio.loop = true;
|
||
bgMusicAudio.volume = 0.20;
|
||
if (audioEnabled) {
|
||
bgMusicAudio.play().catch(err => {
|
||
console.log('[farmer:music] autoplay blocked — will start after user interaction');
|
||
});
|
||
}
|
||
lastMusicHubId = hub.id;
|
||
console.log('[farmer:music] now playing', pick + '.mp3', 'for', hub.id);
|
||
}
|
||
|
||
const map = L.map('map', {
|
||
center: [30, 15], zoom: 3, minZoom: 3, maxZoom: 6,
|
||
// Welt nicht horizontal wiederholen (sonst landen Klicks im "zweiten" Welt-Bild
|
||
// außerhalb der Country-Polygone und werden als Wasser erkannt). Bounds halten
|
||
// die Karte innerhalb einer einzigen Welt-Kachel.
|
||
worldCopyJump: false,
|
||
maxBounds: [[-85, -180], [85, 180]],
|
||
maxBoundsViscosity: 1.0,
|
||
});
|
||
L.tileLayer(TILE_URL, {
|
||
attribution: '© <a href="https://carto.com/attributions">CARTO</a> © OpenStreetMap',
|
||
maxZoom: 18,
|
||
noWrap: true,
|
||
bounds: [[-85, -180], [85, 180]],
|
||
}).addTo(map);
|
||
|
||
let game = null;
|
||
let selectedCrop = null;
|
||
let fieldMarkers = new Map(); // fieldId -> L.marker
|
||
let fieldCircles = new Map(); // fieldId -> L.circle (Bodenwirkungs-Kreis)
|
||
let hubMarkers = new Map(); // hubId -> L.marker (für Blink-Highlight)
|
||
let currentReports = [];
|
||
let currentReportIdx = 0;
|
||
let currentHighlightedId = null;
|
||
let startTime = Date.now(); // für Endbericht: gespielte Zeit
|
||
const END_REPORT_TRIGGER_YEAR = 11; // Endbericht-Modal nach 10 abgeschlossenen Jahren — passt zur 10 – 20-Min-Spielzeit. Schüler*in kann „Weiter arbeiten" wählen bis Jahr 22+ (alle Sorten unlocked).
|
||
|
||
// ===== Boot =====
|
||
(async function boot() {
|
||
try {
|
||
await window.FarmerEngine.loadData();
|
||
} catch (err) {
|
||
console.error('[farmer] Daten laden fehlgeschlagen', err);
|
||
showToast('Fehler beim Laden — Seite neu laden.');
|
||
return;
|
||
}
|
||
game = window.FarmerEngine.init(window.FARMER_FORCED_LEVEL || 'l1', Date.now());
|
||
startTime = Date.now();
|
||
|
||
for (const sel of ['.ggs-topbar', '.farmer-year-bar', '.farmer-side', '.farmer-kpi']) {
|
||
const el = document.querySelector(sel);
|
||
if (el) {
|
||
L.DomEvent.disableClickPropagation(el);
|
||
L.DomEvent.disableScrollPropagation(el);
|
||
}
|
||
}
|
||
|
||
drawHubs();
|
||
buildPicker();
|
||
selectCrop(window.FarmerEngine.getCrops()[0].id);
|
||
updateMoneyDisplay();
|
||
updateKpi();
|
||
updateMarketDisplay();
|
||
// Highlight nach kurzem Delay damit Hub-Marker im DOM sind
|
||
setTimeout(highlightActiveMarketHub, 100);
|
||
|
||
map.on('click', onMapClick);
|
||
document.getElementById('harvest-btn').addEventListener('click', onHarvest);
|
||
document.getElementById('report-next-btn').addEventListener('click', onReportNext);
|
||
document.getElementById('welcome-overlay').classList.add('show');
|
||
|
||
// Audio-Toggle initial setzen, Sounds preloaden, Hintergrundmusik starten
|
||
document.getElementById('audio-toggle').textContent = audioEnabled ? '🔊' : '🔇';
|
||
preloadAllSounds();
|
||
startBackgroundMusic();
|
||
})();
|
||
|
||
// ===== Hubs =====
|
||
function drawHubs() {
|
||
for (const hub of window.FarmerEngine.getHubs()) {
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div class="farmer-hub-icon" data-hub-id="${hub.id}" title="${pickText(hub.title)}">${hub.icon}</div>`,
|
||
iconSize: [28, 28], iconAnchor: [14, 14],
|
||
});
|
||
const m = L.marker([hub.lat, hub.lon], { icon, interactive: true })
|
||
.addTo(map)
|
||
.bindTooltip('🏛 Welt-Hafen: ' + pickText(hub.title), { direction: 'top' });
|
||
hubMarkers.set(hub.id, m);
|
||
}
|
||
}
|
||
|
||
function highlightActiveMarketHub() {
|
||
const activeHub = window.FarmerEngine.getMarketHubForYear(game.year);
|
||
if (!activeHub) return;
|
||
for (const [id, marker] of hubMarkers) {
|
||
const el = marker.getElement();
|
||
if (!el) continue;
|
||
const inner = el.querySelector('.farmer-hub-icon');
|
||
if (!inner) continue;
|
||
inner.classList.toggle('active', id === activeHub.id);
|
||
}
|
||
}
|
||
|
||
// ===== Picker =====
|
||
function buildPicker() {
|
||
const picker = document.getElementById('picker');
|
||
// Existierende Cards entfernen — idempotent, lässt den Title intakt
|
||
picker.querySelectorAll('.farmer-crop-card').forEach(el => el.remove());
|
||
document.getElementById('picker-title').textContent = EASY ? 'Was anbauen?' : 'Anbau-Versuch';
|
||
const unlocked = window.FarmerEngine.getUnlockedCrops(game.year);
|
||
const perCrop = window.FarmerEngine.counterPerCropForYear(game.year);
|
||
for (const crop of unlocked) {
|
||
const card = document.createElement('div');
|
||
card.className = 'farmer-crop-card';
|
||
card.dataset.cropId = crop.id;
|
||
card.innerHTML = `
|
||
<div class="farmer-crop-icon">${crop.icon}</div>
|
||
<div class="farmer-crop-info">
|
||
<div class="farmer-crop-name">${pickText(crop.title)}</div>
|
||
<div class="farmer-crop-desc">${pickText(crop.unit.label)}: ${crop.yieldByClass.S1} €</div>
|
||
</div>
|
||
<div class="farmer-crop-counter" data-counter="${crop.id}">${game.counters[crop.id] || 0}</div>
|
||
`;
|
||
card.addEventListener('click', () => {
|
||
if (game.counters[crop.id] > 0) selectCrop(crop.id);
|
||
});
|
||
picker.appendChild(card);
|
||
}
|
||
refreshPicker();
|
||
}
|
||
|
||
function refreshPicker() {
|
||
for (const card of document.querySelectorAll('.farmer-crop-card')) {
|
||
const id = card.dataset.cropId;
|
||
const left = game.counters[id] || 0;
|
||
const counterEl = card.querySelector(`[data-counter="${id}"]`);
|
||
if (counterEl) counterEl.textContent = left;
|
||
card.classList.toggle('disabled', left <= 0);
|
||
card.classList.toggle('active', id === selectedCrop && left > 0);
|
||
}
|
||
updateUndoButton();
|
||
}
|
||
|
||
function updateUndoButton() {
|
||
const btn = document.getElementById('undo-btn');
|
||
if (!btn) return;
|
||
const has = game.fields.some(f => f.status === 'planted' && f.plantedYear === game.year);
|
||
btn.disabled = !has;
|
||
}
|
||
|
||
/** Nimmt die letzte Pflanzung im aktuellen Jahr zurück (Counter zurück, Marker weg). */
|
||
function undoLastPlant() {
|
||
// Suche von hinten: erstes Feld mit status='planted' und plantedYear=game.year
|
||
let idx = -1;
|
||
for (let i = game.fields.length - 1; i >= 0; i--) {
|
||
const f = game.fields[i];
|
||
if (f.status === 'planted' && f.plantedYear === game.year) { idx = i; break; }
|
||
}
|
||
if (idx === -1) return;
|
||
const field = game.fields[idx];
|
||
// Counter wieder erhöhen
|
||
game.counters[field.cropId] = (game.counters[field.cropId] || 0) + 1;
|
||
// Field aus Liste entfernen
|
||
game.fields.splice(idx, 1);
|
||
// Marker + Kreis weg
|
||
const m = fieldMarkers.get(field.id);
|
||
if (m) { map.removeLayer(m); fieldMarkers.delete(field.id); }
|
||
const c = fieldCircles.get(field.id);
|
||
if (c) { map.removeLayer(c); fieldCircles.delete(field.id); }
|
||
// Falls die zurückgenommene Sorte gerade selektiert war → Picker refresh,
|
||
// Auswahl bleibt aber gültig (Counter ist jetzt > 0)
|
||
selectCrop(field.cropId);
|
||
refreshPicker();
|
||
// Wenn keine Pflanzungen mehr → Erntejahr-Btn deaktivieren
|
||
const stillPlanted = game.fields.some(f => f.status === 'planted');
|
||
if (!stillPlanted) document.getElementById('harvest-btn').disabled = true;
|
||
playSound('reset', { volume: 0.4 });
|
||
}
|
||
|
||
function selectCrop(cropId) {
|
||
selectedCrop = cropId;
|
||
refreshPicker();
|
||
}
|
||
|
||
// ===== Karten-Klick =====
|
||
function onMapClick(e) {
|
||
if (currentReports.length > 0) return; // während Bericht-Modus nichts pflanzen
|
||
if (!selectedCrop) return;
|
||
if (!window.FarmerEngine.canPlant(game, selectedCrop)) {
|
||
showToast(EASY ? 'Keine Versuche mehr.' : 'Keine Versuche mehr — wähle eine andere Sorte oder starte das Erntejahr.');
|
||
return;
|
||
}
|
||
const field = window.FarmerEngine.plant(game, selectedCrop, e.latlng);
|
||
if (!field) return;
|
||
addOrUpdateFieldMarker(field);
|
||
refreshPicker();
|
||
// Per-Sorten-Sound: Tier-Geräusch oder Pflanzen-Rascheln
|
||
playSound('crop_' + selectedCrop);
|
||
document.getElementById('harvest-btn').disabled = false;
|
||
|
||
// 🔬 Cluster-Warnung: ist das frisch gepflanzte Feld jetzt Teil eines
|
||
// Clusters ≥ 3? Dann Krankheits-Risiko deutlich machen (vorher unsichtbar).
|
||
try {
|
||
const mono = window.FarmerEngine.getMonoculture();
|
||
const allFields = (game.fields || []);
|
||
const clusters = window.FarmerEngine.findClusters(allFields, game.year);
|
||
const myCluster = clusters.find(c => c.fields.some(f => f.id === field.id));
|
||
if (myCluster && mono && myCluster.fields.length >= mono.outbreakThreshold) {
|
||
const cropName = pickText(window.FarmerEngine.getCrops().find(c => c.id === selectedCrop).title);
|
||
const pct = Math.round(mono.outbreakChance * 100);
|
||
showToast(`⚠ Monokultur: ${myCluster.fields.length} ${cropName}-Felder im Umkreis. Krankheitsrisiko: ${pct} % pro Jahr.`);
|
||
}
|
||
} catch (e) { /* nicht kritisch */ }
|
||
|
||
// 🔄 Sortenumschlag: war das mein letzter Versuch dieser Sorte?
|
||
// Dann automatisch auf die nächste verfügbare wechseln + sichtbar machen.
|
||
if ((game.counters[selectedCrop] || 0) <= 0) {
|
||
const unlocked = window.FarmerEngine.getUnlockedCrops(game.year);
|
||
const nextCrop = unlocked.find(c => (game.counters[c.id] || 0) > 0);
|
||
if (nextCrop && nextCrop.id !== selectedCrop) {
|
||
const nextName = pickText(nextCrop.title);
|
||
selectCrop(nextCrop.id);
|
||
setTimeout(() => showToast(`👉 Sorte gewechselt: jetzt aktiv ist ${nextCrop.icon} ${nextName}.`), 600);
|
||
}
|
||
}
|
||
if (game.counters[selectedCrop] <= 0) {
|
||
const others = window.FarmerEngine.getCrops().filter(c => game.counters[c.id] > 0);
|
||
if (others.length > 0) {
|
||
const names = others.map(c => c.icon + ' ' + pickText(c.title)).join(' / ');
|
||
showToast(EASY ? 'Andere wählen: ' + names : 'Keine Versuche mehr für diese Sorte — wähle: ' + names);
|
||
} else {
|
||
showToast(EASY ? 'Erntejahr starten!' : 'Alle Versuche verbraucht — Erntejahr starten.');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== Field-Marker + Bodenwirkungs-Kreis =====
|
||
|
||
/** Marker-Größen-Skala. Während Wirkungs-Kreis aktiv (≤ RECOVERY_YEARS) ist
|
||
* der Marker volle Größe. Danach schrumpft er linear über FADE_YEARS auf
|
||
* MIN_SCALE — bleibt aber erkennbar (Emoji + Eignungsfarbe sichtbar). */
|
||
function getMarkerScale(field) {
|
||
const RECOVERY = window.FarmerEngine.RECOVERY_YEARS || 4;
|
||
const FADE_YEARS = 8;
|
||
const MIN_SCALE = 0.32; // ~13 px bei Vollgröße 40 px — Banane noch erkennbar
|
||
const age = game.year - field.plantedYear;
|
||
if (age <= RECOVERY) return 1.0;
|
||
const t = Math.min(1, (age - RECOVERY) / FADE_YEARS);
|
||
return 1.0 - t * (1.0 - MIN_SCALE);
|
||
}
|
||
|
||
function addOrUpdateFieldMarker(field) {
|
||
// --- Marker (Emoji mit Ring/Füllung, schrumpft mit dem Alter) ---
|
||
const existing = fieldMarkers.get(field.id);
|
||
if (existing) map.removeLayer(existing);
|
||
|
||
const crop = window.FarmerEngine.getCrops().find(c => c.id === field.cropId);
|
||
const colors = window.FarmerEngine.getColors();
|
||
const scale = getMarkerScale(field);
|
||
const size = Math.round(40 * scale);
|
||
const fontSize = Math.max(10, Math.round(22 * scale));
|
||
const borderW = Math.max(1, Math.round(scale > 0.7 ? 3 : 2));
|
||
|
||
let bgStyle = 'background:rgba(255, 255, 255, 0.85)';
|
||
let borderStyle = `border:${borderW}px solid rgba(139,90,43,0.4)`;
|
||
if (field.status === 'harvested' && field.lastReport) {
|
||
const c = colors[field.lastReport.suitability];
|
||
if (field.lastReport.year < game.year) {
|
||
bgStyle = `background:${c.hex}d0`;
|
||
borderStyle = `border:${borderW}px solid ${c.hex}; box-shadow: 0 0 0 ${Math.max(1, Math.round(2*scale))}px #fff`;
|
||
} else {
|
||
borderStyle = `border:${Math.max(2, Math.round(4*scale))}px solid ${c.hex}; box-shadow: 0 0 0 2px #fff, 0 0 6px ${c.hex}aa`;
|
||
}
|
||
}
|
||
const html = `<div class="farmer-field-icon" data-field-id="${field.id}" style="${bgStyle}; ${borderStyle}; width:${size}px; height:${size}px;"><span style="font-size:${fontSize}px">${crop.icon}</span></div>`;
|
||
const icon = L.divIcon({ className: '', html, iconSize: [size, size], iconAnchor: [size/2, size/2] });
|
||
const marker = L.marker([field.latlng.lat, field.latlng.lon], { icon }).addTo(map);
|
||
fieldMarkers.set(field.id, marker);
|
||
|
||
// --- Bodenwirkungs-Kreis (schrumpft pro Jahr, Berührung = Risiko) ---
|
||
addOrUpdateFieldCircle(field);
|
||
}
|
||
|
||
function addOrUpdateFieldCircle(field) {
|
||
const old = fieldCircles.get(field.id);
|
||
if (old) { map.removeLayer(old); fieldCircles.delete(field.id); }
|
||
|
||
const radiusKm = window.FarmerEngine.getFieldImpactRadiusKm(field, game.year);
|
||
if (radiusKm <= 0) return; // Boden vollständig erholt → kein Kreis
|
||
|
||
const colors = window.FarmerEngine.getColors();
|
||
let color = '#8b5a2b'; // braun für noch nicht geerntet
|
||
if (field.lastReport) {
|
||
color = colors[field.lastReport.suitability].hex;
|
||
}
|
||
const circle = L.circle([field.latlng.lat, field.latlng.lon], {
|
||
radius: radiusKm * 1000, // Leaflet erwartet Meter
|
||
color,
|
||
weight: 1,
|
||
opacity: 0.5,
|
||
fillColor: color,
|
||
fillOpacity: 0.10,
|
||
interactive: false,
|
||
}).addTo(map);
|
||
circle.bringToBack();
|
||
fieldCircles.set(field.id, circle);
|
||
}
|
||
|
||
function rerenderAllFieldMarkers() {
|
||
for (const field of game.fields) addOrUpdateFieldMarker(field);
|
||
// Verwaiste Kreise (Felder weg) entfernen — nicht der Fall hier, aber sauber
|
||
for (const [id, c] of fieldCircles) {
|
||
if (!game.fields.find(f => f.id === id)) {
|
||
map.removeLayer(c);
|
||
fieldCircles.delete(id);
|
||
}
|
||
}
|
||
}
|
||
|
||
function highlightFieldMarker(fieldId) {
|
||
if (currentHighlightedId === fieldId) return;
|
||
// Vorigen entfernen
|
||
if (currentHighlightedId) {
|
||
const prev = fieldMarkers.get(currentHighlightedId);
|
||
if (prev && prev.getElement()) {
|
||
const inner = prev.getElement().querySelector('.farmer-field-icon');
|
||
if (inner) inner.classList.remove('current');
|
||
}
|
||
}
|
||
// Neuen setzen
|
||
const m = fieldMarkers.get(fieldId);
|
||
if (m && m.getElement()) {
|
||
const inner = m.getElement().querySelector('.farmer-field-icon');
|
||
if (inner) inner.classList.add('current');
|
||
}
|
||
currentHighlightedId = fieldId;
|
||
}
|
||
|
||
function clearFieldHighlight() {
|
||
if (!currentHighlightedId) return;
|
||
const prev = fieldMarkers.get(currentHighlightedId);
|
||
if (prev && prev.getElement()) {
|
||
const inner = prev.getElement().querySelector('.farmer-field-icon');
|
||
if (inner) inner.classList.remove('current');
|
||
}
|
||
currentHighlightedId = null;
|
||
}
|
||
|
||
// ===== Erntejahr → Bericht-Sequenz =====
|
||
function onHarvest() {
|
||
const reports = window.FarmerEngine.harvestYear(game);
|
||
if (reports.length === 0) return;
|
||
rerenderAllFieldMarkers(); // Ringe sichtbar bevor erste Card kommt
|
||
currentReports = reports;
|
||
currentReportIdx = 0;
|
||
document.getElementById('harvest-btn').disabled = true;
|
||
showPicker(false);
|
||
playSound('harvest_start');
|
||
showReportCard(0);
|
||
}
|
||
|
||
function showReportCard(idx) {
|
||
const r = currentReports[idx];
|
||
const colors = window.FarmerEngine.getColors();
|
||
const c = colors[r.suitability];
|
||
|
||
document.getElementById('report-progress-text').textContent =
|
||
(idx + 1) + ' / ' + currentReports.length;
|
||
|
||
// Place-Zeile: Land + Klimazone (oder Wasser)
|
||
let placeHtml;
|
||
if (r.isWater) {
|
||
placeHtml = `🌊 <strong>Im Wasser</strong> — kein Land in der Nähe.`;
|
||
} else {
|
||
const cont = r.continent ? ` (${r.continent})` : '';
|
||
placeHtml = `📍 <strong>${r.countryName}</strong>${cont}<br>Klimazone: ${pickText(r.zoneName)}`;
|
||
}
|
||
|
||
// Trade-Zeilen
|
||
let tradeHtml;
|
||
if (r.transport && r.gross > 0) {
|
||
const netClass = r.net >= 0 ? 'positive' : 'negative';
|
||
const sign = r.net >= 0 ? '+' : '';
|
||
|
||
// Importpreis-Abschlag (cross-continent): zwischen Brutto und Transport
|
||
let importLine = '';
|
||
if (r.importPenaltyAmount > 0) {
|
||
const fromC = germanContinent(r.continent) || 'Wasser';
|
||
const toC = germanContinent(r.marketContinent) || '?';
|
||
const pct = Math.round(r.importPenalty * 100);
|
||
importLine = `
|
||
<div class="farmer-trade-line">
|
||
<span>🌍</span>
|
||
<span>Importpreis-Abschlag (${fromC} → ${toC}, −${pct} %)</span>
|
||
<span class="amount" style="color:#a02a20">−${r.importPenaltyAmount} €</span>
|
||
</div>`;
|
||
}
|
||
|
||
// Brutto vor Importpreis-Abschlag (für klare Anzeige)
|
||
const grossBeforeImport = r.gross + r.importPenaltyAmount;
|
||
|
||
tradeHtml = `
|
||
<div class="farmer-trade-line">
|
||
<span>📦</span>
|
||
<span>${pickText(r.cropUnit.label)} geerntet</span>
|
||
<span class="amount">+${grossBeforeImport} €</span>
|
||
</div>
|
||
${importLine}
|
||
<div class="farmer-trade-line">
|
||
<span class="vehicle">${r.transport.mode.icon}</span>
|
||
<span>${pickText(r.transport.mode.label)}${r.transport.reefer ? ' (Reefer-Kühlung)' : ''} → ${
|
||
r.isRegional && r.destination && r.destination.name
|
||
? `<strong>${r.destination.name}</strong> <small style="opacity:0.7">(${pickText(r.hub.title)}-Region)</small>`
|
||
: `${pickText(r.hub.title)}`
|
||
} (${r.distanceKm.toLocaleString('de-DE')} km)</span>
|
||
<span class="amount" style="color:#a02a20">−${r.transport.cost} €</span>
|
||
</div>
|
||
${r.transport.co2kg ? `
|
||
<div class="farmer-trade-line co2 ${co2Class(r.transport.co2kg)}">
|
||
<span>🌫</span>
|
||
<span>CO₂-Fußabdruck Transport${r.transport.reefer ? ' inkl. Kühlung' : ''}</span>
|
||
<span class="amount">${r.transport.co2kg.toLocaleString('de-DE')} kg</span>
|
||
</div>` : ''}
|
||
<div class="farmer-trade-line net">
|
||
<span>💰</span>
|
||
<span>Netto-Bilanz</span>
|
||
<span class="amount ${netClass}">${sign}${r.net} €</span>
|
||
</div>
|
||
`;
|
||
} else {
|
||
tradeHtml = `<div class="farmer-trade-line totalverlust">Nichts zu liefern — Totalverlust.</div>`;
|
||
}
|
||
|
||
// Krankheits-Box: erscheint bei Ausbruch, ersetzt visuell den Fokus
|
||
let diseaseBox = '';
|
||
if (r.disease) {
|
||
diseaseBox = `<div class="farmer-disease-box">${r.narrative}</div>`;
|
||
}
|
||
|
||
// Bodenmüdigkeits-Hinweis: zeigt Reduktion durch Monokultur (wenn nicht Krankheit)
|
||
// Per-Typ-Wording: Pflanze → Felder/Bodenmüdigkeit, Tier → Herden/Überweidung.
|
||
let mudBox = '';
|
||
if (!r.disease && r.mudPenalty > 0 && r.gross > 0) {
|
||
const pct = Math.round(r.mudPenalty * 100);
|
||
const isAnimal = r.cropType === 'animal';
|
||
const icon = isAnimal ? '🐾' : '🌱';
|
||
const title = isAnimal ? 'Überweidung' : 'Bodenmüdigkeit';
|
||
const what = isAnimal ? 'Herden' : 'Felder';
|
||
const cause = isAnimal ? 'Zu dichte Konzentration der Herden' : 'Monokultur';
|
||
mudBox = `<div class="farmer-mud-box">${icon} <strong>${title} −${pct} %</strong> wegen ${r.neighborCount} naher ${what} gleicher Sorte. Brutto reduziert von ${r.baseGross} € auf ${r.gross} €. ${cause} drückt den Ertrag.</div>`;
|
||
}
|
||
|
||
// Falls Krankheit: Diagnose-Text ist im diseaseBox; sonst normaler Narrative
|
||
const narrativeHtml = r.disease ? '' : `<div class="farmer-report-narrative">${r.narrative}</div>`;
|
||
|
||
document.getElementById('report-body').innerHTML = `
|
||
<div class="farmer-report-headline">
|
||
<div class="farmer-report-icon-big">${r.cropIcon}</div>
|
||
<div class="farmer-report-headline-text">
|
||
<div class="farmer-report-crop-name">${pickText(r.cropTitle)}</div>
|
||
<div class="farmer-report-suitability" style="background:${c.hex}">${pickText(c.label)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="farmer-report-place">${placeHtml}</div>
|
||
${narrativeHtml}
|
||
${diseaseBox}
|
||
${mudBox}
|
||
<div class="farmer-report-trade">${tradeHtml}</div>
|
||
`;
|
||
|
||
document.getElementById('report-next-btn').textContent =
|
||
(idx === currentReports.length - 1)
|
||
? (EASY ? 'Transport starten' : 'Transport starten →')
|
||
: (EASY ? 'Weiter' : 'Weiter →');
|
||
|
||
showReportPanel(true);
|
||
|
||
// Sound je nach Bericht-Typ
|
||
if (r.disease) playSound('disease_outbreak');
|
||
else if (r.mudPenalty > 0.3) playSound('monoculture_warn');
|
||
else playSound('report_open');
|
||
|
||
// Karte fliegen + Marker hervorheben
|
||
map.flyTo([r.latlng.lat, r.latlng.lon], Math.max(map.getZoom(), 4), { duration: 0.7 });
|
||
setTimeout(() => highlightFieldMarker(r.fieldId), 50);
|
||
}
|
||
|
||
async function onReportNext() {
|
||
currentReportIdx++;
|
||
if (currentReportIdx < currentReports.length) {
|
||
playSound('report_next');
|
||
showReportCard(currentReportIdx);
|
||
} else {
|
||
// Letzte Karte → Welt-Übersicht (fitBounds aller Felder + Hub) + Animation + nextYear
|
||
clearFieldHighlight();
|
||
showReportPanel(false);
|
||
// Sammle alle Punkte (Reports + Hub) und fitte die Karte darauf — so sind
|
||
// alle Vehicle-Bewegungen sichtbar, egal in welcher Hemisphäre.
|
||
const points = currentReports.map(r => [r.latlng.lat, r.latlng.lon]);
|
||
// Auch alle Ziele (Hub und/oder Konsumcity) einbeziehen, damit nichts abgeschnitten ist
|
||
for (const r of currentReports) {
|
||
if (r.destination) points.push([r.destination.lat, r.destination.lon]);
|
||
else if (r.hub) points.push([r.hub.lat, r.hub.lon]);
|
||
}
|
||
if (points.length > 0) {
|
||
const bounds = L.latLngBounds(points).pad(0.4);
|
||
map.flyToBounds(bounds, { duration: 0.6, maxZoom: 3 });
|
||
}
|
||
await new Promise(r => setTimeout(r, 700));
|
||
await animateTrades(currentReports);
|
||
window.FarmerEngine.nextYear(game);
|
||
playSound('year_advance');
|
||
document.getElementById('year-num').textContent = game.year;
|
||
currentReports = [];
|
||
currentReportIdx = 0;
|
||
rerenderAllFieldMarkers(); // gefüllte Marker + geschrumpfte Kreise
|
||
updateMarketDisplay(); // Markt-Hub des neuen Jahres
|
||
highlightActiveMarketHub(); // Blink ggf. anderen Hub
|
||
|
||
// Endbericht-Trigger: nach 10 abgeschlossenen Jahren (Beginn Jahr 11).
|
||
// Schüler:in kann „Weiter arbeiten" wählen — der Bericht erscheint nur einmal.
|
||
if (game.year >= END_REPORT_TRIGGER_YEAR && !game.endReportShown) {
|
||
game.endReportShown = true;
|
||
setTimeout(showEndReport, 1200);
|
||
}
|
||
|
||
// Neue Sorten-Freischaltung?
|
||
const newCrops = window.FarmerEngine.newlyUnlockedThisYear(game.year);
|
||
if (newCrops.length > 0) {
|
||
const names = newCrops.map(c => c.icon + ' ' + pickText(c.title)).join(', ');
|
||
const perCrop = window.FarmerEngine.counterPerCropForYear(game.year);
|
||
showToast(EASY
|
||
? '🎉 Neu: ' + names
|
||
: '🎉 Neue Sorte: ' + names + ' — Versuche pro Sorte sinken auf ' + perCrop);
|
||
playSound('unlock_crop');
|
||
}
|
||
|
||
buildPicker(); // ggf. neue Card hinzu
|
||
const unlocked = window.FarmerEngine.getUnlockedCrops(game.year);
|
||
const firstAvail = unlocked.find(c => game.counters[c.id] > 0);
|
||
if (firstAvail) selectCrop(firstAvail.id);
|
||
refreshPicker();
|
||
showPicker(true);
|
||
}
|
||
}
|
||
|
||
function showPicker(show) {
|
||
document.getElementById('picker').classList.toggle('hidden', !show);
|
||
}
|
||
function showReportPanel(show) {
|
||
document.getElementById('report-panel').classList.toggle('show', show);
|
||
}
|
||
|
||
function closeWelcome() {
|
||
document.getElementById('welcome-overlay').classList.remove('show');
|
||
// Erste User-Interaction → Browser-Autoplay-Block aufheben
|
||
if (audioEnabled && bgMusicAudio && bgMusicAudio.paused) {
|
||
bgMusicAudio.play().catch(() => {});
|
||
}
|
||
}
|
||
|
||
// ===== Fachinfo / Glossar =====
|
||
let infoTopics = null;
|
||
|
||
async function openInfoModal() {
|
||
if (!infoTopics) {
|
||
try {
|
||
const r = await fetch((window.FARMER_BASE || './') + 'assets/data/info-topics.json?v=' + (window.FarmerEngine && window.FarmerEngine.DATA_VERSION || '1'));
|
||
const j = await r.json();
|
||
infoTopics = j.topics;
|
||
} catch (err) {
|
||
console.warn('[farmer:info] load fehlgeschlagen', err);
|
||
showToast('Fachinfo konnte nicht geladen werden.');
|
||
return;
|
||
}
|
||
}
|
||
renderInfoList();
|
||
closeInfoDetail();
|
||
document.getElementById('info-overlay').classList.add('show');
|
||
playSound('report_open', { volume: 0.4 });
|
||
}
|
||
|
||
function closeInfoModal() {
|
||
document.getElementById('info-overlay').classList.remove('show');
|
||
}
|
||
|
||
function renderInfoList() {
|
||
const list = document.getElementById('info-list');
|
||
list.innerHTML = '';
|
||
for (const t of infoTopics) {
|
||
const card = document.createElement('div');
|
||
card.className = 'farmer-info-card';
|
||
card.innerHTML =
|
||
'<div class="farmer-info-card-icon">' + t.icon + '</div>' +
|
||
'<div class="farmer-info-card-title">' + pickText(t.title) + '</div>' +
|
||
'<div class="farmer-info-card-short">' + pickText(t.short) + '</div>';
|
||
card.addEventListener('click', () => showInfoDetail(t.id));
|
||
list.appendChild(card);
|
||
}
|
||
}
|
||
|
||
function showInfoDetail(id) {
|
||
const t = infoTopics.find(x => x.id === id);
|
||
if (!t) return;
|
||
document.getElementById('info-list').style.display = 'none';
|
||
document.getElementById('info-detail').style.display = '';
|
||
document.getElementById('info-intro').style.display = 'none';
|
||
document.getElementById('info-detail-body').innerHTML =
|
||
'<div class="farmer-info-detail-head">' +
|
||
'<div class="farmer-info-detail-icon">' + t.icon + '</div>' +
|
||
'<div class="farmer-info-detail-title">' + pickText(t.title) + '</div>' +
|
||
'</div>' +
|
||
'<div class="farmer-info-detail-text">' + (t.text.de || '') + '</div>' +
|
||
'<div class="farmer-info-detail-easy">' + (t.text.easy || '') + '</div>';
|
||
playSound('report_next', { volume: 0.5 });
|
||
}
|
||
|
||
function closeInfoDetail() {
|
||
document.getElementById('info-list').style.display = '';
|
||
document.getElementById('info-detail').style.display = 'none';
|
||
document.getElementById('info-intro').style.display = '';
|
||
}
|
||
|
||
// ===== Endbericht =====
|
||
|
||
/** Score-Berechnung: gewichtete Komposition aus 6 Kategorien.
|
||
* Money 25 % · HitRate 20 % · CO2 15 % · Disease 10 % · Crops 10 % · Country 10 % · Climate 10 %. */
|
||
function computeFinalScore() {
|
||
const reports = game.reports;
|
||
const total = reports.length;
|
||
|
||
const moneyScore = Math.max(0, Math.min(100, (game.money / 30000) * 100));
|
||
|
||
const hits = reports.filter(r => r.suitability === 'S1' || r.suitability === 'S2').length;
|
||
const hitRatePct = total > 0 ? (hits / total) * 100 : 0;
|
||
|
||
const totalCo2 = reports.reduce((s, r) => s + (r.transport ? r.transport.co2kg : 0), 0);
|
||
const yearsPlayed = Math.max(1, game.year - 1);
|
||
const co2PerYear = totalCo2 / yearsPlayed;
|
||
const co2Score = Math.max(0, Math.min(100, 100 - (co2PerYear - 1000) / 40)); // 1000 → 100, 5000 → 0
|
||
|
||
const diseases = reports.filter(r => r.disease).length;
|
||
const diseaseScore = Math.max(0, 100 - diseases * 20);
|
||
|
||
const cropsUsed = new Set(reports.map(r => r.cropId)).size;
|
||
const cropScore = Math.min(100, cropsUsed / 8 * 100);
|
||
|
||
const countriesUsed = new Set(reports.filter(r => r.countryName).map(r => r.countryName)).size;
|
||
const countryScore = Math.min(100, countriesUsed / 10 * 100);
|
||
|
||
const climatesUsed = new Set(reports.map(r => r.zoneId)).size;
|
||
const climateScore = Math.min(100, climatesUsed / 6 * 100);
|
||
|
||
const totalScore = Math.round(
|
||
moneyScore * 0.25 +
|
||
hitRatePct * 0.20 +
|
||
co2Score * 0.15 +
|
||
diseaseScore * 0.10 +
|
||
cropScore * 0.10 +
|
||
countryScore * 0.10 +
|
||
climateScore * 0.10
|
||
);
|
||
|
||
let stars = 1;
|
||
if (totalScore >= 30) stars = 2;
|
||
if (totalScore >= 50) stars = 3;
|
||
if (totalScore >= 70) stars = 4;
|
||
if (totalScore >= 85) stars = 5;
|
||
|
||
return {
|
||
stars, score: totalScore,
|
||
breakdown: {
|
||
money: { value: game.money, score: Math.round(moneyScore) },
|
||
hitRate: { value: Math.round(hitRatePct), score: Math.round(hitRatePct) },
|
||
co2: { value: totalCo2, perYear: Math.round(co2PerYear), score: Math.round(co2Score) },
|
||
diseases: { value: diseases, score: Math.round(diseaseScore) },
|
||
crops: { value: cropsUsed, score: Math.round(cropScore) },
|
||
countries: { value: countriesUsed, score: Math.round(countryScore) },
|
||
climates: { value: climatesUsed, score: Math.round(climateScore) },
|
||
},
|
||
};
|
||
}
|
||
|
||
function statRating(score) {
|
||
if (score >= 70) return 'good';
|
||
if (score >= 40) return 'warn';
|
||
return 'bad';
|
||
}
|
||
|
||
function generateTips(b) {
|
||
const tips = [];
|
||
if (b.hitRate.value < 50) tips.push('🎯 Achte stärker auf passende Klimazonen — viele Versuche waren marginal oder ungeeignet (S3/N1/N2). Tipp: Klima-Reaktion abwarten und beim nächsten Anbau berücksichtigen.');
|
||
if (b.diseases.value > 2) tips.push('🦠 Vermeide Monokultur — du hattest ' + b.diseases.value + ' Krankheits-Ausbrüche. Pflanze gleiche Sorten so, dass sich die Wirkungs-Kreise nicht überlappen.');
|
||
if (b.co2.perYear > 3000) tips.push('🌫 Hoher CO₂-Fußabdruck (' + b.co2.perYear.toLocaleString('de-DE') + ' kg/Jahr). Bevorzuge regionale Anbaugebiete in der Nähe des aktuellen Markts — kürzere Transportwege.');
|
||
if (b.crops.value < 5) tips.push('🌱 Wenig Sortenvielfalt (' + b.crops.value + ' von 10 verfügbaren). Probiere alle freigeschalteten Sorten aus — Kamel/Reis/Datteln sind starke Spezialisten.');
|
||
if (b.countries.value < 5) tips.push('🌍 Wenige Länder genutzt (' + b.countries.value + '). Verteile deinen Anbau weltweit — verschiedene Hubs verlangen verschiedene Anbaugebiete.');
|
||
if (b.money.value < 5000) tips.push('💰 Schwache Bilanz (' + b.money.value.toLocaleString('de-DE') + ' €). Überprüfe ob du in passenden Klimazonen pflanzt und Transportkosten kontrollierst.');
|
||
return tips;
|
||
}
|
||
|
||
function showEndReport() {
|
||
const result = computeFinalScore();
|
||
const { stars, score, breakdown } = result;
|
||
|
||
document.getElementById('end-stars').textContent = '⭐'.repeat(stars) + '☆'.repeat(5 - stars);
|
||
document.getElementById('end-score').textContent = score;
|
||
|
||
const stat = (label, value, score, meta) =>
|
||
`<div class="farmer-end-stat ${statRating(score)}">
|
||
<div class="farmer-end-stat-label">${label}</div>
|
||
<div class="farmer-end-stat-value">${value}</div>
|
||
<div class="farmer-end-stat-meta">${meta} · ${score} / 100 Punkte</div>
|
||
</div>`;
|
||
|
||
document.getElementById('end-stats').innerHTML =
|
||
stat('💰 Einnahmen', breakdown.money.value.toLocaleString('de-DE') + ' €', breakdown.money.score, 'Gesamt') +
|
||
stat('🎯 Treffer-Quote', breakdown.hitRate.value + ' %', breakdown.hitRate.score, 'S1/S2-Anteil') +
|
||
stat('🌫 CO₂-Fußabdruck', breakdown.co2.value.toLocaleString('de-DE') + ' kg', breakdown.co2.score, breakdown.co2.perYear.toLocaleString('de-DE') + ' kg/Jahr') +
|
||
stat('🦠 Krankheits-Ausbrüche', breakdown.diseases.value + ' ×', breakdown.diseases.score, 'Monokultur-Strafen') +
|
||
stat('🌱 Sortenvielfalt', breakdown.crops.value + ' / 10', breakdown.crops.score, 'verschiedene Sorten') +
|
||
stat('🌍 Länder bepflanzt', breakdown.countries.value + ' Länder', breakdown.countries.score, breakdown.climates.value + ' Klimazonen');
|
||
|
||
const tips = generateTips(breakdown);
|
||
const tipsEl = document.getElementById('end-tips');
|
||
if (tips.length > 0) {
|
||
tipsEl.classList.remove('green');
|
||
tipsEl.innerHTML = '<h3>💡 Hinweise zur Verbesserung</h3><ul>' + tips.map(t => '<li>' + t + '</li>').join('') + '</ul>';
|
||
} else {
|
||
tipsEl.classList.add('green');
|
||
tipsEl.innerHTML = '<h3>🌟 Hervorragende Bilanz!</h3><p style="font-size:12px;color:#1f4b37;margin:0">Du hast in allen Bereichen sehr gut gearbeitet. Klimagerecht, sortenvielfältig, klimafreundlich.</p>';
|
||
}
|
||
|
||
document.getElementById('end-submit-status').textContent = '';
|
||
document.getElementById('end-report-overlay').classList.add('show');
|
||
playSound('end_report_open');
|
||
|
||
// Submit zur Plattform (im Hintergrund)
|
||
submitAssessment(stars, score, breakdown);
|
||
}
|
||
|
||
function closeEndReport() {
|
||
document.getElementById('end-report-overlay').classList.remove('show');
|
||
}
|
||
|
||
async function submitAssessment(stars, score, breakdown) {
|
||
const statusEl = document.getElementById('end-submit-status');
|
||
if (!window.FARMER_SESSION_ID) {
|
||
statusEl.textContent = '(kein Plattform-Kontext — Gast-Modus, Ergebnis nicht übermittelt)';
|
||
statusEl.className = 'farmer-end-submit-status';
|
||
return;
|
||
}
|
||
const action_log = game.reports.slice(-100).map(r => ({
|
||
year: r.year, crop: r.cropId, country: r.countryName,
|
||
zone: r.zoneId, suitability: r.suitability,
|
||
gross: r.gross, net: r.net,
|
||
co2_kg: r.transport ? r.transport.co2kg : 0,
|
||
disease: !!r.disease,
|
||
}));
|
||
const body = JSON.stringify({
|
||
action: 'complete',
|
||
simId: 'farmer',
|
||
stars,
|
||
durationMs: Date.now() - startTime,
|
||
data: {
|
||
level: parseInt(window.FARMER_FORCED_LEVEL || '1', 10),
|
||
score,
|
||
results: {
|
||
money: breakdown.money.value,
|
||
co2_kg: breakdown.co2.value,
|
||
attempts: game.reports.length,
|
||
success_rate: breakdown.hitRate.value / 100,
|
||
diseases: breakdown.diseases.value,
|
||
crops_used: breakdown.crops.value,
|
||
countries_used: breakdown.countries.value,
|
||
climates_used: breakdown.climates.value,
|
||
year_reached: game.year,
|
||
// Einheitliche Leistungskennzahl (0–100): gewichtete Gesamtwertung (totalScore).
|
||
leistung_pct: Math.max(0, Math.min(100, Math.round(score))),
|
||
},
|
||
action_log,
|
||
},
|
||
});
|
||
|
||
const apiBase = window.FARMER_API_BASE || '/geograsim/App/api';
|
||
try {
|
||
const r = await fetch(apiBase + '/progress', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
body,
|
||
});
|
||
if (r.ok) {
|
||
statusEl.textContent = '✓ Ergebnis an Klassen-Plattform übermittelt';
|
||
statusEl.className = 'farmer-end-submit-status ok';
|
||
return;
|
||
}
|
||
console.log('[farmer] submit → HTTP ' + r.status);
|
||
} catch (err) {
|
||
console.log('[farmer] submit → fehlgeschlagen', err.message);
|
||
}
|
||
statusEl.textContent = '⚠ Übermittlung an Plattform fehlgeschlagen';
|
||
statusEl.className = 'farmer-end-submit-status err';
|
||
}
|
||
|
||
function resetGame() {
|
||
if (!confirm('Wirklich neu starten? Alle Pflanzungen, Geld und Statistiken werden gelöscht.')) return;
|
||
playSound('reset');
|
||
for (const m of fieldMarkers.values()) map.removeLayer(m);
|
||
for (const c of fieldCircles.values()) map.removeLayer(c);
|
||
fieldMarkers.clear();
|
||
fieldCircles.clear();
|
||
game = window.FarmerEngine.init(window.FARMER_FORCED_LEVEL || 'l1', Date.now());
|
||
startTime = Date.now();
|
||
_prevMoneyNeg = false;
|
||
document.getElementById('year-num').textContent = game.year;
|
||
buildPicker();
|
||
selectCrop(window.FarmerEngine.getCrops()[0].id);
|
||
updateMoneyDisplay();
|
||
updateMarketDisplay();
|
||
highlightActiveMarketHub();
|
||
document.getElementById('harvest-btn').disabled = true;
|
||
closeEndReport();
|
||
map.setView([30, 15], 3, { animate: true });
|
||
}
|
||
|
||
// ===== Animation =====
|
||
async function animateTrades(reports) {
|
||
// Phase 1: alle Vehicles fahren parallel — regionale direkt zur Konsumcity,
|
||
// internationale zum Markt-Hub
|
||
const promises = reports
|
||
.filter(r => r.transport && r.hub)
|
||
.map(r => animateOneTrade(r));
|
||
await Promise.all(promises);
|
||
|
||
// Phase 2: Distribution nur wenn mindestens eine internationale Lieferung
|
||
// am Hub angekommen ist (bei rein regionaler Lieferung sind alle Vehicles
|
||
// bereits direkt bei den Konsument*innen)
|
||
const intl = reports.find(r => r.hub && !r.isRegional);
|
||
if (intl && intl.hub.consumerCities && intl.hub.consumerCities.length > 0) {
|
||
await animateDistribution(intl.hub);
|
||
}
|
||
}
|
||
|
||
function animateDistribution(hub) {
|
||
// 3 zufällige Konsumcity (oder weniger wenn nicht vorhanden)
|
||
const cities = hub.consumerCities.slice();
|
||
for (let i = cities.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[cities[i], cities[j]] = [cities[j], cities[i]];
|
||
}
|
||
const picks = cities.slice(0, Math.min(3, cities.length));
|
||
return Promise.all(picks.map((city, i) =>
|
||
new Promise(r => setTimeout(r, i * 250)).then(() => animateOneDistribution(hub, city))
|
||
));
|
||
}
|
||
|
||
function animateOneDistribution(hub, city) {
|
||
return new Promise(resolve => {
|
||
const distKm = window.FarmerEngine.haversineKm(hub.lat, hub.lon, city.lat, city.lon);
|
||
// Kürzere Animationen — Distribution ist Garnitur, nicht Hauptakt
|
||
const duration = Math.max(1200, Math.min(3500, distKm / 700 * 1500));
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div class="farmer-vehicle-icon" style="font-size:20px">🚚</div>`,
|
||
iconSize: [24, 24], iconAnchor: [12, 12],
|
||
});
|
||
const marker = L.marker([hub.lat, hub.lon], { icon, interactive: false }).addTo(map);
|
||
playSound('vehicle_truck', { volume: 0.4 });
|
||
const t0 = performance.now();
|
||
function step(t) {
|
||
const p = Math.min(1, (t - t0) / duration);
|
||
marker.setLatLng([hub.lat + (city.lat - hub.lat) * p, hub.lon + (city.lon - hub.lon) * p]);
|
||
if (p < 1) requestAnimationFrame(step);
|
||
else {
|
||
map.removeLayer(marker);
|
||
// Kleiner Ankunfts-Pin am Zielort mit Stadtnamen
|
||
const toastIcon = L.divIcon({
|
||
className: '',
|
||
html: `<div class="farmer-money-toast" style="background:#4a7c4e;font-size:11px">📍 ${city.name}</div>`,
|
||
iconSize: [110, 22], iconAnchor: [55, 30],
|
||
});
|
||
const tm = L.marker([city.lat, city.lon], { icon: toastIcon, interactive: false }).addTo(map);
|
||
let t1 = performance.now();
|
||
const fadeDur = 1800;
|
||
function fade(t) {
|
||
const fp = Math.min(1, (t - t1) / fadeDur);
|
||
const el = tm.getElement();
|
||
if (el) el.style.opacity = String(1 - fp);
|
||
if (fp < 1) requestAnimationFrame(fade);
|
||
else map.removeLayer(tm);
|
||
}
|
||
requestAnimationFrame(fade);
|
||
resolve();
|
||
}
|
||
}
|
||
requestAnimationFrame(step);
|
||
});
|
||
}
|
||
|
||
function animateOneTrade(report) {
|
||
return new Promise(resolve => {
|
||
const startLat = report.latlng.lat, startLon = report.latlng.lon;
|
||
// Ziel: Konsumcity (regional) oder Markt-Hub (international)
|
||
const dest = report.destination || report.hub;
|
||
const endLat = dest.lat, endLon = dest.lon;
|
||
const speed = report.transport.mode.speedKmPerSec;
|
||
// Animations-Tempo bewusst langsam (1.5× ggü. v17a) — Bewegung muss
|
||
// mit den Augen folgbar sein, gerade auf Welt-Karte mit großen Distanzen.
|
||
const duration = Math.max(2250, Math.min(12000, (report.distanceKm / speed) * 1000));
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div class="farmer-vehicle-icon">${report.transport.mode.icon}</div>`,
|
||
iconSize: [30, 30], iconAnchor: [15, 15],
|
||
});
|
||
const marker = L.marker([startLat, startLon], { icon, interactive: false }).addTo(map);
|
||
playSound('vehicle_' + report.transport.modeKey);
|
||
const t0 = performance.now();
|
||
function step(t) {
|
||
const p = Math.min(1, (t - t0) / duration);
|
||
marker.setLatLng([startLat + (endLat - startLat) * p, startLon + (endLon - startLon) * p]);
|
||
if (p < 1) requestAnimationFrame(step);
|
||
else {
|
||
map.removeLayer(marker);
|
||
window.FarmerEngine.applyReportToBudget(game, report);
|
||
showMoneyToast(report);
|
||
updateMoneyDisplay();
|
||
playSound('vehicle_arrive');
|
||
resolve();
|
||
}
|
||
}
|
||
requestAnimationFrame(step);
|
||
});
|
||
}
|
||
|
||
function showMoneyToast(report) {
|
||
const isLoss = report.net < 0;
|
||
const text = (report.net >= 0 ? '+' : '') + report.net + ' €';
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div class="farmer-money-toast ${isLoss ? 'loss' : ''}">${report.cropIcon} ${text}</div>`,
|
||
iconSize: [90, 22], iconAnchor: [45, 30],
|
||
});
|
||
const dest = report.destination || report.hub;
|
||
const marker = L.marker([dest.lat, dest.lon], { icon, interactive: false }).addTo(map);
|
||
let t0 = performance.now();
|
||
const dur = 2500;
|
||
function fade(t) {
|
||
const p = Math.min(1, (t - t0) / dur);
|
||
const el = marker.getElement();
|
||
if (el) el.style.opacity = String(1 - p);
|
||
if (p < 1) requestAnimationFrame(fade);
|
||
else map.removeLayer(marker);
|
||
}
|
||
requestAnimationFrame(fade);
|
||
}
|
||
|
||
let _prevMoneyNeg = false;
|
||
function updateMarketDisplay() {
|
||
const hub = window.FarmerEngine.getMarketHubForYear(game.year);
|
||
if (!hub) return;
|
||
const left = window.FarmerEngine.yearsLeftInMarketBlock(game.year);
|
||
const yearWord = left === 1 ? 'Jahr' : 'Jahre';
|
||
document.getElementById('market-text').innerHTML =
|
||
hub.icon + ' ' + pickText(hub.title) + ` <small style="opacity:0.6;font-weight:500">(noch ${left} ${yearWord})</small>`;
|
||
// Bei Markt-Wechsel ggf. neue regionale Musik
|
||
updateMusicForCurrentMarket();
|
||
}
|
||
|
||
function updateMoneyDisplay() {
|
||
const el = document.getElementById('money-display');
|
||
el.textContent = game.money.toLocaleString('de-DE') + ' €';
|
||
const isNeg = game.money < 0;
|
||
el.classList.toggle('negative', isNeg);
|
||
if (isNeg && !_prevMoneyNeg) playSound('negative_balance');
|
||
_prevMoneyNeg = isNeg;
|
||
updateKpi(); // KPIs sind an Money-Update gekoppelt — bei jeder Bilanz-Änderung neu rendern
|
||
}
|
||
|
||
/** Baut kumulative Zeitreihen pro Jahr aus game.reports.
|
||
* Iteriert von 1 bis zum letzten Jahr in dem es Reports gab — egal ob
|
||
* game.year inzwischen weitergerückt ist. Damit ist die Sparkline immer
|
||
* konsistent mit der KPI-Zahl (game.money etc.). */
|
||
function buildKpiTimeSeries(reports) {
|
||
const money = [];
|
||
const rate = [];
|
||
const co2 = [];
|
||
let cumMoney = 0, cumCo2 = 0, cumHits = 0, cumTotal = 0;
|
||
const maxYear = reports.length > 0 ? Math.max.apply(null, reports.map(r => r.year)) : 0;
|
||
for (let y = 1; y <= maxYear; y++) {
|
||
const yReports = reports.filter(r => r.year === y);
|
||
cumMoney += yReports.reduce((s, r) => s + r.net, 0);
|
||
cumCo2 += yReports.reduce((s, r) => s + (r.transport ? r.transport.co2kg : 0), 0);
|
||
cumHits += yReports.filter(r => r.suitability === 'S1' || r.suitability === 'S2').length;
|
||
cumTotal += yReports.length;
|
||
money.push({ year: y, value: cumMoney });
|
||
co2.push ({ year: y, value: cumCo2 });
|
||
rate.push ({ year: y, value: cumTotal > 0 ? cumHits / cumTotal * 100 : 0 });
|
||
}
|
||
return { money, rate, co2 };
|
||
}
|
||
|
||
/** Mini-Sparkline als SVG mit Achsen-Beschriftung.
|
||
* Achsen werden IMMER gerendert (auch bei leeren Daten). Y auto-skaliert,
|
||
* X fix 1..xMax (default 22). Y-Defaults: 0..yDefault (für leere Daten). */
|
||
function renderSparkline(elId, data, color, opts) {
|
||
const el = document.getElementById(elId);
|
||
if (!el) return;
|
||
|
||
const w = 200, h = 56;
|
||
const padLeft = 36, padRight = 6;
|
||
const padTop = 4, padBottom = 12;
|
||
const plotW = w - padLeft - padRight;
|
||
const plotH = h - padTop - padBottom;
|
||
|
||
const xMin = 1;
|
||
const xMax = (opts && opts.xMax) || 22;
|
||
|
||
// Y-Range
|
||
let yMin, yMax;
|
||
if (opts && opts.yMin !== undefined) yMin = opts.yMin;
|
||
else yMin = data.length > 0 ? Math.min(0, Math.min.apply(null, data.map(d => d.value))) : 0;
|
||
|
||
if (opts && opts.yMax !== undefined) {
|
||
yMax = opts.yMax;
|
||
} else if (data.length > 0) {
|
||
yMax = Math.max(1, Math.max.apply(null, data.map(d => d.value)));
|
||
yMax = yMax + (yMax - yMin) * 0.10;
|
||
} else {
|
||
yMax = (opts && opts.yDefault) || 1;
|
||
}
|
||
if (yMax === yMin) yMax = yMin + 1;
|
||
|
||
const xPx = (x) => padLeft + ((x - xMin) / (xMax - xMin)) * plotW;
|
||
const yPx = (y) => padTop + plotH - ((y - yMin) / (yMax - yMin)) * plotH;
|
||
|
||
const fmt = (opts && opts.formatY) ? opts.formatY : (v => Math.round(v).toString());
|
||
const yMaxLabel = fmt(yMax);
|
||
const yMinLabel = fmt(yMin);
|
||
|
||
const triggerX = xPx(11);
|
||
const baselineY = yPx(yMin);
|
||
|
||
// Polyline + aktueller Punkt nur wenn Daten da
|
||
let line = '';
|
||
if (data.length === 1) {
|
||
// Einzelner Punkt: kleines Kreuz markieren (sonst ist Polyline unsichtbar)
|
||
const p = data[0];
|
||
const x = xPx(p.year), y = yPx(p.value);
|
||
line = '<circle cx="' + x + '" cy="' + y + '" r="2.5" fill="' + color + '"/>';
|
||
} else if (data.length > 1) {
|
||
const points = data.map(d => xPx(d.year).toFixed(1) + ',' + yPx(d.value).toFixed(1)).join(' ');
|
||
const last = data[data.length - 1];
|
||
const lx = xPx(last.year), ly = yPx(last.value);
|
||
line =
|
||
'<polyline points="' + points + '" fill="none" stroke="' + color + '" stroke-width="1.8" stroke-linejoin="round"/>' +
|
||
'<circle cx="' + lx + '" cy="' + ly + '" r="2.5" fill="' + color + '"/>';
|
||
}
|
||
|
||
el.innerHTML =
|
||
'<svg viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="xMidYMid meet">' +
|
||
'<line x1="' + padLeft + '" y1="' + padTop + '" x2="' + padLeft + '" y2="' + (padTop + plotH) + '" class="farmer-kpi-spark-tick"/>' +
|
||
'<line x1="' + padLeft + '" y1="' + baselineY + '" x2="' + (padLeft + plotW) + '" y2="' + baselineY + '" class="farmer-kpi-spark-tick"/>' +
|
||
'<text x="' + (padLeft - 3) + '" y="' + (padTop + 4) + '" text-anchor="end" class="farmer-kpi-spark-axis">' + yMaxLabel + '</text>' +
|
||
'<text x="' + (padLeft - 3) + '" y="' + baselineY + '" text-anchor="end" class="farmer-kpi-spark-axis">' + yMinLabel + '</text>' +
|
||
'<text x="' + xPx(1) + '" y="' + (h - 2) + '" text-anchor="middle" class="farmer-kpi-spark-axis">1</text>' +
|
||
'<text x="' + xPx(11) + '" y="' + (h - 2) + '" text-anchor="middle" class="farmer-kpi-spark-trigger">↑11</text>' +
|
||
'<text x="' + xPx(22) + '" y="' + (h - 2) + '" text-anchor="middle" class="farmer-kpi-spark-axis">22</text>' +
|
||
'<line x1="' + triggerX + '" y1="' + padTop + '" x2="' + triggerX + '" y2="' + (padTop + plotH) + '" ' +
|
||
'stroke="#b87800" stroke-width="0.5" stroke-dasharray="2,2" opacity="0.6"/>' +
|
||
line +
|
||
'</svg>';
|
||
}
|
||
|
||
// ===== Live-State-Hook für Lehrer-Live-View =====
|
||
// Plattform-Live-Client (App/assets/js/live-client.js) ruft alle 4 s auf.
|
||
// Liefert kompakten Snapshot für die Klassenliste-Vergleichstabelle.
|
||
window.GGS_LIVE_STATE = function () {
|
||
if (!game) return null;
|
||
try {
|
||
const reports = game.reports;
|
||
const totalCo2 = reports.reduce((s, r) => s + (r.transport ? r.transport.co2kg : 0), 0);
|
||
const successCount = reports.filter(r => r.suitability === 'S1' || r.suitability === 'S2').length;
|
||
const totalCount = reports.length;
|
||
const hitRate = totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0;
|
||
const diseases = reports.filter(r => r.disease).length;
|
||
const hub = window.FarmerEngine && window.FarmerEngine.getMarketHubForYear
|
||
? window.FarmerEngine.getMarketHubForYear(game.year)
|
||
: null;
|
||
// Plattform-Standardfelder für das Lehrer-Cockpit (simId, phase, progressPct):
|
||
// Farmer geht 10 Jahre bis Endbericht, danach „Weiter arbeiten" bis 22+
|
||
// (siehe END_REPORT_TRIGGER_YEAR = 11). Skala bezogen auf 10 Jahre, gekappt.
|
||
const targetYear = (typeof END_REPORT_TRIGGER_YEAR === 'number') ? (END_REPORT_TRIGGER_YEAR - 1) : 10;
|
||
const yearProgress = Math.max(0, Math.min(100, Math.round((game.year / targetYear) * 100)));
|
||
// Einheitliche Leistungskennzahl (0–100): dieselbe gewichtete Gesamtwertung wie
|
||
// im Endbericht/Submit, live aus dem aktuellen Stand berechnet. Bricht die
|
||
// Berechnung wider Erwarten, fällt sie sicher auf die Trefferquote (hit_rate) als
|
||
// dokumentierten Proxy zurück, damit die Live-Telemetrie nie ausfällt.
|
||
let leistungPct = hitRate; // Proxy: Trefferquote
|
||
try {
|
||
leistungPct = Math.max(0, Math.min(100, Math.round(computeFinalScore().score)));
|
||
} catch (e) {
|
||
leistungPct = hitRate; // Proxy: Trefferquote
|
||
}
|
||
return {
|
||
simId: 'farmer',
|
||
phase: 'Jahr ' + game.year + ' von ' + targetYear,
|
||
progressPct: yearProgress,
|
||
year: game.year,
|
||
market: hub ? hub.id : null,
|
||
market_de: hub ? hub.title.de : null,
|
||
cash: Math.round(game.money),
|
||
fields: game.fields.length,
|
||
attempts: totalCount,
|
||
hit_rate: hitRate,
|
||
co2_kg: totalCo2,
|
||
diseases: diseases,
|
||
leistung_pct: leistungPct,
|
||
crops_used: new Set(reports.map(r => r.cropId)).size,
|
||
countries: new Set(reports.filter(r => r.countryName).map(r => r.countryName)).size,
|
||
timestamp: Date.now(),
|
||
};
|
||
} catch (err) {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
function updateKpi() {
|
||
if (!game) return;
|
||
const reports = game.reports;
|
||
const totalCo2 = reports.reduce((s, r) => s + (r.transport ? r.transport.co2kg : 0), 0);
|
||
const successCount = reports.filter(r => r.suitability === 'S1' || r.suitability === 'S2').length;
|
||
const totalCount = reports.length;
|
||
const rate = totalCount > 0 ? Math.round((successCount / totalCount) * 100) : 0;
|
||
const diseases = reports.filter(r => r.disease).length;
|
||
|
||
// Aktuelle Werte
|
||
const moneyEl = document.getElementById('kpi-money');
|
||
moneyEl.textContent = game.money.toLocaleString('de-DE') + ' €';
|
||
moneyEl.classList.toggle('negative', game.money < 0);
|
||
moneyEl.classList.toggle('green', game.money > 0);
|
||
|
||
document.getElementById('kpi-rate').textContent = totalCount > 0 ? rate + ' %' : '— %';
|
||
|
||
const co2El = document.getElementById('kpi-co2');
|
||
co2El.textContent = totalCo2.toLocaleString('de-DE') + ' kg';
|
||
const yearsPlayed = Math.max(1, game.year - 1);
|
||
const co2PerYear = totalCo2 / yearsPlayed;
|
||
co2El.classList.remove('negative', 'green');
|
||
if (co2PerYear > 5000) co2El.classList.add('negative');
|
||
else if (co2PerYear < 1500 && totalCount > 0) co2El.classList.add('green');
|
||
|
||
document.getElementById('kpi-attempts').textContent = totalCount;
|
||
document.getElementById('kpi-diseases').textContent = diseases;
|
||
|
||
// Sparklines
|
||
const ts = buildKpiTimeSeries(reports);
|
||
renderSparkline('kpi-money-spark', ts.money, '#1f7a3d', {
|
||
yDefault: 1000,
|
||
formatY: (v) => v >= 1000 ? (Math.round(v/100)/10) + 'k€' : Math.round(v) + '€',
|
||
});
|
||
renderSparkline('kpi-rate-spark', ts.rate, '#8b5a2b', {
|
||
yMin: 0, yMax: 100, // Treffer-Quote fix 0..100 %
|
||
formatY: (v) => Math.round(v) + '%',
|
||
});
|
||
renderSparkline('kpi-co2-spark', ts.co2, '#a02a20', {
|
||
yDefault: 100,
|
||
formatY: (v) => v >= 1000 ? (Math.round(v/100)/10) + 't' : Math.round(v) + 'kg',
|
||
});
|
||
}
|
||
|
||
// ===== Toast =====
|
||
let toastTimer = null;
|
||
function showToast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => t.classList.remove('show'), 2400);
|
||
}
|
||
|
||
console.log('[farmer] Phase 1.6 geladen — Monokultur, Krankheit, Unlock.',
|
||
'mode:', window.FARMER_SESSION_MODE,
|
||
'easy:', EASY,
|
||
'level:', window.FARMER_FORCED_LEVEL || 'l1');
|
||
</script>
|
||
|
||
<script src="../../assets/js/ggs-quick-intro.js" defer></script>
|
||
<script>
|
||
window.addEventListener('DOMContentLoaded', function () {
|
||
if (window.GgsQuickIntro) GgsQuickIntro.init({
|
||
simId: 'farmer',
|
||
title: '🌾 Landwirtschaft',
|
||
lead: 'In dieser Simulation bewirtschaftest du Höfe auf der ganzen Welt. Welche Kultur passt zum Boden, zum Klima und zur Marktlage? Jede Saison ist eine neue Entscheidung.',
|
||
steps: [
|
||
'Felder anlegen und Kulturen aussäen — Boden + Klimazone bestimmen, was wächst.',
|
||
'Wetter und Klimawandel beobachten — die Bedingungen ändern sich pro Jahr.',
|
||
'Ernten und verkaufen — mit den Jahren werden neue Sorten freigeschaltet.',
|
||
'Glossar (📖 im Header) liefert Fachinfos zu Pflanzen, Klimazonen und Importpreisen.',
|
||
],
|
||
goals: [
|
||
'Klimazonen und passende Kulturpflanzen zuordnen',
|
||
'Wirtschaftliche Folgen von Wetter und Markt einschätzen',
|
||
'Nachhaltigkeit, Biodiversität und Erträge gegeneinander abwägen',
|
||
],
|
||
});
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|