Files
geograsim/App/sims/farmer/game.html
T
Adminator 7b1bdb8d1b ggs-header Batch 2 Start: Farmer + Komponenten-Vorruestung
- Komponente: Logo-Fallback (Kreis-Logo wenn Sim keins hat), Overlay-Modus
  (data-ggs-overlay fuer Vollbild-Canvas), generische Icon-Button-Optik fuer
  Aktionen ohne .ggs-btn-ghost-Klasse, Ton-IDs erweitert (audio-toggle/btnSound).
- Farmer als erstes Bespoke-Muster: ggs-topbar -> ggs-header, schwebende Pille
  (Fallback-Logo + Titel) + Geld-KPI + Icon-Buttons; interne Fachinfo bleibt
  (data-ggs-no-glossar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 14:33:53 +02:00

1984 lines
80 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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/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; left: 8px; z-index: 1000;
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: 8px; 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: 76px; 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">
<h1 class="ggs-header-module">🌾 Landwirtschaft</h1>
<div class="farmer-money" id="money-display">0 €</div>
<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-year-label">Jahr</span>
<span class="farmer-year-num" id="year-num">1</span>
<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 &lt; 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 &amp; 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: '&copy; <a href="https://carto.com/attributions">CARTO</a> &copy; 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 (0100): 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 (0100): 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>