47adf0497d
floodedPct = sea*0.45 + max(0,sea-prot)*1.1. Vorher deckelte der Strand-Anteil bei 12%, sobald Schutz das Inland trocken hielt -> alle Spieler zeigten 12% egal wie das Klima stand. Die Karte zeichnet die Flut aber aus seaLevelCm -> Zahl und Bild liefen auseinander. Jetzt steigt die Zahl stetig mit dem Meeresspiegel (Klimafolge), Schutz daempft nur den Zusatzschaden. Node-validiert: gutes Klima=niedrige Flut, hohe Temp= hohe Flut, gleiche-Tick-Spreizung statt Plateau. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
899 lines
40 KiB
JavaScript
899 lines
40 KiB
JavaScript
/**
|
||
* Klima-Wächter — Engine (Single Source of Truth für 2D und 3D)
|
||
* ------------------------------------------------------------------
|
||
* Headless Spiellogik: kein DOM, keine kwAudio-Aufrufe, keine Renderings.
|
||
* Konsumenten (game-2d.html, game-3d.html) rufen die Engine-Methoden auf
|
||
* und verarbeiten die zurückgegebenen Events (Toast, Sound) selbst.
|
||
*
|
||
* Exportiert als globales `window.KlimaEngine` (kein ES-Module, kein Bundler).
|
||
*
|
||
* Die Engine-Funktionen sind zustandsfrei gegenüber dem Modul — sie operieren
|
||
* auf einem `game`-Objekt, das du mit `createGame(levelId)` erzeugst.
|
||
*
|
||
* Event-Pattern: mutierende Aktionen (buyMeasure, tick, applyCitizenChoice)
|
||
* pushen fire-and-forget Events in einen internen Puffer `game._pendingEvents`
|
||
* und geben beim Aufruf die in DIESEM Aufruf frisch hinzugekommenen Events
|
||
* zurück, damit die View Sound/Toast ansteuern kann. Alle Events werden
|
||
* ausserdem permanent in `game.events` (Event-Stapel) gehalten.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
/* ============================================================
|
||
KONSTANTEN — Maßnahmen, Schwierigkeit, Achievements, Events
|
||
============================================================ */
|
||
|
||
const MEASURES = [
|
||
{ id:'forest', name:'Wald aufforsten', emoji:'🌲', description:'Bäume binden langfristig CO₂. Sehr günstig im Unterhalt — die Naturlösung.',
|
||
cost:12, upkeep:0.5, co2Reduction:0.04, protection:0 },
|
||
{ id:'solar', name:'Solaranlage', emoji:'☀️', description:'Klein und günstig — sofort kaufbar. Wartung ist mittelhoch.',
|
||
cost:40, upkeep:2, co2Reduction:0.14, protection:0, powerOutput:1 },
|
||
{ id:'wind', name:'Windpark', emoji:'🌬️', description:'Großes Windrad — hohe Anschaffung, dafür viel sauberer Strom.',
|
||
cost:150, upkeep:4, co2Reduction:0.50, protection:0, powerOutput:3 },
|
||
{ id:'green-roof', name:'Gründächer', emoji:'🏡', description:'Begrünte Dächer kühlen die Stadt. Keine jährlichen Kosten.',
|
||
cost:30, upkeep:0, co2Reduction:0.04, protection:0 },
|
||
{ id:'bikes', name:'Radwege-Netz', emoji:'🚲', description:'Macht die Insel fahrradfreundlich. Günstig, kein Strom nötig.',
|
||
cost:18, upkeep:0.5, co2Reduction:0.06, protection:0 },
|
||
{ id:'mangrove', name:'Mangrovenwald', emoji:'🪸', description:'Bindet CO₂ UND schützt die Küste vor Erosion — beides in einem.',
|
||
cost:35, upkeep:1, co2Reduction:0.06, protection:6 },
|
||
{ id:'sand-fill', name:'Sand-Aufschüttung', emoji:'🏖', description:'Künstlicher Strand. Sofort Schutz — wird aber jährlich vom Meer weggespült.',
|
||
cost:50, upkeep:0, co2Reduction:0, protection:18 },
|
||
{ id:'dike', name:'Deich bauen', emoji:'🌊', description:'Niedrige Hürde — schnell baubar, aber teuer pro cm Schutz.',
|
||
cost:80, upkeep:3, co2Reduction:0, protection:12 },
|
||
{ id:'sea-wall', name:'Hochwasserschutz', emoji:'🛡️', description:'Teuer, aber deutlich effizienter pro Mio €. Sehr starker Schutz.',
|
||
cost:240, upkeep:6, co2Reduction:0, protection:50 },
|
||
{ id:'coal', name:'Kohlekraftwerk', emoji:'🏭', description:'Liefert viel Strom — billig und sofort. Aber viel CO₂-Ausstoß!',
|
||
cost:20, upkeep:1, co2Reduction:-0.38, protection:0, powerOutput:4 },
|
||
{ id:'airport', name:'Tourismus-Flughafen', emoji:'✈️', description:'Bringt Touristen — aber Flugverkehr bedeutet viel CO₂.',
|
||
cost:62, upkeep:3, co2Reduction:-0.22, protection:0 },
|
||
{ id:'cloud-seed', name:'Wolken-Impfung', emoji:'🌤️', description:'Geo-Engineering. Klingt nach Wundermittel — wirkt aber kaum.',
|
||
cost:125, upkeep:4.5, co2Reduction:0.04, protection:0 },
|
||
];
|
||
|
||
const DIFFICULTY = {
|
||
1: { id:1, label:'Lernen', emoji:'🟢',
|
||
startBudget:600, startPopulation:6000, incomePer10k:220,
|
||
blackoutGrace:10, treesPerBlackout:1, co2BlackoutPerYear:0.15,
|
||
climateDamageMul:0.35, popLossMul:0.35,
|
||
skipCitizenEvents:['citizen-scientist','citizen-farmer','citizen-tourism','citizen-youth','citizen-industry','citizen-mountain'],
|
||
citizenEventTickOffset:20 },
|
||
2: { id:2, label:'Üben', emoji:'🟡',
|
||
startBudget:480, startPopulation:6000, incomePer10k:155,
|
||
blackoutGrace:0, treesPerBlackout:1, co2BlackoutPerYear:0.4,
|
||
climateDamageMul:1.2, popLossMul:1.1 },
|
||
3: { id:3, label:'Profi', emoji:'🔴',
|
||
startBudget:220, startPopulation:7000, incomePer10k:110,
|
||
blackoutGrace:0, treesPerBlackout:2, co2BlackoutPerYear:0.7,
|
||
climateDamageMul:2.2, popLossMul:1.8 }
|
||
};
|
||
|
||
const ACHIEVEMENTS = [
|
||
{ key:'first_build', icon:'🔨', title:'Erste Maßnahme', desc:'Deine erste Klimaschutz-Maßnahme gebaut.' },
|
||
{ key:'first_wind', icon:'🌬️', title:'Windkraft-Pionier', desc:'Ersten Windpark errichtet.' },
|
||
{ key:'first_coast', icon:'🛡️', title:'Küstenschützer*in', desc:'Deich oder Hochwasserschutz gebaut.' },
|
||
{ key:'renewable_100', icon:'☀️', title:'100 % Erneuerbar', desc:'Keine fossilen Kraftwerke, Stromdeckung gesichert.' },
|
||
{ key:'under_1_5', icon:'🌍', title:'Unter 1,5 °C', desc:'Temperatur bleibt im Pariser Zielkorridor.' },
|
||
{ key:'forester', icon:'🌲', title:'Baummeister*in', desc:'Mindestens 10 Wald-Aufforstungen.' },
|
||
{ key:'survivor', icon:'🏆', title:'Überlebenskünstler*in', desc:'Das Jahr 2175 erreicht.' },
|
||
{ key:'frugal', icon:'💰', title:'Sparfuchs', desc:'Am Ende noch > 100 Mio € übrig.' },
|
||
];
|
||
|
||
// Aufklärungs-Events bei zweifelhaften Maßnahmen, einmal pro Maßnahme.
|
||
const FIRST_BUY_HINTS = {
|
||
'cloud-seed': { icon:'⚠️', text:'Wolken-Impfung klingt groß — Forschung zeigt: kaum Klima-Wirkung. Klick für Details.', infoKey:'cloud_seeding' },
|
||
'coal': { icon:'🏭', text:'Kohle liefert schnell Strom — aber jede Tonne Kohle = sehr viel CO₂. Klick für Details.', infoKey:'coal_power' },
|
||
'airport': { icon:'✈️', text:'Flughafen bringt Geld — Flugverkehr ist aber sehr CO₂-intensiv. Klick für Details.', infoKey:'aviation' },
|
||
};
|
||
|
||
const CITIZEN_EVENTS = [
|
||
{ id:'citizen-fisher', tick:30, cond:g => true,
|
||
char:'🎣', name:'Lina, Fischerin',
|
||
msg:'Bürgermeister*in! Das Wasser wird wärmer, die Fische ziehen weg. Mein Boot ist alt — wir brauchen Hilfe, oder wir müssen aufgeben.',
|
||
choices:[
|
||
{ title:'120 Mio € für moderne Boote', desc:'Bevölkerung stabil. Budget −120 Mio €.',
|
||
apply:(g, ctx) => { g.budget -= 120; } },
|
||
{ title:'Auf Tourismus umstellen', desc:'Kostenlos, Extra-Einnahmen solange Strand. Mehr CO₂. Strand weg → Kollaps.',
|
||
apply:(g, ctx) => { g.population -= 200; g.tourismMode = true; ctx.pushEvent('🏖','Die Insel stellt sich auf Tourismus um.','neutral'); } },
|
||
{ title:'Nichts tun', desc:'−400 Bevölkerung, aber kein Geldverlust.',
|
||
apply:(g, ctx) => { g.population -= 400; } },
|
||
] },
|
||
{ id:'citizen-scientist', tick:18, cond:g => true,
|
||
char:'👩🔬', name:'Dr. Hassan, Klimaforscherin',
|
||
msg:'Wir haben ein neues Verfahren entwickelt — mit einem Forschungszentrum werden alle künftigen Maßnahmen 20 % billiger.',
|
||
choices:[
|
||
{ title:'Forschungszentrum bauen (300 Mio €)', desc:'Alle künftigen Maßnahmen 20 % billiger.',
|
||
apply:(g, ctx) => { g.budget -= 300; g.researchDiscount = 0.8; } },
|
||
{ title:'Antrag ablehnen', desc:'Nichts passiert.',
|
||
apply:(g, ctx) => {} },
|
||
] },
|
||
{ id:'citizen-farmer', tick:28, cond:g => g.currentTemp > 15.8,
|
||
char:'👨🌾', name:'Yusuf, Bauer',
|
||
msg:'Die Felder vertrocknen! Letztes Jahr hatten wir kaum Ernte. Brauchen wir Bewässerung?',
|
||
choices:[
|
||
{ title:'Bewässerungssystem bauen (180 Mio €)', desc:'Bevölkerung stabil.',
|
||
apply:(g, ctx) => { g.budget -= 180; } },
|
||
{ title:'Salzresistente Sorten subventionieren (60 Mio €)', desc:'−100 Menschen, langfristig stabiler.',
|
||
apply:(g, ctx) => { g.budget -= 60; g.population -= 100; } },
|
||
{ title:'Nichts tun', desc:'−500 Menschen ziehen weg.',
|
||
apply:(g, ctx) => { g.population -= 500; } },
|
||
] },
|
||
{ id:'citizen-tourism', tick:40, cond:g => g.seaLevelCm > 15,
|
||
char:'🏖️', name:'Maria, Hotelbesitzerin',
|
||
msg:'Der Strand wird kleiner. Meine Hotels stehen halbleer. Wir müssen etwas gegen den Meeresspiegel tun!',
|
||
choices:[
|
||
{ title:'Strand künstlich aufschütten (50 Mio €)', desc:'+5 cm Küstenschutz.',
|
||
apply:(g, ctx) => { g.budget -= 50; g.protection += 5; g.hasSandBeach = true; } },
|
||
{ title:'„Wir können das Meer nicht aufhalten"', desc:'−300 Menschen wandern ab.',
|
||
apply:(g, ctx) => { g.population -= 300; } },
|
||
] },
|
||
{ id:'citizen-youth', tick:56, cond:g => g.co2Reduction < 1,
|
||
char:'👧', name:'Lia, 14 Jahre',
|
||
msg:'Sie planen unsere Zukunft! Wir haben heute gestreikt. Machen Sie endlich ernst mit dem Klimaschutz.',
|
||
choices:[
|
||
{ title:'„Ich verspreche, mehr zu tun" — 1 Solar gratis', desc:'+1 kostenlose Solaranlage.',
|
||
apply:(g, ctx) => {
|
||
if (!g.ownedMeasures.solar) g.ownedMeasures.solar = { count:0, instances:[] };
|
||
g.ownedMeasures.solar.count++;
|
||
const extra = ctx.resolveMeta ? ctx.resolveMeta('solar') : null;
|
||
g.ownedMeasures.solar.instances.push(Object.assign({ builtAt: g.tick }, extra || {}));
|
||
recalcEffects(g);
|
||
} },
|
||
{ title:'Streik aussitzen', desc:'−150 Bevölkerung (Jugend wandert ab).',
|
||
apply:(g, ctx) => { g.population -= 150; } },
|
||
] },
|
||
{ id:'citizen-industry', tick:76, cond:g => true,
|
||
char:'🏭', name:'Konzernchef Vogel',
|
||
msg:'Wir bringen Arbeitsplätze — wenn Sie uns Steuererleichterungen geben. Sonst wandern wir ab.',
|
||
choices:[
|
||
{ title:'Subventionen zahlen (250 Mio €)', desc:'+500 Bevölkerung, +5 ppm CO₂.',
|
||
apply:(g, ctx) => { g.budget -= 250; g.population += 500; g.co2Ppm += 5; } },
|
||
{ title:'Klar nein', desc:'−200 Bevölkerung verlässt die Insel.',
|
||
apply:(g, ctx) => { g.population -= 200; } },
|
||
{ title:'Nur klimaneutral', desc:'Konzern lehnt ab, nichts passiert.',
|
||
apply:(g, ctx) => {} },
|
||
] },
|
||
{ id:'citizen-mountain', tick:100, cond:g => g.currentTemp > 16.5,
|
||
char:'🏔', name:'Anna vom Bergdorf',
|
||
msg:'Der Gletscher schmilzt — bei Starkregen fließt das Wasser in unser Dorf. Wir brauchen Schutzwälle!',
|
||
choices:[
|
||
{ title:'Lawinen-Schutzwall bauen (55 Mio €)', desc:'+4 cm Küstenschutz, Dorf sicher.',
|
||
apply:(g, ctx) => { g.budget -= 55; g.protection += 4; g.hasMountainShield = true; } },
|
||
{ title:'Bergdorf evakuieren', desc:'−400 Menschen, aber Geld gespart.',
|
||
apply:(g, ctx) => { g.population -= 400; } },
|
||
] },
|
||
];
|
||
|
||
const CO2_FLOOR = 350;
|
||
const TEMP_FLOOR = 15.0;
|
||
|
||
/* ============================================================
|
||
HILFEN
|
||
============================================================ */
|
||
|
||
function findMeasure(id) { return MEASURES.find(m => m.id === id); }
|
||
|
||
function pushEvent(game, icon, text, type, infoKey) {
|
||
const ev = { icon, text, type: type || 'neutral', tick: game.tick, infoKey: infoKey || null };
|
||
game.events.unshift(ev);
|
||
if (game.events.length > 40) game.events.pop();
|
||
game._pendingEvents.push(ev);
|
||
return ev;
|
||
}
|
||
|
||
function eventFiredOnce(game, id) {
|
||
if (game.firedEvents.has(id)) return true;
|
||
game.firedEvents.add(id);
|
||
return false;
|
||
}
|
||
|
||
/* ============================================================
|
||
KLIMA-PHYSIK
|
||
============================================================ */
|
||
|
||
function computeTemperatureFromCO2(co2ppm) {
|
||
const PRE = 280, CS = 3.0;
|
||
const SOLAR = 1361, STEF = 5.67e-8, ALBEDO = 0.3;
|
||
const absorbed = (SOLAR / 4) * (1 - ALBEDO);
|
||
const tempNoGH = Math.pow(absorbed / STEF, 0.25) - 273.15;
|
||
const NATURAL_GH = 33;
|
||
return tempNoGH + NATURAL_GH + CS * Math.log2(co2ppm / PRE);
|
||
}
|
||
|
||
function computeClimate(game) {
|
||
game.targetTemp = computeTemperatureFromCO2(game.co2Ppm);
|
||
if (game.currentTemp <= 0) game.currentTemp = game.targetTemp;
|
||
else game.currentTemp += (game.targetTemp - game.currentTemp) * 0.08;
|
||
if (game.currentTemp < TEMP_FLOOR) game.currentTemp = TEMP_FLOOR;
|
||
|
||
const deltaT = Math.max(0, game.currentTemp - 15);
|
||
|
||
// Schneller Anteil (thermische Ausdehnung + schnelle Gletscherschmelze)
|
||
const fastTarget = deltaT * 15;
|
||
if (fastTarget > game.seaFastCm) game.seaFastCm += (fastTarget - game.seaFastCm) * 0.04;
|
||
else game.seaFastCm += (fastTarget - game.seaFastCm) * 0.003;
|
||
if (game.seaFastCm < 0) game.seaFastCm = 0;
|
||
|
||
// Committed sea level rise (Eisschilde, Tiefenozean — irreversibel)
|
||
game.committedSeaCm += deltaT * 0.15;
|
||
|
||
game.seaLevelCm = game.seaFastCm + game.committedSeaCm;
|
||
|
||
// Geflutet-Anteil = sichtbare Wirkung auf der Karte. STETIG mit dem
|
||
// Meeresspiegel — KEIN Deckel-Plateau, damit der Wert die Klimaleistung
|
||
// widerspiegelt (nicht nur „hat Deiche gebaut"):
|
||
// • Grundanteil (Strand/Küste) steigt direkt mit dem Meeresspiegel und
|
||
// lässt sich NICHT wegdeichen — er ist die unvermeidbare Folge der
|
||
// Erwärmung. Niedrige Temperatur → niedriger Meeresspiegel → wenig Flut.
|
||
// • Zusatzschaden dort, wo der Anstieg den Schutz ÜBERRENNT (effRise):
|
||
// Deiche/Mauern verzögern das Absaufen des Inlands, halten aber nicht
|
||
// ewig. So hilft Schutz spürbar, ersetzt aber keinen Klimaschutz.
|
||
const effRise = Math.max(0, game.seaLevelCm - game.protection);
|
||
game.floodedPct = Math.round(Math.min(100, game.seaLevelCm * 0.45 + effRise * 1.1));
|
||
}
|
||
|
||
/* ============================================================
|
||
ABGELEITETE WERTE
|
||
============================================================ */
|
||
|
||
function recalcEffects(game) {
|
||
let co2Red=0, prot=0, upkeep=0, power=0, renewablePower=0;
|
||
for (const id in game.ownedMeasures) {
|
||
const m = findMeasure(id);
|
||
if (!m) continue;
|
||
const entry = game.ownedMeasures[id];
|
||
co2Red += m.co2Reduction * entry.count;
|
||
upkeep += m.upkeep * entry.count;
|
||
power += (m.powerOutput || 0) * entry.count;
|
||
if (id !== 'coal') renewablePower += (m.powerOutput || 0) * entry.count;
|
||
if (id === 'sand-fill') {
|
||
for (const inst of entry.instances) {
|
||
const age = Math.max(0, game.tick - inst.builtAt);
|
||
prot += Math.max(0, m.protection - age * 2);
|
||
}
|
||
} else {
|
||
prot += m.protection * entry.count;
|
||
}
|
||
}
|
||
game.co2Reduction = co2Red;
|
||
game.protection = prot;
|
||
game.upkeepTotal = upkeep;
|
||
game.powerOutput = power;
|
||
game.renewablePower = renewablePower;
|
||
}
|
||
|
||
function getEmissionsThisYear(game) {
|
||
const startEm = 2.5, peakEm = 3.2, ramp = 50;
|
||
if (game.tick < ramp) return startEm + (peakEm - startEm) * (game.tick / ramp);
|
||
return peakEm;
|
||
}
|
||
|
||
function getPowerDemand(game) {
|
||
let demand = Math.max(0, Math.round(game.population / 1000));
|
||
const airport = game.ownedMeasures.airport;
|
||
if (airport && airport.count > 0) demand += airport.count * 3;
|
||
const cs = game.ownedMeasures['cloud-seed'];
|
||
if (cs && cs.count > 0) demand += cs.count * 2;
|
||
return demand;
|
||
}
|
||
|
||
function getIncomeThisYear(game) {
|
||
const popRatio = game.population / 10000;
|
||
return Math.round(game.diff.incomePer10k * popRatio);
|
||
}
|
||
|
||
function getYearlyBalance(game) {
|
||
const popRatio = game.population / 10000;
|
||
const income = Math.round(game.diff.incomePer10k * popRatio);
|
||
const tourism = (game.tourismMode && game.seaLevelCm < 40) ? Math.round(35 * popRatio) : 0;
|
||
const upkeep = game.upkeepTotal;
|
||
let climate = 0;
|
||
if (game.seaLevelCm > 25) {
|
||
const sev = Math.min(1, (game.seaLevelCm - 25) / 40);
|
||
climate = Math.round(sev * 12 * game.diff.climateDamageMul);
|
||
}
|
||
return { income, tourism, upkeep, climate, net: income + tourism - upkeep - climate };
|
||
}
|
||
|
||
/* ============================================================
|
||
ACHIEVEMENTS
|
||
============================================================ */
|
||
|
||
function tryUnlockAchievement(game, key) {
|
||
if (game.achievements.has(key)) return null;
|
||
const a = ACHIEVEMENTS.find(x => x.key === key);
|
||
if (!a) return null;
|
||
game.achievements.add(key);
|
||
game._pendingAchievements.push(a);
|
||
return a;
|
||
}
|
||
|
||
function checkRenewable100(game) {
|
||
if (game.achievements.has('renewable_100')) return;
|
||
const hasCoal = game.ownedMeasures.coal && game.ownedMeasures.coal.count > 0;
|
||
const hasAir = game.ownedMeasures.airport && game.ownedMeasures.airport.count > 0;
|
||
if (hasCoal || hasAir) return;
|
||
const demand = getPowerDemand(game);
|
||
if (game.powerOutput >= demand && demand > 0 && game.tick >= 5) tryUnlockAchievement(game, 'renewable_100');
|
||
}
|
||
|
||
/* ============================================================
|
||
BEVÖLKERUNGSWACHSTUM
|
||
============================================================ */
|
||
|
||
function computePopulationGrowth(game) {
|
||
let measureCount = 0;
|
||
for (const id in game.ownedMeasures) measureCount += game.ownedMeasures[id].count;
|
||
const capBonus = Math.min(1.0, measureCount * 0.05);
|
||
const popCap = game.diff.startPopulation * (1 + capBonus);
|
||
if (game.population >= popCap) return;
|
||
|
||
const tempFactor = game.currentTemp < 16.5 ? 1 : game.currentTemp < 17.5 ? 0.4 : 0;
|
||
const floodFactor = game.floodedPct < 15 ? 1 : game.floodedPct < 35 ? 0.5 : 0;
|
||
const demand = getPowerDemand(game);
|
||
const powerFactor = demand === 0 ? 1 : (game.powerOutput >= demand ? 1 : 0.3);
|
||
const budgetFactor = game.budget > 0 ? 1 : 0.2;
|
||
|
||
const attractiveness = tempFactor * floodFactor * powerFactor * budgetFactor;
|
||
if (attractiveness <= 0) return;
|
||
|
||
const gap = popCap - game.population;
|
||
const growth = Math.round(gap * 0.02 * attractiveness);
|
||
if (growth > 0) game.population += growth;
|
||
}
|
||
|
||
/* ============================================================
|
||
AKTIONEN — kaufen, abreissen
|
||
============================================================ */
|
||
|
||
/**
|
||
* buyMeasure(game, id, options = { resolveMeta, forceNegBalance })
|
||
* Rückgabe:
|
||
* { ok: true, cost, events: [...], unlockedAchievements: [...],
|
||
* firstBuyHint: { icon, text, type, infoKey } | null }
|
||
* { ok: false, reason: 'not-enough-budget' | 'unknown-measure' | 'negative-balance' }
|
||
*
|
||
* Bei `negative-balance` entscheidet die View (Confirm-Dialog) und ruft
|
||
* `buyMeasure(game, id, { forceNegBalance: true, resolveMeta })` erneut.
|
||
*/
|
||
function buyMeasure(game, id, options) {
|
||
options = options || {};
|
||
const m = findMeasure(id);
|
||
if (!m) return { ok: false, reason: 'unknown-measure' };
|
||
|
||
const cost = Math.round(m.cost * game.researchDiscount);
|
||
if (game.budget < cost) {
|
||
return { ok: false, reason: 'not-enough-budget', cost };
|
||
}
|
||
|
||
// Einmalige Negativ-Bilanz-Warnung — View muss confirmieren
|
||
if (!game.negBalanceWarningShown && !options.forceNegBalance) {
|
||
const bal = getYearlyBalance(game);
|
||
const netAfter = bal.income + bal.tourism - (bal.upkeep + m.upkeep) - bal.climate;
|
||
if (netAfter < 0) {
|
||
return { ok: false, reason: 'negative-balance', cost, netAfter, measure: m };
|
||
}
|
||
}
|
||
if (options.forceNegBalance) game.negBalanceWarningShown = true;
|
||
|
||
game._beginEvents();
|
||
game._beginAchievements();
|
||
|
||
game.budget -= cost;
|
||
if (!game.ownedMeasures[id]) game.ownedMeasures[id] = { count:0, instances:[] };
|
||
game.ownedMeasures[id].count++;
|
||
const extra = options.resolveMeta ? options.resolveMeta(id) : null;
|
||
game.ownedMeasures[id].instances.push(Object.assign({ builtAt: game.tick }, extra || {}));
|
||
game.actionLog.push({ tick: game.tick, action:'buy', item:id, cost });
|
||
recalcEffects(game);
|
||
|
||
const evType = m.co2Reduction >= 0.05 ? 'good' : (m.co2Reduction < 0 ? 'bad' : 'neutral');
|
||
pushEvent(game, m.emoji, m.name + ' gebaut (−' + cost + ' Mio €)', evType);
|
||
|
||
if (!game.achievements.has('first_build')) tryUnlockAchievement(game, 'first_build');
|
||
if (id === 'wind' && !game.achievements.has('first_wind')) tryUnlockAchievement(game, 'first_wind');
|
||
if ((id === 'dike' || id === 'sea-wall' || id === 'mangrove') && !game.achievements.has('first_coast')) tryUnlockAchievement(game, 'first_coast');
|
||
if (id === 'forest' && game.ownedMeasures.forest && game.ownedMeasures.forest.count >= 10 && !game.achievements.has('forester')) tryUnlockAchievement(game, 'forester');
|
||
checkRenewable100(game);
|
||
|
||
// First-Buy-Hint bei zweifelhaften Maßnahmen (einmal pro Maßnahme)
|
||
let firstBuyHint = null;
|
||
if (game.ownedMeasures[id].count === 1) {
|
||
const trap = FIRST_BUY_HINTS[id];
|
||
if (trap && !eventFiredOnce(game, 'first-buy-' + id)) {
|
||
firstBuyHint = trap;
|
||
pushEvent(game, trap.icon, trap.text, 'bad', trap.infoKey);
|
||
}
|
||
}
|
||
|
||
return {
|
||
ok: true,
|
||
cost,
|
||
measure: m,
|
||
events: game._drainEvents(),
|
||
unlockedAchievements: game._drainAchievements(),
|
||
firstBuyHint,
|
||
};
|
||
}
|
||
|
||
function demolishMeasure(game, id) {
|
||
const m = findMeasure(id);
|
||
if (!m) return { ok: false, reason: 'unknown-measure' };
|
||
const entry = game.ownedMeasures[id];
|
||
if (!entry || entry.count <= 0) return { ok: false, reason: 'nothing-to-demolish' };
|
||
|
||
game._beginEvents();
|
||
|
||
entry.count--;
|
||
entry.instances.pop();
|
||
if (entry.count === 0) delete game.ownedMeasures[id];
|
||
const refund = Math.round(m.cost * game.researchDiscount * 0.5);
|
||
game.budget += refund;
|
||
game.actionLog.push({ tick: game.tick, action:'demolish', item:id, refund });
|
||
recalcEffects(game);
|
||
pushEvent(game, '🗑', m.name + ' abgerissen (+' + refund + ' Mio €)', 'neutral');
|
||
|
||
return {
|
||
ok: true,
|
||
refund,
|
||
events: game._drainEvents(),
|
||
};
|
||
}
|
||
|
||
/* ============================================================
|
||
BÜRGER-EVENTS
|
||
============================================================ */
|
||
|
||
function maybeCitizenEvent(game) {
|
||
if (game.pendingCitizenEventId) return null;
|
||
const offset = game.diff.citizenEventTickOffset || 0;
|
||
const skipped = new Set(game.diff.skipCitizenEvents || []);
|
||
for (const ev of CITIZEN_EVENTS) {
|
||
if (skipped.has(ev.id)) continue;
|
||
if (game.citizenEventsFired.has(ev.id)) continue;
|
||
if (game.tick !== ev.tick + offset) continue;
|
||
if (!ev.cond(game)) continue;
|
||
// Pending markieren, damit der gleiche Event nicht im nächsten Tick
|
||
// erneut gezeigt wird. `citizenEventsFired` wird erst in
|
||
// applyCitizenChoice gefüllt — so überlebt ein Refresh mitten im
|
||
// Dialog: die View kann den Event wieder zeigen, solange der
|
||
// Spieler*in noch nicht entschieden hat.
|
||
game.pendingCitizenEventId = ev.id;
|
||
return ev;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* applyCitizenChoice(game, eventId, choiceIndex, options = { resolveMeta })
|
||
* Rückgabe: { ok, events, unlockedAchievements, choiceTitle, eventMeta }
|
||
*/
|
||
function applyCitizenChoice(game, eventId, choiceIndex, options) {
|
||
options = options || {};
|
||
const ev = CITIZEN_EVENTS.find(e => e.id === eventId);
|
||
if (!ev) return { ok: false, reason: 'unknown-citizen-event' };
|
||
const choice = ev.choices[choiceIndex];
|
||
if (!choice) return { ok: false, reason: 'invalid-choice' };
|
||
|
||
game._beginEvents();
|
||
game._beginAchievements();
|
||
|
||
const ctx = {
|
||
resolveMeta: options.resolveMeta,
|
||
pushEvent: (icon, text, type, infoKey) => pushEvent(game, icon, text, type, infoKey),
|
||
};
|
||
choice.apply(game, ctx);
|
||
game.actionLog.push({ tick: game.tick, action:'citizen', id: ev.id, choice: choiceIndex });
|
||
recalcEffects(game);
|
||
|
||
pushEvent(game, ev.char, ev.name + ': „' + choice.title + '"', 'neutral');
|
||
// Erst nach der Entscheidung als endgültig erledigt markieren; so kann
|
||
// ein Refresh mitten im Dialog den Event erneut anzeigen.
|
||
game.citizenEventsFired.add(ev.id);
|
||
game.pendingCitizenEventId = null;
|
||
|
||
return {
|
||
ok: true,
|
||
choiceTitle: choice.title,
|
||
eventMeta: { id: ev.id, char: ev.char, name: ev.name },
|
||
events: game._drainEvents(),
|
||
unlockedAchievements: game._drainAchievements(),
|
||
};
|
||
}
|
||
|
||
function getCitizenEvent(eventId) {
|
||
return CITIZEN_EVENTS.find(e => e.id === eventId) || null;
|
||
}
|
||
|
||
/* ============================================================
|
||
NARRATIVE EVENTS (tickweise)
|
||
============================================================ */
|
||
|
||
function tickNarrativeEvents(game) {
|
||
if (game.tick === 4 && !eventFiredOnce(game, 'model-hint'))
|
||
pushEvent(game, '📚', 'Diese Insel steht stellvertretend für das gesamte Weltklima.', 'neutral', 'wedge');
|
||
if (game.tick === 10 && !eventFiredOnce(game, 'warning-1'))
|
||
pushEvent(game, '⚠️', 'Wissenschaftler*innen warnen: CO₂ steigt weiter!', 'bad', 'co2');
|
||
if (game.tick === 30 && game.currentTemp > 15.5 && !eventFiredOnce(game, 'warning-temp1'))
|
||
pushEvent(game, '🌡️', 'Die Temperatur ist um 0,5 °C gestiegen.', 'bad', 'temperature');
|
||
if (game.tick === 50 && game.currentTemp > 16 && !eventFiredOnce(game, 'warning-temp2'))
|
||
pushEvent(game, '🌡️', '+1 °C: Hitzewellen werden häufiger.', 'bad', 'temperature');
|
||
if (game.tick === 80 && game.currentTemp > 17 && !eventFiredOnce(game, 'warning-temp3'))
|
||
pushEvent(game, '🔥', '+2 °C: Pariser Klimaziel überschritten!', 'bad', 'temperature');
|
||
if (game.floodedPct > 8 && game.floodedPct < 20 && !eventFiredOnce(game, 'flood-1'))
|
||
pushEvent(game, '🌊', 'Erste Häuser an der Küste sind betroffen.', 'bad', 'sealevel');
|
||
if (game.floodedPct > 40 && !eventFiredOnce(game, 'flood-2'))
|
||
pushEvent(game, '🌊', 'Massive Überflutung — viele verlieren ihr Zuhause!', 'bad', 'sealevel');
|
||
if (game.seaLevelCm > 20 && !eventFiredOnce(game, 'vegetation-dying'))
|
||
pushEvent(game, '🌿', 'Mangroven und Küstenwälder sterben — Salzwasser versalzt die Böden.', 'bad', 'vegetation');
|
||
if (game.seaLevelCm > 35 && !eventFiredOnce(game, 'drinking-water'))
|
||
pushEvent(game, '💧', 'Die Brunnen liefern immer mehr salziges Wasser.', 'bad', 'drinking_water');
|
||
if (game.currentTemp > 16.8 && !eventFiredOnce(game, 'glacier-melt'))
|
||
pushEvent(game, '🏔', 'Der Gletscher am Vulkan schrumpft merklich.', 'bad', 'glacier');
|
||
if (game.tick === 60 && game.budget > 500 && !eventFiredOnce(game, 'praise-1'))
|
||
pushEvent(game, '👏', 'Die Bevölkerung lobt deine kluge Haushaltsführung.', 'good');
|
||
if (game.tick === 100 && game.co2Reduction > 3 && !eventFiredOnce(game, 'praise-co2'))
|
||
pushEvent(game, '🌿', 'Deine Klimaschutzmaßnahmen zeigen Wirkung!', 'good');
|
||
if (game.tick === 120 && game.currentTemp < 16 && !eventFiredOnce(game, 'praise-temp'))
|
||
pushEvent(game, '🏆', 'Internationale Anerkennung für deine Klimapolitik!', 'good');
|
||
|
||
const cs = game.ownedMeasures['cloud-seed'];
|
||
if (cs && cs.count > 0 && cs.instances.length > 0 && !eventFiredOnce(game, 'cloud-seed-verdict')) {
|
||
const builtAt = cs.instances[0].builtAt;
|
||
if (game.tick - builtAt >= 30) {
|
||
pushEvent(game, '☁️',
|
||
'Wolken-Impfung nach 30 Jahren: reine Scheinlösung. Wirkung minimal, Geld besser in echte Klimaschutzmaßnahmen investieren.',
|
||
'bad', 'cloud_seeding');
|
||
}
|
||
}
|
||
const ap = game.ownedMeasures['airport'];
|
||
if (ap && ap.count > 0 && ap.instances.length > 0 && !eventFiredOnce(game, 'airport-verdict')) {
|
||
const builtAt = ap.instances[0].builtAt;
|
||
if (game.tick - builtAt >= 20) {
|
||
pushEvent(game, '✈️',
|
||
'Tourismus-Bilanz: Einnahmen stehen gegen zusätzlichen CO₂-Ausstoß. Die Temperatur-Kurve wäre ohne Flughafen niedriger.',
|
||
'bad', 'aviation');
|
||
}
|
||
}
|
||
const coal = game.ownedMeasures['coal'];
|
||
if (coal && coal.count > 0 && coal.instances.length > 0 && !eventFiredOnce(game, 'coal-verdict')) {
|
||
const builtAt = coal.instances[0].builtAt;
|
||
if (game.tick - builtAt >= 16) {
|
||
pushEvent(game, '🏭',
|
||
'Kohle-Bilanz: dein Kraftwerk hat jetzt '+(coal.count*Math.abs(-0.38).toFixed(2))+' ppm/Jahr zusätzliches CO₂ in die Luft gepumpt. Abschalten und auf Wind/Solar umsteigen?',
|
||
'bad', 'coal_power');
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
TICK — Ein Jahr simulieren
|
||
============================================================ */
|
||
|
||
/**
|
||
* tick(game) — Rückgabe:
|
||
* {
|
||
* newEvents: [...],
|
||
* unlockedAchievements: [...],
|
||
* citizenEvent: citizenEventObject | null,
|
||
* endReason: 'won' | 'pleite' | 'ueberflutet' | null,
|
||
* timelineSample: { tick, co2, temp, budget, sea, power, demand }
|
||
* }
|
||
*/
|
||
function tick(game) {
|
||
if (game.levelCompleted) return { newEvents: [], unlockedAchievements: [], citizenEvent: null, endReason: null };
|
||
|
||
game._beginEvents();
|
||
game._beginAchievements();
|
||
|
||
recalcEffects(game);
|
||
|
||
const popRatio = game.population / 10000;
|
||
game.budget += Math.round(game.diff.incomePer10k * popRatio);
|
||
|
||
if (game.tourismMode) {
|
||
if (game.seaLevelCm < 40) {
|
||
game.budget += Math.round(35 * popRatio);
|
||
game.co2Ppm += 0.6;
|
||
} else {
|
||
game.population -= Math.round(60 * popRatio);
|
||
if (!eventFiredOnce(game, 'tourism-collapse')) {
|
||
pushEvent(game, '🏖', 'Der Strand ist vom Meer verschluckt — der Tourismus bricht zusammen!', 'bad', 'sealevel');
|
||
}
|
||
}
|
||
}
|
||
|
||
game.budget -= game.upkeepTotal;
|
||
|
||
const emissions = getEmissionsThisYear(game);
|
||
const reduction = game.co2Reduction;
|
||
game.co2Ppm = Math.max(CO2_FLOOR, game.co2Ppm + emissions - reduction);
|
||
|
||
computeClimate(game);
|
||
|
||
// Strom-Bilanz / Blackout
|
||
const powerDemand = getPowerDemand(game);
|
||
if (powerDemand > game.powerOutput) {
|
||
game.blackoutStreak++;
|
||
if (!eventFiredOnce(game, 'blackout-first')) {
|
||
pushEvent(game, '⚡', 'Zu wenig Strom! Baue schnell ein Kraftwerk, sonst wird es bald kalt.', 'bad', 'blackout');
|
||
}
|
||
if (game.blackoutStreak > game.diff.blackoutGrace) {
|
||
game.treesChoppedForHeat += game.diff.treesPerBlackout;
|
||
game.co2Ppm += game.diff.co2BlackoutPerYear;
|
||
const forest = game.ownedMeasures.forest;
|
||
if (forest && forest.count > 0) {
|
||
const kill = Math.min(forest.count, game.diff.treesPerBlackout);
|
||
forest.count -= kill;
|
||
for (let k=0; k<kill; k++) forest.instances.pop();
|
||
if (forest.count === 0) delete game.ownedMeasures.forest;
|
||
}
|
||
}
|
||
const thr = game.diff.blackoutGrace + 3;
|
||
if (game.blackoutStreak >= thr) {
|
||
const drift = Math.round(25 * popRatio * game.diff.popLossMul * (game.blackoutStreak - thr + 1));
|
||
game.population -= drift;
|
||
}
|
||
} else {
|
||
if (game.blackoutStreak > game.diff.blackoutGrace && !eventFiredOnce(game, 'blackout-recovered')) {
|
||
pushEvent(game, '💡', 'Der Strom ist wieder da. Die gefällten Bäume kommen aber nicht zurück.', 'neutral');
|
||
}
|
||
game.blackoutStreak = 0;
|
||
}
|
||
|
||
// Überflutungs-Schäden — Multiplikator gesenkt (5 → 3), weil die Flut-Kurve
|
||
// jetzt stetig ist und mittlere Läufe höhere %-Werte zeigen als früher.
|
||
if (game.floodedPct > 0) {
|
||
game.population -= Math.round(game.floodedPct * 3 * game.diff.popLossMul);
|
||
}
|
||
if (game.seaLevelCm > 25) {
|
||
const sev = Math.min(1, (game.seaLevelCm - 25) / 40);
|
||
game.budget -= Math.round(sev * 12 * game.diff.climateDamageMul);
|
||
if (Math.random() < sev * game.diff.popLossMul) {
|
||
game.population -= Math.round(sev * 15 * game.diff.popLossMul);
|
||
}
|
||
}
|
||
|
||
if (game.population < 0) game.population = 0;
|
||
|
||
tickNarrativeEvents(game);
|
||
|
||
const citizenEv = maybeCitizenEvent(game);
|
||
|
||
computePopulationGrowth(game);
|
||
|
||
const timelineSample = {
|
||
tick: game.tick,
|
||
co2: game.co2Ppm,
|
||
temp: game.currentTemp,
|
||
budget: game.budget,
|
||
sea: game.seaLevelCm,
|
||
power: game.powerOutput,
|
||
demand: getPowerDemand(game),
|
||
};
|
||
game.timeline.push(timelineSample);
|
||
|
||
checkRenewable100(game);
|
||
if (game.currentTemp < 16.5 && game.tick > 30 && !game.achievements.has('under_1_5')) tryUnlockAchievement(game, 'under_1_5');
|
||
|
||
game.tick++;
|
||
|
||
// End-Bedingungen
|
||
let endReason = null;
|
||
const budgetLoss = (game.levelConfig && typeof game.levelConfig.budgetLoss === 'number') ? game.levelConfig.budgetLoss : -500;
|
||
// Hitze-Katastrophe: > 18,5 °C = Klima-System ausser Kontrolle → Niederlage,
|
||
// unabhängig vom restlichen Spielstand. „Überlebt, aber Klima zerstört" ist
|
||
// didaktisch kein Sieg.
|
||
if (game.currentTemp >= 18.5) {
|
||
endReason = 'hitze';
|
||
game.levelCompleted = true;
|
||
game.levelWon = false;
|
||
} else if (game.tick >= game.maxTick) {
|
||
endReason = 'won';
|
||
game.levelCompleted = true;
|
||
game.levelWon = true;
|
||
tryUnlockAchievement(game, 'survivor');
|
||
if (game.budget > 100) tryUnlockAchievement(game, 'frugal');
|
||
} else if (game.budget < budgetLoss) {
|
||
endReason = 'pleite';
|
||
game.levelCompleted = true;
|
||
game.levelWon = false;
|
||
} else if (game.floodedPct > 60) {
|
||
endReason = 'ueberflutet';
|
||
game.levelCompleted = true;
|
||
game.levelWon = false;
|
||
}
|
||
|
||
return {
|
||
newEvents: game._drainEvents(),
|
||
unlockedAchievements: game._drainAchievements(),
|
||
citizenEvent: citizenEv,
|
||
endReason,
|
||
timelineSample,
|
||
};
|
||
}
|
||
|
||
/* ============================================================
|
||
SPIEL ERZEUGEN / ZURÜCKSETZEN
|
||
============================================================ */
|
||
|
||
function createGame(levelId, opts) {
|
||
opts = opts || {};
|
||
const diff = DIFFICULTY[levelId] || DIFFICULTY[1];
|
||
const game = {
|
||
// Simulations-State
|
||
tick: 0,
|
||
maxTick: opts.maxTick || 150,
|
||
startYear: opts.startYear || 2025,
|
||
difficulty: levelId,
|
||
diff,
|
||
levelConfig: opts.levelConfig || null,
|
||
|
||
budget: diff.startBudget,
|
||
population: diff.startPopulation,
|
||
co2Ppm: 425,
|
||
currentTemp: 15.5,
|
||
targetTemp: 15.5,
|
||
seaLevelCm: 0, floodedPct: 0,
|
||
seaFastCm: 0, committedSeaCm: 0,
|
||
|
||
ownedMeasures: {},
|
||
co2Reduction: 0, protection: 0, upkeepTotal: 0, powerOutput: 0, renewablePower: 0,
|
||
|
||
// Stapel + Historie
|
||
timeline: [],
|
||
actionLog: [],
|
||
events: [],
|
||
firedEvents: new Set(),
|
||
citizenEventsFired: new Set(),
|
||
achievements: new Set(),
|
||
|
||
// Flags
|
||
researchDiscount: 1.0,
|
||
tourismMode: false, hasMountainShield: false, hasSandBeach: false,
|
||
negBalanceWarningShown: false,
|
||
blackoutStreak: 0, treesChoppedForHeat: 0,
|
||
|
||
pendingCitizenEventId: null,
|
||
levelCompleted: false, levelWon: false,
|
||
startTime: Date.now(),
|
||
lastProgressTick: -1,
|
||
|
||
// Event-/Achievement-Puffer, pro Aufruf geleert (internal)
|
||
_pendingEvents: [],
|
||
_pendingAchievements: [],
|
||
_beginEvents: function() { this._pendingEvents = []; },
|
||
_drainEvents: function() { const out = this._pendingEvents; this._pendingEvents = []; return out; },
|
||
_beginAchievements: function() { this._pendingAchievements = []; },
|
||
_drainAchievements: function() { const out = this._pendingAchievements; this._pendingAchievements = []; return out; },
|
||
};
|
||
|
||
// Klima konsistent initialisieren
|
||
game.currentTemp = computeTemperatureFromCO2(game.co2Ppm);
|
||
game.targetTemp = game.currentTemp;
|
||
recalcEffects(game);
|
||
return game;
|
||
}
|
||
|
||
/* ============================================================
|
||
SERIALIZE / DESERIALIZE
|
||
============================================================ */
|
||
|
||
function serialize(game) {
|
||
return {
|
||
v: 4,
|
||
tick: game.tick,
|
||
maxTick: game.maxTick,
|
||
startYear: game.startYear,
|
||
difficulty: game.difficulty,
|
||
levelConfig: game.levelConfig,
|
||
budget: game.budget, population: game.population,
|
||
co2Ppm: game.co2Ppm, currentTemp: game.currentTemp, targetTemp: game.targetTemp,
|
||
seaLevelCm: game.seaLevelCm, floodedPct: game.floodedPct,
|
||
seaFastCm: game.seaFastCm, committedSeaCm: game.committedSeaCm,
|
||
ownedMeasures: game.ownedMeasures,
|
||
timeline: game.timeline, events: game.events,
|
||
firedEvents: Array.from(game.firedEvents),
|
||
citizenEventsFired: Array.from(game.citizenEventsFired),
|
||
achievements: Array.from(game.achievements),
|
||
pendingCitizenEventId: game.pendingCitizenEventId,
|
||
researchDiscount: game.researchDiscount,
|
||
tourismMode: game.tourismMode, hasMountainShield: game.hasMountainShield,
|
||
hasSandBeach: game.hasSandBeach,
|
||
negBalanceWarningShown: game.negBalanceWarningShown,
|
||
blackoutStreak: game.blackoutStreak,
|
||
treesChoppedForHeat: game.treesChoppedForHeat,
|
||
lastProgressTick: game.lastProgressTick,
|
||
levelCompleted: game.levelCompleted, levelWon: game.levelWon,
|
||
savedAt: Date.now(),
|
||
};
|
||
}
|
||
|
||
function deserialize(data) {
|
||
if (!data || typeof data !== 'object') return null;
|
||
const game = createGame(data.difficulty || 1, {
|
||
maxTick: data.maxTick,
|
||
startYear: data.startYear,
|
||
levelConfig: data.levelConfig,
|
||
});
|
||
game.tick = data.tick | 0;
|
||
game.budget = data.budget || 0;
|
||
game.population = data.population || 0;
|
||
game.co2Ppm = data.co2Ppm || 425;
|
||
game.currentTemp = data.currentTemp || 15.5;
|
||
game.targetTemp = data.targetTemp || game.currentTemp;
|
||
game.seaLevelCm = data.seaLevelCm || 0;
|
||
game.seaFastCm = data.seaFastCm || 0;
|
||
game.committedSeaCm = data.committedSeaCm || 0;
|
||
game.floodedPct = data.floodedPct || 0;
|
||
game.ownedMeasures = data.ownedMeasures || {};
|
||
game.timeline = Array.isArray(data.timeline) ? data.timeline : [];
|
||
game.events = Array.isArray(data.events) ? data.events : [];
|
||
game.firedEvents = new Set(data.firedEvents || []);
|
||
game.citizenEventsFired = new Set(data.citizenEventsFired || []);
|
||
game.achievements = new Set(data.achievements || []);
|
||
game.pendingCitizenEventId = data.pendingCitizenEventId || null;
|
||
game.researchDiscount = data.researchDiscount != null ? data.researchDiscount : 1.0;
|
||
game.tourismMode = !!data.tourismMode;
|
||
game.hasMountainShield = !!data.hasMountainShield;
|
||
game.hasSandBeach = !!data.hasSandBeach;
|
||
game.negBalanceWarningShown = !!data.negBalanceWarningShown;
|
||
game.blackoutStreak = data.blackoutStreak || 0;
|
||
game.treesChoppedForHeat= data.treesChoppedForHeat || 0;
|
||
game.lastProgressTick = data.lastProgressTick != null ? data.lastProgressTick : -1;
|
||
game.levelCompleted = !!data.levelCompleted;
|
||
game.levelWon = !!data.levelWon;
|
||
recalcEffects(game);
|
||
return game;
|
||
}
|
||
|
||
/* ============================================================
|
||
PUBLIC API
|
||
============================================================ */
|
||
|
||
window.KlimaEngine = {
|
||
// Konstanten
|
||
MEASURES,
|
||
DIFFICULTY,
|
||
ACHIEVEMENTS,
|
||
CITIZEN_EVENTS,
|
||
FIRST_BUY_HINTS,
|
||
CO2_FLOOR,
|
||
TEMP_FLOOR,
|
||
|
||
// Lebenszyklus
|
||
createGame,
|
||
serialize,
|
||
deserialize,
|
||
|
||
// Ableitung
|
||
recalcEffects,
|
||
computeTemperatureFromCO2,
|
||
computeClimate,
|
||
getEmissionsThisYear,
|
||
getPowerDemand,
|
||
getIncomeThisYear,
|
||
getYearlyBalance,
|
||
computePopulationGrowth,
|
||
|
||
// Aktionen
|
||
buyMeasure,
|
||
demolishMeasure,
|
||
|
||
// Tick
|
||
tick,
|
||
|
||
// Bürger-Events
|
||
maybeCitizenEvent,
|
||
applyCitizenChoice,
|
||
getCitizenEvent,
|
||
|
||
// Achievements
|
||
checkRenewable100,
|
||
tryUnlockAchievement,
|
||
|
||
// Helpers
|
||
findMeasure,
|
||
pushEvent,
|
||
};
|
||
})();
|