diff --git a/App/assets/data/lg-locations.json b/App/assets/data/lg-locations.json index 5d7534f..d52e8f1 100644 --- a/App/assets/data/lg-locations.json +++ b/App/assets/data/lg-locations.json @@ -1,15 +1,59 @@ [ - { "id": "wien", "name": "Wien", "countryId": "AT", "regionId": "central_europe", "type": "CAPITAL", "lat": 48.2082, "lon": 16.3738, "visibleFromLevel": 1, "didacticDifficulty": 1 }, - { "id": "muenchen", "name": "München", "countryId": "DE", "regionId": "central_europe", "type": "CAPITAL", "lat": 48.1351, "lon": 11.5820, "visibleFromLevel": 1, "didacticDifficulty": 1 }, - { "id": "hamburg", "name": "Hamburg", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 53.5511, "lon": 9.9937, "visibleFromLevel": 1, "didacticDifficulty": 1 }, - { "id": "hamburg_hafen","name": "Hamburg Hafen", "countryId": "DE", "regionId": "central_europe", "type": "PORT", "lat": 53.5461, "lon": 9.9661, "visibleFromLevel": 2, "didacticDifficulty": 2 }, - { "id": "rotterdam", "name": "Rotterdam", "countryId": "NL", "regionId": "benelux", "type": "CITY", "lat": 51.9244, "lon": 4.4777, "visibleFromLevel": 2, "didacticDifficulty": 2 }, - { "id": "rotterdam_hafen","name":"Rotterdam Hafen","countryId":"NL","regionId":"benelux", "type": "PORT", "lat": 51.9519, "lon": 4.1378, "visibleFromLevel": 2, "didacticDifficulty": 2 }, - { "id": "paris", "name": "Paris", "countryId": "FR", "regionId": "western_europe", "type": "CAPITAL", "lat": 48.8566, "lon": 2.3522, "visibleFromLevel": 2, "didacticDifficulty": 1 }, - { "id": "berlin", "name": "Berlin", "countryId": "DE", "regionId": "central_europe", "type": "CAPITAL", "lat": 52.5200, "lon": 13.4050, "visibleFromLevel": 1, "didacticDifficulty": 1 }, - { "id": "mailand", "name": "Mailand", "countryId": "IT", "regionId": "southern_europe","type": "CITY", "lat": 45.4642, "lon": 9.1900, "visibleFromLevel": 2, "didacticDifficulty": 2 }, - { "id": "salzburg", "name": "Salzburg", "countryId": "AT", "regionId": "central_europe", "type": "CITY", "lat": 47.8095, "lon": 13.0550, "visibleFromLevel": 1, "didacticDifficulty": 1 }, - { "id": "warschau", "name": "Warschau", "countryId": "PL", "regionId": "central_europe", "type": "CAPITAL", "lat": 52.2297, "lon": 21.0122, "visibleFromLevel": 2, "didacticDifficulty": 2 }, - { "id": "madrid", "name": "Madrid", "countryId": "ES", "regionId": "iberia", "type": "CAPITAL", "lat": 40.4168, "lon": -3.7038, "visibleFromLevel": 3, "didacticDifficulty": 3 }, - { "id": "kopenhagen", "name": "Kopenhagen", "countryId": "DK", "regionId": "scandinavia", "type": "CAPITAL", "lat": 55.6761, "lon": 12.5683, "visibleFromLevel": 2, "didacticDifficulty": 2 } + { "id": "wien", "name": "Wien", "countryId": "AT", "regionId": "central_europe", "type": "CAPITAL", "lat": 48.2082, "lon": 16.3738, "didacticInfo": "Hauptstadt Österreichs; rund 2 Mio. Einwohner im Großraum. Wichtiger Verkehrsknoten zwischen Mittel- und Südosteuropa." }, + { "id": "muenchen", "name": "München", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 48.1351, "lon": 11.5820, "didacticInfo": "Drittgrößte Stadt Deutschlands, Wirtschaftsdrehkreuz Süddeutschlands. Starke Automobil- und Maschinenbau-Industrie." }, + { "id": "berlin", "name": "Berlin", "countryId": "DE", "regionId": "central_europe", "type": "CAPITAL", "lat": 52.5200, "lon": 13.4050, "didacticInfo": "Hauptstadt Deutschlands, größte Stadt des Landes. Zentrum für Politik, Tech und Kultur." }, + { "id": "hamburg", "name": "Hamburg", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 53.5511, "lon": 9.9937, "didacticInfo": "Zweitgrößte Stadt Deutschlands. Tor zur Welt durch den größten deutschen Seehafen." }, + { "id": "hamburg_hafen", "name": "Hamburg Hafen", "countryId": "DE", "regionId": "central_europe", "type": "PORT", "lat": 53.5461, "lon": 9.9661, "didacticInfo": "Drittgrößter Seehafen Europas, ca. 130 Mio. Tonnen Umschlag pro Jahr. Spezialisiert auf Container." }, + { "id": "rotterdam", "name": "Rotterdam", "countryId": "NL", "regionId": "benelux", "type": "CITY", "lat": 51.9244, "lon": 4.4777, "didacticInfo": "Zweitgrößte Stadt der Niederlande, maritime Drehscheibe. Wohnt direkt am Mündungsdelta von Rhein und Maas." }, + { "id": "rotterdam_hafen","name": "Rotterdam Hafen","countryId": "NL", "regionId": "benelux", "type": "PORT", "lat": 51.9519, "lon": 4.1378, "didacticInfo": "Größter Seehafen Europas, ca. 470 Mio. Tonnen Umschlag pro Jahr. Wichtigstes Import-Tor für Rohstoffe aus Übersee." }, + { "id": "paris", "name": "Paris", "countryId": "FR", "regionId": "western_europe", "type": "CAPITAL", "lat": 48.8566, "lon": 2.3522, "didacticInfo": "Hauptstadt Frankreichs, Großraum 12 Mio. Einwohner. Wirtschaftsmotor Frankreichs." }, + { "id": "mailand", "name": "Mailand", "countryId": "IT", "regionId": "southern_europe","type": "CITY", "lat": 45.4642, "lon": 9.1900, "didacticInfo": "Wirtschaftsmetropole Italiens, Finanz- und Modezentrum. Zugang zum Alpentransit über den Brenner." }, + { "id": "salzburg", "name": "Salzburg", "countryId": "AT", "regionId": "central_europe", "type": "CITY", "lat": 47.8095, "lon": 13.0550, "didacticInfo": "Landeshauptstadt im Salzkammergut, wichtiger Ost-West-Transit-Knoten auf der A1." }, + { "id": "warschau", "name": "Warschau", "countryId": "PL", "regionId": "central_europe", "type": "CAPITAL", "lat": 52.2297, "lon": 21.0122, "didacticInfo": "Hauptstadt Polens, 1,8 Mio. Einwohner. Wirtschaftszentrum zwischen West- und Osteuropa." }, + { "id": "madrid", "name": "Madrid", "countryId": "ES", "regionId": "iberia", "type": "CAPITAL", "lat": 40.4168, "lon": -3.7038, "didacticInfo": "Hauptstadt Spaniens, geographisches Zentrum der iberischen Halbinsel. Großer Binnenmarkt ohne direkten Seezugang." }, + { "id": "kopenhagen", "name": "Kopenhagen", "countryId": "DK", "regionId": "scandinavia", "type": "CAPITAL", "lat": 55.6761, "lon": 12.5683, "didacticInfo": "Hauptstadt Dänemarks. Knoten zwischen Skandinavien und dem europäischen Kontinent (Öresundbrücke)." }, + { "id": "rom", "name": "Rom", "countryId": "IT", "regionId": "southern_europe","type": "CAPITAL", "lat": 41.9028, "lon": 12.4964, "didacticInfo": "Hauptstadt Italiens, 2,8 Mio. Einwohner. Verwaltungszentrum, weniger Industrie als Mailand." }, + { "id": "london", "name": "London", "countryId": "GB", "regionId": "western_europe", "type": "CAPITAL", "lat": 51.5074, "lon": -0.1278, "didacticInfo": "Hauptstadt Großbritanniens, Finanzmetropole. Seit Brexit (2020) mit Zollkontrolle zur EU." }, + { "id": "bruessel", "name": "Brüssel", "countryId": "BE", "regionId": "benelux", "type": "CAPITAL", "lat": 50.8503, "lon": 4.3517, "didacticInfo": "Hauptstadt Belgiens und de-facto der EU. Sitz von EU-Kommission und NATO." }, + { "id": "amsterdam", "name": "Amsterdam", "countryId": "NL", "regionId": "benelux", "type": "CAPITAL", "lat": 52.3676, "lon": 4.9041, "didacticInfo": "Hauptstadt der Niederlande. Luftfahrt-Drehkreuz (Flughafen Schiphol), Agrarexport-Zentrum." }, + { "id": "prag", "name": "Prag", "countryId": "CZ", "regionId": "central_europe", "type": "CAPITAL", "lat": 50.0755, "lon": 14.4378, "didacticInfo": "Hauptstadt Tschechiens. Industriestandort, enge Verflechtung mit deutscher Automobil-Lieferkette." }, + { "id": "budapest", "name": "Budapest", "countryId": "HU", "regionId": "central_europe", "type": "CAPITAL", "lat": 47.4979, "lon": 19.0402, "didacticInfo": "Hauptstadt Ungarns, Donaumetropole. Wichtig für Verkehr Richtung Südosteuropa und Schwarzes Meer." }, + { "id": "athen", "name": "Athen", "countryId": "GR", "regionId": "southern_europe","type": "CAPITAL", "lat": 37.9838, "lon": 23.7275, "didacticInfo": "Hauptstadt Griechenlands, mit Seehafen Piräus zweitgrößter Containerhafen des Mittelmeers." }, + { "id": "stockholm", "name": "Stockholm", "countryId": "SE", "regionId": "scandinavia", "type": "CAPITAL", "lat": 59.3293, "lon": 18.0686, "didacticInfo": "Hauptstadt Schwedens, Inselstadt auf 14 Inseln. Hightech- und Pharma-Zentrum." }, + { "id": "oslo", "name": "Oslo", "countryId": "NO", "regionId": "scandinavia", "type": "CAPITAL", "lat": 59.9139, "lon": 10.7522, "didacticInfo": "Hauptstadt Norwegens. Erdöl- und Schifffahrts-Drehscheibe, kein EU-Mitglied (aber EWR)." }, + { "id": "helsinki", "name": "Helsinki", "countryId": "FI", "regionId": "scandinavia", "type": "CAPITAL", "lat": 60.1699, "lon": 24.9384, "didacticInfo": "Hauptstadt Finnlands, nördlichste EU-Hauptstadt am Festland. Tor zum Baltikum und nach St. Petersburg." }, + { "id": "dublin", "name": "Dublin", "countryId": "IE", "regionId": "western_europe", "type": "CAPITAL", "lat": 53.3498, "lon": -6.2603, "didacticInfo": "Hauptstadt Irlands. EU-Standort vieler Tech-Konzerne, verkehrstechnisch Insellage." }, + { "id": "lissabon", "name": "Lissabon", "countryId": "PT", "regionId": "iberia", "type": "CAPITAL", "lat": 38.7223, "lon": -9.1393, "didacticInfo": "Hauptstadt Portugals, westlichste Hauptstadt des europäischen Festlandes. Atlantik-Hafen." }, + { "id": "bern", "name": "Bern", "countryId": "CH", "regionId": "central_europe", "type": "CAPITAL", "lat": 46.9481, "lon": 7.4474, "didacticInfo": "Hauptstadt der Schweiz. Nicht EU, daher eigenes Zollsystem — wichtig für logistische Abwicklung." }, + + { "id": "frankfurt", "name": "Frankfurt a. M.","countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 50.1109, "lon": 8.6821, "didacticInfo": "Finanzmetropole und zentrale Logistik-Drehscheibe Deutschlands. Frankfurt-Flughafen: größtes Luftfracht-Drehkreuz des Kontinents." }, + { "id": "koeln", "name": "Köln", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 50.9375, "lon": 6.9603, "didacticInfo": "Großstadt am Rhein, Autobahn-Drehkreuz A1/A3/A4. Wichtig für Rhein-Güterverkehr und Binnenschifffahrt." }, + { "id": "stuttgart", "name": "Stuttgart", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 48.7758, "lon": 9.1829, "didacticInfo": "Landeshauptstadt Baden-Württemberg, Zentrum der Automobilindustrie (Mercedes, Porsche). Lieferketten weltweit." }, + { "id": "leipzig", "name": "Leipzig", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 51.3397, "lon": 12.3731, "didacticInfo": "Logistik-Drehkreuz Ostdeutschland, DHL-Hub am Flughafen Leipzig/Halle. Zugknoten Ost-West." }, + { "id": "nuernberg", "name": "Nürnberg", "countryId": "DE", "regionId": "central_europe", "type": "CITY", "lat": 49.4521, "lon": 11.0767, "didacticInfo": "Größte Stadt Frankens, Bayern-Nord-Süd-Knoten A9/A3. Wichtiger Güterverkehrs-Knoten im Schienennetz." }, + { "id": "innsbruck", "name": "Innsbruck", "countryId": "AT", "regionId": "central_europe", "type": "CITY", "lat": 47.2692, "lon": 11.4041, "didacticInfo": "Alpenstadt in Tirol, Brenner-Nordknoten. Über 2 Mio. LKW pro Jahr durch den Brenner-Korridor Richtung Italien." }, + { "id": "graz", "name": "Graz", "countryId": "AT", "regionId": "central_europe", "type": "CITY", "lat": 47.0707, "lon": 15.4395, "didacticInfo": "Zweitgrößte Stadt Österreichs, Landeshauptstadt Steiermark. Starke Automobilzulieferer (Magna Steyr)." }, + { "id": "zuerich", "name": "Zürich", "countryId": "CH", "regionId": "central_europe", "type": "CITY", "lat": 47.3769, "lon": 8.5417, "didacticInfo": "Größte Stadt der Schweiz, Finanzplatz. Nördlicher Eingang zu den Alpentransit-Routen (Gotthard)." }, + { "id": "turin", "name": "Turin", "countryId": "IT", "regionId": "southern_europe","type": "CITY", "lat": 45.0703, "lon": 7.6869, "didacticInfo": "Viertgrößte Stadt Italiens, traditionell Fiat/Stellantis-Heimat. Westlicher Alpenquerungs-Knoten (Mont Blanc, Fréjus)." }, + { "id": "neapel", "name": "Neapel", "countryId": "IT", "regionId": "southern_europe","type": "CITY", "lat": 40.8518, "lon": 14.2681, "didacticInfo": "Drittgrößte Stadt Italiens, Süditalienisches Wirtschaftszentrum am Vesuv. Bedeutender Mittelmeerhafen." }, + { "id": "lyon", "name": "Lyon", "countryId": "FR", "regionId": "western_europe", "type": "CITY", "lat": 45.7640, "lon": 4.8357, "didacticInfo": "Zweit-/Drittgrößte Stadt Frankreichs, Rhône-Tal-Knoten. Autobahnkreuz A6/A7 — Paris nach Südfrankreich/Italien." }, + { "id": "marseille", "name": "Marseille", "countryId": "FR", "regionId": "western_europe", "type": "CITY", "lat": 43.2965, "lon": 5.3698, "didacticInfo": "Zweitgrößte Stadt Frankreichs, bedeutendster französischer Mittelmeerhafen. Tor zu Nordafrika und Nahost." }, + { "id": "barcelona", "name": "Barcelona", "countryId": "ES", "regionId": "iberia", "type": "CITY", "lat": 41.3851, "lon": 2.1734, "didacticInfo": "Zweitgrößte Stadt Spaniens, Katalonien. Mittelmeer-Hafenstadt, südwestliches Tor zu Nordeuropa." }, + { "id": "valencia", "name": "Valencia", "countryId": "ES", "regionId": "iberia", "type": "CITY", "lat": 39.4699, "lon": -0.3763, "didacticInfo": "Drittgrößte Stadt Spaniens, Orangenexport-Zentrum. Containerhafen unter den Top 5 des Mittelmeers." }, + { "id": "porto", "name": "Porto", "countryId": "PT", "regionId": "iberia", "type": "CITY", "lat": 41.1579, "lon": -8.6291, "didacticInfo": "Zweitgrößte Stadt Portugals, Dourotal. Exportstadt (Portwein, Textilien), Hafen Leixões in der Nähe." }, + { "id": "manchester", "name": "Manchester", "countryId": "GB", "regionId": "western_europe", "type": "CITY", "lat": 53.4808, "lon": -2.2426, "didacticInfo": "Drittgrößter Großraum Großbritanniens, ehemalige Industrie-Hochburg. Heute Logistik und Dienstleistungen." }, + { "id": "krakau", "name": "Krakau", "countryId": "PL", "regionId": "central_europe", "type": "CITY", "lat": 50.0647, "lon": 19.9450, "didacticInfo": "Zweitgrößte Stadt Polens, Süd-Polens Wirtschaftszentrum. Autobahnkreuz A4 Richtung Deutschland/Ukraine." }, + { "id": "thessaloniki", "name": "Thessaloniki", "countryId": "GR", "regionId": "southern_europe","type": "CITY", "lat": 40.6401, "lon": 22.9444, "didacticInfo": "Zweitgrößte Stadt Griechenlands, Hafen für den Balkan. Tor zu Nord-Griechenland und Mazedonien." }, + + { "id": "antwerpen_hafen","name": "Antwerpen Hafen","countryId": "BE", "regionId": "benelux", "type": "PORT", "lat": 51.2296, "lon": 4.4020, "didacticInfo": "Zweitgrößter Seehafen Europas, an der Schelde. Spezialisiert auf Chemie-Umschlag und Rohöl." }, + { "id": "bremerhaven_hafen","name":"Bremerhaven","countryId": "DE", "regionId": "central_europe", "type": "PORT", "lat": 53.5396, "lon": 8.5810, "didacticInfo": "Wichtigster Auto-Umschlagplatz Europas, ca. 2 Mio. Fahrzeuge pro Jahr. Tiefwasserhafen an der Wesermündung." }, + { "id": "duisburg_terminal","name":"Duisburg","countryId": "DE", "regionId": "central_europe", "type": "TERMINAL","lat": 51.4344, "lon": 6.7623, "didacticInfo": "Europas größter Binnenhafen am Rhein-Ruhr-Zusammenfluss. Neue Seidenstraßen-Endstation für chinesische Züge." }, + { "id": "basel_terminal", "name": "Basel Terminal", "countryId": "CH", "regionId": "central_europe", "type": "TERMINAL","lat": 47.5596, "lon": 7.5886, "didacticInfo": "Größter Rheinhafen der Schweiz. Seehafen-Ersatz für Binnenland Schweiz über Rhein bis Rotterdam." }, + { "id": "marseille_hafen","name": "Marseille Hafen","countryId": "FR", "regionId": "western_europe", "type": "PORT", "lat": 43.3485, "lon": 5.2954, "didacticInfo": "Größter französischer Hafen, Mittelmeer-Eingang. Container, Kreuzfahrten, Rohölpipeline nach Deutschland." }, + { "id": "barcelona_hafen","name": "Barcelona Hafen","countryId": "ES", "regionId": "iberia", "type": "PORT", "lat": 41.3443, "lon": 2.1700, "didacticInfo": "Größter Mittelmeerhafen Spaniens. Strategisch an der Route Asien-Europa, Umschlagplatz für Afrika-Handel." }, + { "id": "genua_hafen", "name": "Genua Hafen", "countryId": "IT", "regionId": "southern_europe","type": "PORT", "lat": 44.4056, "lon": 8.8691, "didacticInfo": "Wichtigster italienischer Seehafen. Tor für Nord-Italien und Süddeutschland Richtung Mittelmeer." }, + { "id": "triest_hafen", "name": "Triest Hafen", "countryId": "IT", "regionId": "southern_europe","type": "PORT", "lat": 45.6495, "lon": 13.7680, "didacticInfo": "Schnellster Mittelmeer-Zugang für Zentraleuropa. Wichtiger Öl-Hub und wachsender Container-Hafen." }, + { "id": "piraeus_hafen", "name": "Piräus", "countryId": "GR", "regionId": "southern_europe","type": "PORT", "lat": 37.9475, "lon": 23.6364, "didacticInfo": "Haupthafen Athens, viertgrößter Container-Hafen Europas. Teilweise in chinesischem Besitz (COSCO), Tor Richtung Asien." }, + { "id": "gdansk_hafen", "name": "Danzig Hafen", "countryId": "PL", "regionId": "central_europe", "type": "PORT", "lat": 54.3520, "lon": 18.6466, "didacticInfo": "Größter polnischer Ostsee-Hafen. Wichtig für Container-Verkehr und Kohle-Umschlag, Tiefwasser-Terminal." }, + { "id": "goeteborg_hafen","name": "Göteborg Hafen", "countryId": "SE", "regionId": "scandinavia", "type": "PORT", "lat": 57.6882, "lon": 11.8462, "didacticInfo": "Größter Hafen Skandinaviens. Hauptumschlag für die nordischen Länder, 90 % aller schwedischen Container." } ] diff --git a/App/assets/data/lg-railnet.json b/App/assets/data/lg-railnet.json index 7fbc01a..f3b76fc 100644 --- a/App/assets/data/lg-railnet.json +++ b/App/assets/data/lg-railnet.json @@ -1,17 +1 @@ -{ - "_comment": "Seed-Bahnnetz aus Pflichtenheft Kap 65.8. Logistik-Instanz erweitert in Phase 4.", - "nodes": [ - { "id": "wien", "locationId": "wien", "name": "Wien Hbf" }, - { "id": "muenchen", "locationId": "muenchen", "name": "München Hbf" }, - { "id": "hamburg", "locationId": "hamburg", "name": "Hamburg Hbf" }, - { "id": "rotterdam", "locationId": "rotterdam", "name": "Rotterdam Centraal" }, - { "id": "paris", "locationId": "paris", "name": "Paris Gare du Nord" } - ], - "edges": [ - { "id": "wien-muenchen", "from": "wien", "to": "muenchen", "distanceKm": 400, "durationMinutes": 267 }, - { "id": "muenchen-hamburg", "from": "muenchen", "to": "hamburg", "distanceKm": 800, "durationMinutes": 533 }, - { "id": "hamburg-rotterdam", "from": "hamburg", "to": "rotterdam", "distanceKm": 500, "durationMinutes": 333 }, - { "id": "rotterdam-paris", "from": "rotterdam","to": "paris", "distanceKm": 520, "durationMinutes": 347 }, - { "id": "paris-muenchen", "from": "paris", "to": "muenchen", "distanceKm": 820, "durationMinutes": 547 } - ] -} +{"_comment":"Seed-Bahnnetz aus Pflichtenheft Kap 65.8. Logistik-Instanz erweitert in Phase 4.","nodes":[{"id":"wien","locationId":"wien","name":"Wien Hbf"},{"id":"muenchen","locationId":"muenchen","name":"München Hbf"},{"id":"hamburg","locationId":"hamburg","name":"Hamburg Hbf"},{"id":"rotterdam","locationId":"rotterdam","name":"Rotterdam Centraal"},{"id":"paris","locationId":"paris","name":"Paris Gare du Nord"}],"edges":[{"id":"wien-muenchen","from":"wien","to":"muenchen","distanceKm":400,"durationMinutes":267,"polyline":[[48.20782,16.37372],[48.20698,16.37275],[48.20446,16.37771],[48.20184,16.375],[48.20272,16.36799],[48.19877,16.36339],[48.19573,16.35458],[48.19042,16.35218],[48.18842,16.33856],[48.18411,16.33187],[48.18408,16.3271],[48.1881,16.31315],[48.18823,16.30322],[48.19351,16.26797],[48.20125,16.25092],[48.20052,16.24289],[48.20461,16.22989],[48.20465,16.22505],[48.20176,16.21318],[48.19343,16.20375],[48.18017,16.17592],[48.17348,16.13293],[48.17636,16.11758],[48.1755,16.10222],[48.17914,16.09176],[48.17928,16.08636],[48.17634,16.07726],[48.17629,16.06539],[48.16854,16.02898],[48.1617,16.01572],[48.16206,15.99711],[48.15413,15.98606],[48.1525,15.978],[48.1585,15.95291],[48.15526,15.92901],[48.1542,15.8988],[48.15812,15.89044],[48.16938,15.88127],[48.17265,15.87341],[48.17388,15.84569],[48.17864,15.82581],[48.18431,15.81011],[48.18544,15.79468],[48.18418,15.77591],[48.1865,15.7458],[48.18596,15.68519],[48.17622,15.64677],[48.178,15.54862],[48.1688,15.50324],[48.16834,15.49246],[48.17091,15.47696],[48.18021,15.45325],[48.18585,15.43279],[48.19038,15.3937],[48.19938,15.37008],[48.20273,15.36606],[48.21206,15.35978],[48.21501,15.35554],[48.21785,15.32614],[48.21367,15.31029],[48.21488,15.28967],[48.2121,15.2812],[48.19818,15.25376],[48.1981,15.23077],[48.19129,15.20115],[48.18512,15.19054],[48.17371,15.18317],[48.16519,15.16233],[48.16224,15.15047],[48.15842,15.10959],[48.13663,15.05891],[48.13364,15.04776],[48.12888,15.00967],[48.13664,14.97677],[48.13903,14.95314],[48.1387,14.9296],[48.14694,14.90744],[48.14592,14.8673],[48.14382,14.85323],[48.13366,14.83162],[48.12642,14.82255],[48.12034,14.80647],[48.11882,14.77931],[48.12128,14.75491],[48.11658,14.73081],[48.11212,14.68048],[48.11863,14.64761],[48.11572,14.63253],[48.11605,14.62634],[48.12837,14.60706],[48.13394,14.58762],[48.15133,14.56152],[48.1801,14.54372],[48.19709,14.51865],[48.20793,14.43673],[48.2212,14.39945],[48.23313,14.3765],[48.23666,14.35367],[48.23325,14.32405],[48.19336,14.23767],[48.19274,14.22895],[48.19729,14.20932],[48.19675,14.19637],[48.18956,14.16566],[48.17686,14.13549],[48.1747,14.12535],[48.17652,14.11041],[48.18888,14.08256],[48.19177,14.06048],[48.19016,14.03455],[48.18364,14.01553],[48.17902,13.99186],[48.17985,13.98381],[48.18405,13.97332],[48.18802,13.95518],[48.18934,13.92778],[48.18778,13.91693],[48.18004,13.89952],[48.17743,13.88569],[48.17945,13.86061],[48.17602,13.84039],[48.18236,13.8261],[48.18367,13.81822],[48.18196,13.8057],[48.18293,13.79069],[48.17848,13.77104],[48.17571,13.70545],[48.18788,13.68081],[48.19037,13.65791],[48.19298,13.64932],[48.21337,13.62729],[48.22464,13.60859],[48.23174,13.59312],[48.23826,13.5727],[48.24338,13.53603],[48.24757,13.52226],[48.25175,13.51657],[48.26045,13.50961],[48.27246,13.49398],[48.29302,13.48355],[48.30224,13.47115],[48.30614,13.45859],[48.30918,13.43276],[48.31473,13.42268],[48.31942,13.41851],[48.3206,13.41925],[48.3194,13.4202],[48.31726,13.40733],[48.31982,13.38957],[48.32521,13.37779],[48.33066,13.37063],[48.32504,13.36193],[48.31055,13.3489],[48.30456,13.34012],[48.30072,13.32098],[48.29436,13.31049],[48.27754,13.27157],[48.2749,13.25915],[48.26963,13.2491],[48.26312,13.24237],[48.25906,13.21684],[48.25604,13.21244],[48.25146,13.21193],[48.24969,13.20885],[48.24897,13.18853],[48.25307,13.17862],[48.25382,13.17041],[48.25096,13.15446],[48.25698,13.14047],[48.25506,13.1185],[48.24791,13.09362],[48.25191,13.08772],[48.25388,13.07618],[48.2409,13.03802],[48.23903,13.0285],[48.23989,13.02533],[48.24608,13.01883],[48.25105,13.00339],[48.23202,12.97308],[48.22856,12.9617],[48.22758,12.95002],[48.23013,12.93227],[48.2429,12.88945],[48.24439,12.87547],[48.24434,12.83867],[48.24813,12.80891],[48.2513,12.79699],[48.25102,12.77146],[48.2474,12.744],[48.24858,12.73575],[48.25337,12.72564],[48.25472,12.71898],[48.25449,12.69613],[48.25162,12.68367],[48.25291,12.63657],[48.25566,12.62644],[48.26662,12.60481],[48.27183,12.58859],[48.27736,12.52859],[48.27703,12.51317],[48.27257,12.48637],[48.27114,12.46001],[48.26724,12.45268],[48.25764,12.44768],[48.25307,12.44233],[48.24641,12.41485],[48.25073,12.38982],[48.24775,12.36564],[48.24798,12.33928],[48.25318,12.31034],[48.24972,12.28095],[48.2511,12.25281],[48.25512,12.23494],[48.2522,12.21037],[48.25922,12.18552],[48.26041,12.17088],[48.25391,12.14563],[48.25588,12.12497],[48.249,12.09489],[48.24794,12.05149],[48.24561,12.02702],[48.24333,12.01848],[48.23554,12.00311],[48.23319,11.97856],[48.23024,11.97224],[48.22156,11.96231],[48.2147,11.94868],[48.20247,11.93072],[48.18881,11.91983],[48.181,11.90918],[48.16942,11.8895],[48.16396,11.87346],[48.15926,11.83281],[48.15142,11.80833],[48.14936,11.78735],[48.14272,11.76911],[48.1422,11.69944],[48.13723,11.67741],[48.13941,11.65736],[48.13838,11.62453],[48.13573,11.59789],[48.13161,11.58767],[48.13298,11.58369],[48.13519,11.58213]],"distanceKmOsrm":403.0},{"id":"muenchen-hamburg","from":"muenchen","to":"hamburg","distanceKm":800,"durationMinutes":533,"polyline":[[48.13519,11.58213],[48.13603,11.5792],[48.13848,11.5833],[48.13725,11.58974],[48.1395,11.59314],[48.14807,11.59621],[48.15758,11.60402],[48.15913,11.60373],[48.16349,11.59634],[48.17159,11.60009],[48.17583,11.59358],[48.18192,11.59906],[48.18536,11.60866],[48.18915,11.6133],[48.25431,11.64606],[48.25983,11.64738],[48.28519,11.63901],[48.36267,11.59534],[48.38833,11.59737],[48.40366,11.58992],[48.44139,11.58936],[48.45729,11.59099],[48.47477,11.58528],[48.47905,11.58634],[48.49082,11.59399],[48.49566,11.59495],[48.50028,11.59336],[48.51114,11.58425],[48.51795,11.58289],[48.52394,11.58484],[48.53227,11.59366],[48.5358,11.59534],[48.54723,11.59375],[48.55752,11.58858],[48.56806,11.59241],[48.57691,11.59261],[48.58116,11.58878],[48.58782,11.57196],[48.61319,11.53998],[48.62127,11.52749],[48.63385,11.52338],[48.64829,11.51511],[48.67044,11.50785],[48.69488,11.49169],[48.72163,11.47922],[48.75035,11.45819],[48.75542,11.45742],[48.77267,11.46113],[48.78726,11.46027],[48.8037,11.4657],[48.81434,11.46681],[48.82765,11.47303],[48.85218,11.46487],[48.87333,11.4682],[48.88243,11.46622],[48.89263,11.46693],[48.90519,11.47041],[48.91569,11.47],[48.92585,11.46634],[48.94001,11.45053],[48.95177,11.43094],[48.96697,11.41576],[48.9793,11.39587],[48.99388,11.38066],[49.00301,11.35766],[49.00851,11.35463],[49.01167,11.35492],[49.01994,11.36588],[49.02375,11.3677],[49.03827,11.35805],[49.04086,11.35404],[49.04522,11.33933],[49.0517,11.33066],[49.06183,11.32206],[49.07438,11.29748],[49.07926,11.29156],[49.08384,11.28895],[49.10451,11.28351],[49.13278,11.27073],[49.17585,11.2605],[49.21763,11.22664],[49.24254,11.21808],[49.27917,11.2095],[49.31458,11.20757],[49.37854,11.20041],[49.38957,11.20219],[49.40759,11.19558],[49.41517,11.19911],[49.45141,11.23708],[49.45524,11.23846],[49.45926,11.23709],[49.46322,11.23214],[49.46594,11.22404],[49.46479,11.20068],[49.46693,11.19075],[49.48324,11.169],[49.49767,11.13999],[49.50645,11.11834],[49.51407,11.09057],[49.53005,11.05911],[49.54107,11.02303],[49.54473,11.01704],[49.55347,11.00871],[49.55741,11.00194],[49.55996,10.99125],[49.56145,10.959],[49.56814,10.94105],[49.57485,10.93294],[49.58288,10.92784],[49.59101,10.92634],[49.60871,10.92745],[49.63968,10.91297],[49.66034,10.89236],[49.67842,10.86821],[49.68716,10.86167],[49.70238,10.85366],[49.71096,10.84598],[49.73215,10.81461],[49.73585,10.80603],[49.73792,10.79537],[49.73829,10.78044],[49.73128,10.72338],[49.7313,10.67352],[49.73345,10.66482],[49.74065,10.64997],[49.74462,10.63049],[49.75906,10.59937],[49.75895,10.56886],[49.76254,10.54818],[49.7641,10.51985],[49.77044,10.47118],[49.77295,10.46213],[49.78347,10.4411],[49.78568,10.43031],[49.78456,10.41743],[49.77578,10.39016],[49.77414,10.3743],[49.7754,10.34852],[49.77421,10.319],[49.7817,10.29145],[49.78311,10.28013],[49.78021,10.2477],[49.78109,10.2182],[49.7888,10.17702],[49.78532,10.14871],[49.78479,10.11832],[49.78075,10.09496],[49.78708,10.08058],[49.79222,10.06202],[49.79854,10.05161],[49.8087,10.04429],[49.82346,10.04329],[49.83021,10.04002],[49.83927,10.02831],[49.85222,10.00055],[49.86082,9.99408],[49.87112,9.99209],[49.89509,10.00135],[49.91831,9.99695],[49.92537,9.99775],[49.93201,10.00066],[49.93796,10.00645],[49.94542,10.02035],[49.95063,10.02663],[49.95622,10.03013],[49.97081,10.03429],[49.9789,10.04078],[49.98493,10.05061],[49.99249,10.07237],[49.9981,10.0781],[50.00496,10.07738],[50.01526,10.06724],[50.02861,10.07036],[50.03805,10.06828],[50.05177,10.05541],[50.06189,10.04951],[50.06676,10.04413],[50.07188,10.03271],[50.08252,9.99063],[50.08576,9.98657],[50.09822,9.97885],[50.1096,9.9595],[50.11411,9.95773],[50.12528,9.95856],[50.14289,9.94971],[50.14614,9.9514],[50.15043,9.95861],[50.15454,9.96187],[50.16468,9.9641],[50.17283,9.96213],[50.17797,9.95817],[50.18993,9.94406],[50.20078,9.9377],[50.20638,9.9322],[50.21815,9.90691],[50.23913,9.87591],[50.24562,9.87398],[50.25869,9.88193],[50.26751,9.88124],[50.27622,9.87483],[50.28261,9.86007],[50.28907,9.85326],[50.29741,9.84907],[50.31025,9.8484],[50.31546,9.84534],[50.31911,9.83902],[50.3234,9.82172],[50.3229,9.79956],[50.32547,9.78677],[50.32993,9.77879],[50.34101,9.76669],[50.35048,9.74395],[50.35752,9.73607],[50.36781,9.73259],[50.38451,9.73443],[50.40415,9.73125],[50.41624,9.73461],[50.43192,9.74388],[50.44101,9.74395],[50.44563,9.74078],[50.46415,9.71926],[50.4733,9.71275],[50.47881,9.71151],[50.48344,9.71266],[50.48894,9.71673],[50.4998,9.7321],[50.50562,9.73745],[50.51277,9.74045],[50.51995,9.74042],[50.53642,9.73103],[50.55509,9.73122],[50.56682,9.726],[50.57875,9.7135],[50.59431,9.68609],[50.60086,9.6809],[50.6141,9.6826],[50.62826,9.6761],[50.64383,9.68299],[50.66496,9.67664],[50.67106,9.67762],[50.68471,9.68324],[50.69665,9.6818],[50.70625,9.67395],[50.71909,9.65252],[50.73658,9.64294],[50.74686,9.63246],[50.75549,9.61713],[50.76375,9.59306],[50.76708,9.58968],[50.77389,9.58758],[50.77688,9.58374],[50.78514,9.55326],[50.788,9.55005],[50.79581,9.5461],[50.80169,9.53562],[50.80599,9.53554],[50.81473,9.54514],[50.81829,9.55248],[50.81865,9.56376],[50.82264,9.5708],[50.84617,9.5847],[50.86451,9.59126],[50.87543,9.60047],[50.88058,9.60069],[50.88585,9.5951],[50.88715,9.58495],[50.88938,9.58062],[50.89338,9.57932],[50.90179,9.58576],[50.90637,9.58517],[50.92606,9.54631],[50.93938,9.54169],[50.94227,9.53926],[50.94454,9.53349],[50.94524,9.52243],[50.9493,9.51263],[50.95723,9.49985],[50.9608,9.48632],[50.96555,9.48017],[50.97105,9.4788],[50.97528,9.47372],[50.97991,9.47198],[50.9879,9.47193],[50.99815,9.47506],[51.00755,9.48659],[51.01123,9.4889],[51.022,9.48952],[51.02817,9.48511],[51.03161,9.48485],[51.04827,9.49736],[51.06089,9.49474],[51.08304,9.48531],[51.09508,9.48325],[51.10093,9.48531],[51.1079,9.48301],[51.11756,9.48466],[51.12156,9.48224],[51.12729,9.475],[51.13082,9.4736],[51.14907,9.4761],[51.15563,9.48112],[51.15873,9.48175],[51.17282,9.47327],[51.18512,9.47382],[51.20173,9.46687],[51.22322,9.47935],[51.22799,9.48404],[51.24892,9.51625],[51.25232,9.51752],[51.26561,9.51673],[51.27025,9.5196],[51.29455,9.56105],[51.29945,9.56284],[51.31014,9.56004],[51.3201,9.56493],[51.34493,9.60072],[51.36253,9.61914],[51.37515,9.65395],[51.39263,9.66547],[51.39621,9.66986],[51.39865,9.67881],[51.39728,9.69635],[51.40619,9.71806],[51.40795,9.72905],[51.40643,9.74378],[51.39615,9.76969],[51.39518,9.77945],[51.40821,9.82165],[51.41996,9.84149],[51.43744,9.84949],[51.45575,9.86686],[51.46505,9.87059],[51.49755,9.87802],[51.56285,9.87991],[51.56783,9.8859],[51.57204,9.90319],[51.58047,9.91225],[51.58508,9.91199],[51.59344,9.90542],[51.59849,9.90406],[51.6151,9.90892],[51.6326,9.92032],[51.64256,9.92153],[51.65913,9.9156],[51.67849,9.926],[51.68657,9.92838],[51.69646,9.93715],[51.71446,9.94084],[51.72196,9.94751],[51.72771,9.9574],[51.73632,9.98071],[51.73886,9.99735],[51.74162,10.00426],[51.74532,10.00799],[51.75934,10.01455],[51.78155,10.03489],[51.78892,10.05063],[51.79529,10.08115],[51.81961,10.10872],[51.84936,10.123],[51.9136,10.13507],[51.93353,10.14527],[51.94069,10.1453],[51.95576,10.14026],[51.96287,10.14096],[51.98049,10.15268],[52.00597,10.15911],[52.01996,10.1684],[52.03685,10.17545],[52.05223,10.18862],[52.06338,10.19359],[52.06927,10.19391],[52.07493,10.19223],[52.08588,10.18242],[52.09238,10.17176],[52.10356,10.14526],[52.11719,10.12336],[52.12062,10.10842],[52.12228,10.06688],[52.13138,10.03962],[52.13652,10.01412],[52.14048,10.00366],[52.14817,9.99448],[52.17065,9.98285],[52.17832,9.97695],[52.18474,9.96825],[52.19711,9.9454],[52.2041,9.93748],[52.21153,9.9343],[52.23456,9.93194],[52.25643,9.91923],[52.27683,9.91094],[52.28857,9.90012],[52.30738,9.87304],[52.31477,9.86731],[52.32346,9.86394],[52.33275,9.86385],[52.3413,9.86696],[52.34867,9.8728],[52.36335,9.89049],[52.37989,9.89766],[52.39756,9.89983],[52.4152,9.89479],[52.42957,9.88467],[52.44937,9.86345],[52.45562,9.86037],[52.46659,9.85827],[52.47331,9.85472],[52.50237,9.82641],[52.52447,9.81422],[52.54392,9.79926],[52.56667,9.78523],[52.57555,9.77659],[52.59628,9.75],[52.653,9.7046],[52.67626,9.69248],[52.71868,9.68163],[52.74533,9.66854],[52.75562,9.66579],[52.77128,9.66698],[52.81917,9.68302],[52.84047,9.68744],[52.8488,9.69272],[52.85513,9.70201],[52.86733,9.73564],[52.88628,9.76053],[52.89988,9.78328],[52.91757,9.80551],[52.92324,9.81702],[52.94118,9.86491],[52.97867,9.92411],[52.98668,9.92903],[53.00903,9.92974],[53.04032,9.95077],[53.06968,9.957],[53.1119,9.9846],[53.13103,9.99287],[53.13732,9.99943],[53.14301,10.00937],[53.15417,10.04834],[53.16024,10.06216],[53.16983,10.07408],[53.18275,10.0827],[53.19019,10.08351],[53.20086,10.0797],[53.22495,10.07587],[53.23438,10.07805],[53.25232,10.08742],[53.26279,10.08763],[53.31069,10.07032],[53.3338,10.04161],[53.33875,10.03941],[53.36338,10.0363],[53.3678,10.03252],[53.37306,10.0229],[53.38034,10.01733],[53.39847,10.02603],[53.41687,10.03069],[53.42698,10.03492],[53.44182,10.0302],[53.47761,10.02253],[53.48182,10.024],[53.50118,10.03916],[53.50847,10.03644],[53.52564,10.02237],[53.54023,10.03264],[53.54758,10.01393],[53.54812,10.00632],[53.5527,10.00552],[53.55583,10.0006],[53.55111,9.99367]],"distanceKmOsrm":776.5},{"id":"hamburg-rotterdam","from":"hamburg","to":"rotterdam","distanceKm":500,"durationMinutes":333,"polyline":[[53.55111,9.99367],[53.55046,9.996],[53.54118,9.99965],[53.5386,10.02165],[53.53786,10.02319],[53.53601,10.02215],[53.53366,10.0273],[53.52795,10.02265],[53.52475,10.02235],[53.51113,10.03418],[53.50318,10.039],[53.49898,10.03791],[53.47902,10.02241],[53.4418,10.02995],[53.42705,10.03468],[53.41745,10.03062],[53.39818,10.02566],[53.38567,10.02001],[53.38127,10.01515],[53.38012,9.97485],[53.37118,9.88817],[53.37306,9.86276],[53.36935,9.75072],[53.32867,9.60244],[53.29753,9.52825],[53.28458,9.47924],[53.25189,9.42283],[53.24308,9.40214],[53.23979,9.38951],[53.23523,9.35521],[53.22875,9.33416],[53.21064,9.31073],[53.16828,9.24871],[53.14195,9.22317],[53.11639,9.20725],[53.08973,9.17794],[53.08475,9.16835],[53.06157,9.10196],[53.05701,9.08056],[53.05099,9.00938],[53.03947,8.97814],[53.03015,8.95902],[53.0279,8.95005],[53.02819,8.93465],[53.03737,8.90942],[53.03875,8.88918],[53.03592,8.87514],[53.02457,8.8453],[53.02231,8.79256],[53.01334,8.75271],[53.00242,8.6827],[52.99483,8.65972],[52.97461,8.62031],[52.96571,8.59906],[52.94429,8.5231],[52.9377,8.50895],[52.92511,8.48844],[52.92037,8.47677],[52.9158,8.45259],[52.91367,8.39841],[52.90969,8.3689],[52.90391,8.34754],[52.88975,8.31126],[52.88247,8.27092],[52.87237,8.2492],[52.85938,8.23306],[52.84278,8.22059],[52.79079,8.20261],[52.74409,8.17739],[52.72586,8.17299],[52.68418,8.17239],[52.66215,8.16549],[52.64193,8.15252],[52.60851,8.12133],[52.59441,8.11235],[52.57923,8.10955],[52.54676,8.11394],[52.52951,8.11053],[52.51919,8.10296],[52.49559,8.07674],[52.47789,8.05999],[52.4605,8.04667],[52.44099,8.03556],[52.42909,8.03182],[52.41691,8.031],[52.40583,8.03326],[52.38047,8.04331],[52.37123,8.04323],[52.36391,8.04067],[52.34904,8.02912],[52.33872,8.01442],[52.32661,7.97386],[52.31569,7.95439],[52.30291,7.94397],[52.28263,7.93685],[52.27089,7.92842],[52.2685,7.92445],[52.26729,7.90068],[52.26156,7.86407],[52.26033,7.83913],[52.25206,7.80244],[52.2528,7.78934],[52.25941,7.75972],[52.25698,7.72155],[52.25768,7.70945],[52.26923,7.66219],[52.28223,7.63374],[52.28632,7.61897],[52.29109,7.53701],[52.29426,7.5182],[52.31102,7.47283],[52.32145,7.43323],[52.32613,7.42345],[52.33634,7.41119],[52.34082,7.40241],[52.34398,7.38862],[52.34555,7.37024],[52.34445,7.35034],[52.33671,7.30987],[52.34189,7.25278],[52.34407,7.20198],[52.34294,7.18163],[52.3375,7.14231],[52.33533,7.10005],[52.32901,7.07548],[52.31422,7.04523],[52.30692,6.98312],[52.30398,6.97515],[52.29621,6.96225],[52.28918,6.93963],[52.28832,6.92427],[52.29427,6.89808],[52.29437,6.88356],[52.2915,6.87169],[52.27851,6.84352],[52.27675,6.83458],[52.27785,6.82347],[52.28866,6.78929],[52.28852,6.7781],[52.28441,6.75524],[52.28515,6.745],[52.28855,6.73621],[52.31175,6.70028],[52.30825,6.6872],[52.29652,6.65888],[52.2851,6.59801],[52.28324,6.51611],[52.27386,6.48121],[52.25563,6.43922],[52.25243,6.42647],[52.25025,6.3987],[52.24612,6.3764],[52.24598,6.35467],[52.24306,6.32127],[52.24299,6.27976],[52.23362,6.17574],[52.22797,6.15222],[52.21848,6.12774],[52.21505,6.11081],[52.20898,6.09434],[52.1988,6.04985],[52.18662,6.01902],[52.1803,5.99163],[52.17298,5.96956],[52.17327,5.95667],[52.17872,5.93848],[52.18211,5.91578],[52.18856,5.85284],[52.19206,5.84187],[52.2018,5.82486],[52.20416,5.81182],[52.19085,5.69459],[52.17198,5.63107],[52.16761,5.56539],[52.16321,5.52952],[52.16293,5.51204],[52.16776,5.44693],[52.17388,5.43199],[52.1766,5.42741],[52.1778,5.42801],[52.17694,5.4292],[52.17138,5.42409],[52.15036,5.41486],[52.13949,5.40455],[52.13528,5.39825],[52.12473,5.34631],[52.11724,5.32279],[52.11439,5.29552],[52.10511,5.25281],[52.10253,5.21764],[52.09305,5.20013],[52.09386,5.15961],[52.09187,5.1575],[52.08758,5.16194],[52.08059,5.16063],[52.06668,5.14658],[52.05741,5.14235],[52.05511,5.13753],[52.0621,5.08503],[52.06926,5.05469],[52.08066,4.98765],[52.08045,4.93523],[52.07046,4.89753],[52.0698,4.84162],[52.06463,4.81277],[52.0673,4.75525],[52.06658,4.74549],[52.0622,4.73333],[52.04217,4.70761],[52.03284,4.68925],[52.0221,4.65433],[52.01823,4.65209],[52.01069,4.65597],[52.00156,4.65222],[51.98121,4.62074],[51.95672,4.56565],[51.95152,4.52775],[51.95004,4.52745],[51.94637,4.53145],[51.92885,4.53823],[51.9143,4.53347],[51.91607,4.52006],[51.91964,4.51457],[51.92325,4.49821],[51.91803,4.48127],[51.92413,4.4782]],"distanceKmOsrm":503.0},{"id":"rotterdam-paris","from":"rotterdam","to":"paris","distanceKm":520,"durationMinutes":347,"polyline":[[51.92413,4.4782],[51.92543,4.48239],[51.9243,4.48866],[51.92038,4.49113],[51.92245,4.4969],[51.92015,4.49903],[51.91591,4.51532],[51.91341,4.53569],[51.89931,4.54618],[51.89215,4.56042],[51.88047,4.56593],[51.87503,4.57033],[51.85522,4.59898],[51.83299,4.62039],[51.81641,4.63059],[51.81097,4.64519],[51.80702,4.64977],[51.79299,4.64874],[51.77744,4.64997],[51.75938,4.64511],[51.73208,4.63232],[51.72469,4.63235],[51.69265,4.6536],[51.66806,4.67627],[51.60612,4.71951],[51.59654,4.72348],[51.58189,4.72435],[51.56413,4.73167],[51.55404,4.73798],[51.53727,4.74043],[51.5227,4.74509],[51.49917,4.74209],[51.47559,4.72993],[51.45137,4.72046],[51.41386,4.70236],[51.4053,4.69443],[51.38408,4.66598],[51.33425,4.60649],[51.32483,4.5901],[51.29117,4.5209],[51.27172,4.49869],[51.2671,4.48419],[51.2685,4.46554],[51.26714,4.44652],[51.26476,4.43806],[51.26101,4.43177],[51.25335,4.4264],[51.23847,4.4265],[51.22549,4.44722],[51.21882,4.45086],[51.19656,4.43421],[51.19349,4.42994],[51.19059,4.41726],[51.19175,4.40475],[51.19972,4.37863],[51.21269,4.36253],[51.21408,4.35604],[51.21344,4.3493],[51.20478,4.32671],[51.18744,4.29622],[51.17357,4.26118],[51.15949,4.23292],[51.14455,4.18788],[51.13401,4.13356],[51.12881,4.08874],[51.12586,4.07687],[51.12103,4.06799],[51.10188,4.04551],[51.08257,4.01278],[51.07798,4.00129],[51.07488,3.98481],[51.06863,3.89188],[51.05384,3.83828],[51.03593,3.7582],[51.02383,3.73532],[51.00384,3.72126],[50.99378,3.71208],[50.98998,3.70656],[50.9866,3.69615],[50.97626,3.62662],[50.95881,3.57038],[50.92251,3.50296],[50.88352,3.46262],[50.87344,3.44734],[50.84055,3.34938],[50.80339,3.25966],[50.79433,3.23947],[50.78336,3.21996],[50.77128,3.19187],[50.76169,3.15814],[50.75735,3.15098],[50.74564,3.14118],[50.73497,3.12836],[50.72875,3.12494],[50.70843,3.12223],[50.69053,3.11128],[50.67127,3.11445],[50.6602,3.11354],[50.64851,3.09872],[50.6307,3.0819],[50.62407,3.08444],[50.61987,3.08093],[50.61752,3.08109],[50.60389,3.10111],[50.60017,3.10326],[50.59632,3.10269],[50.55569,3.06349],[50.51415,3.03372],[50.50861,3.02582],[50.49096,2.9915],[50.48257,2.9826],[50.44945,2.97065],[50.44093,2.97264],[50.42784,2.98244],[50.42095,2.985],[50.3918,2.98206],[50.3841,2.97983],[50.35916,2.95931],[50.33873,2.92698],[50.30828,2.89314],[50.25778,2.86104],[50.24697,2.85583],[50.23827,2.85458],[50.22986,2.85588],[50.18201,2.87422],[50.07942,2.87006],[50.04907,2.87551],[50.03361,2.88344],[50.0245,2.88422],[50.00763,2.87645],[49.99168,2.87332],[49.95769,2.85445],[49.90619,2.84958],[49.88558,2.83815],[49.83388,2.82874],[49.8082,2.81608],[49.79302,2.80404],[49.7551,2.79206],[49.71031,2.77225],[49.67551,2.77187],[49.63383,2.75649],[49.60299,2.75396],[49.52951,2.72292],[49.47205,2.7105],[49.44632,2.69707],[49.4087,2.69375],[49.38047,2.70312],[49.36423,2.70379],[49.35095,2.70089],[49.32077,2.68731],[49.30899,2.68409],[49.29938,2.68924],[49.27981,2.69563],[49.27414,2.69534],[49.2493,2.67841],[49.23896,2.66305],[49.22649,2.6494],[49.21351,2.62129],[49.20596,2.6112],[49.19436,2.60346],[49.18187,2.60518],[49.17298,2.60008],[49.16251,2.59755],[49.12978,2.55886],[49.10909,2.55181],[49.09938,2.55011],[49.06322,2.55215],[49.04779,2.54663],[49.00742,2.52713],[49.00155,2.52305],[48.98844,2.50879],[48.96464,2.47419],[48.95339,2.4736],[48.9374,2.47998],[48.92753,2.48134],[48.91376,2.47701],[48.89973,2.4694],[48.88878,2.47387],[48.88648,2.47331],[48.88399,2.46873],[48.88364,2.45867],[48.86605,2.42187],[48.86497,2.39555],[48.86293,2.38725],[48.85983,2.38876],[48.85779,2.38065],[48.85872,2.37858],[48.85552,2.36989],[48.85325,2.3686],[48.85724,2.35232]],"distanceKmOsrm":443.8},{"id":"paris-muenchen","from":"paris","to":"muenchen","distanceKm":820,"durationMinutes":547,"polyline":[[48.85724,2.35232],[48.85672,2.34896],[48.85214,2.36093],[48.84949,2.36164],[48.82286,2.39599],[48.81735,2.41536],[48.81652,2.45268],[48.82075,2.46133],[48.82628,2.46592],[48.83014,2.48849],[48.82648,2.52233],[48.83656,2.54359],[48.83444,2.58548],[48.82989,2.61611],[48.83237,2.66145],[48.82868,2.7296],[48.83336,2.79483],[48.83561,2.80814],[48.83986,2.82006],[48.84534,2.8295],[48.86087,2.84492],[48.86758,2.85396],[48.8791,2.88351],[48.89712,2.91382],[48.9084,2.9451],[48.91366,2.9743],[48.92815,3.01917],[48.93301,3.02597],[48.95699,3.04941],[48.9662,3.05628],[48.98141,3.07774],[49.00919,3.14498],[49.01678,3.15701],[49.0291,3.1689],[49.03721,3.18983],[49.04051,3.20636],[49.03997,3.23923],[49.04603,3.26529],[49.05034,3.30758],[49.05566,3.32198],[49.06955,3.34111],[49.07453,3.35171],[49.07714,3.36341],[49.07787,3.38746],[49.08013,3.40046],[49.10437,3.45955],[49.11268,3.49859],[49.12459,3.52884],[49.1279,3.55001],[49.12906,3.59166],[49.13211,3.60394],[49.14238,3.62839],[49.14673,3.64423],[49.15,3.69011],[49.15362,3.70946],[49.1604,3.72721],[49.16838,3.73946],[49.1775,3.74765],[49.19682,3.75851],[49.20246,3.7651],[49.20675,3.77335],[49.21775,3.80733],[49.2274,3.82526],[49.23152,3.83724],[49.23467,3.86257],[49.24264,3.88953],[49.24262,3.91275],[49.24784,3.94263],[49.24738,3.95207],[49.24357,3.96417],[49.23982,3.96822],[49.22734,3.97043],[49.22099,3.97572],[49.20621,4.01221],[49.20477,4.02588],[49.20966,4.04142],[49.21069,4.0503],[49.20614,4.0854],[49.1986,4.11463],[49.19137,4.13458],[49.17492,4.16494],[49.1662,4.18496],[49.14525,4.21139],[49.13146,4.22432],[49.11684,4.24773],[49.10825,4.25361],[49.09461,4.25635],[49.07403,4.26594],[49.05571,4.2865],[49.0496,4.2976],[49.04192,4.32587],[49.03933,4.34459],[49.03416,4.36159],[49.03364,4.39884],[49.03579,4.4284],[49.03342,4.46051],[49.02679,4.51062],[49.03019,4.54614],[49.03579,4.58045],[49.03697,4.62404],[49.04916,4.66873],[49.05679,4.6903],[49.06909,4.74164],[49.06976,4.76995],[49.07757,4.82406],[49.07507,4.83964],[49.072,4.89707],[49.07619,4.91602],[49.07949,4.94345],[49.08592,4.96904],[49.09234,4.98388],[49.09133,5.00991],[49.09806,5.02233],[49.09945,5.02853],[49.09465,5.05248],[49.09687,5.07716],[49.09178,5.0968],[49.09254,5.1305],[49.09094,5.14903],[49.08711,5.16575],[49.0918,5.22629],[49.09027,5.24265],[49.09565,5.27133],[49.09845,5.30386],[49.10605,5.33462],[49.1106,5.36398],[49.11725,5.37984],[49.11908,5.38897],[49.1179,5.39743],[49.11206,5.4067],[49.10731,5.42232],[49.10822,5.43001],[49.11485,5.44338],[49.11697,5.45648],[49.12016,5.46521],[49.12158,5.48661],[49.11916,5.50299],[49.11875,5.5434],[49.11157,5.56035],[49.11065,5.56885],[49.1123,5.57489],[49.1202,5.58765],[49.12503,5.60691],[49.12884,5.66088],[49.13859,5.69451],[49.15384,5.73123],[49.17417,5.76348],[49.18553,5.80617],[49.19034,5.84484],[49.19645,5.87257],[49.19965,5.93582],[49.20408,5.95117],[49.20331,5.96143],[49.19717,5.98478],[49.1876,5.99803],[49.18327,6.00893],[49.17909,6.04514],[49.18471,6.07969],[49.19065,6.08664],[49.20116,6.10525],[49.20444,6.11431],[49.20711,6.12995],[49.20507,6.13668],[49.19768,6.14592],[49.1959,6.15242],[49.19627,6.15961],[49.20115,6.177],[49.19841,6.20482],[49.19538,6.21351],[49.1879,6.22681],[49.18274,6.24292],[49.17625,6.24957],[49.15942,6.25309],[49.15334,6.25209],[49.14305,6.24665],[49.13575,6.24719],[49.1284,6.2559],[49.12631,6.27478],[49.12688,6.30182],[49.13382,6.33337],[49.13554,6.36813],[49.13982,6.38842],[49.13683,6.41063],[49.14187,6.43149],[49.13997,6.44921],[49.14079,6.47837],[49.13357,6.50368],[49.13173,6.52003],[49.13384,6.54259],[49.13193,6.55828],[49.13282,6.57442],[49.13728,6.59915],[49.13473,6.61707],[49.13783,6.63265],[49.13179,6.67451],[49.13581,6.69276],[49.13598,6.72119],[49.13404,6.73334],[49.13895,6.75778],[49.13395,6.77906],[49.13956,6.80895],[49.13919,6.81339],[49.1355,6.81971],[49.1267,6.82304],[49.12266,6.82668],[49.11524,6.84326],[49.10186,6.8658],[49.0853,6.87573],[49.07908,6.88409],[49.06997,6.91323],[49.06808,6.93053],[49.06065,6.94751],[49.05892,6.96488],[49.05116,6.97736],[49.04858,6.98445],[49.04654,6.99936],[49.04165,7.01475],[49.04297,7.04488],[49.03963,7.05544],[49.03439,7.06367],[49.02835,7.0677],[49.01576,7.07124],[49.00327,7.06955],[48.98973,7.06191],[48.97949,7.06285],[48.96949,7.0701],[48.95564,7.09069],[48.94146,7.10238],[48.9299,7.11708],[48.89996,7.12546],[48.89214,7.12456],[48.87968,7.11946],[48.8555,7.12128],[48.84553,7.12675],[48.83268,7.14367],[48.81795,7.15259],[48.81134,7.16118],[48.80727,7.17342],[48.79991,7.18829],[48.79218,7.21373],[48.78762,7.22117],[48.7792,7.22968],[48.77636,7.23587],[48.77571,7.26431],[48.77049,7.30593],[48.77294,7.32771],[48.77065,7.33496],[48.76426,7.34273],[48.76046,7.35171],[48.76105,7.36496],[48.77458,7.39255],[48.77779,7.4074],[48.77618,7.43091],[48.76952,7.47736],[48.77075,7.49669],[48.76745,7.58421],[48.77112,7.6262],[48.76905,7.64218],[48.76132,7.66418],[48.74955,7.68295],[48.74276,7.68714],[48.72732,7.69066],[48.71368,7.69912],[48.69452,7.72387],[48.68553,7.72959],[48.68169,7.73043],[48.67804,7.73398],[48.67454,7.73252],[48.6731,7.73434],[48.67963,7.80548],[48.68267,7.81244],[48.68978,7.82179],[48.71944,7.87935],[48.72778,7.88737],[48.73339,7.89753],[48.74372,7.90881],[48.74959,7.91943],[48.76826,7.93335],[48.77881,7.95265],[48.7863,7.96243],[48.80289,7.97507],[48.83301,8.01036],[48.83816,8.01975],[48.84633,8.0431],[48.85424,8.05407],[48.83704,8.08122],[48.83212,8.1169],[48.83015,8.12244],[48.8055,8.14043],[48.79147,8.16166],[48.79186,8.16036],[48.80982,8.1845],[48.83264,8.2205],[48.87706,8.25949],[48.8877,8.28579],[48.90379,8.34388],[48.90846,8.34903],[48.94021,8.36356],[48.9534,8.37725],[48.95733,8.38593],[48.96184,8.41186],[48.97189,8.43757],[48.97055,8.45865],[48.95841,8.47888],[48.94686,8.49236],[48.93881,8.51541],[48.93709,8.52394],[48.93751,8.53856],[48.93375,8.5513],[48.934,8.57022],[48.92547,8.591],[48.92423,8.60085],[48.91844,8.61291],[48.91282,8.61971],[48.90968,8.63038],[48.90772,8.66298],[48.90895,8.68869],[48.91957,8.73248],[48.92402,8.74397],[48.92418,8.75008],[48.92151,8.75608],[48.89753,8.78308],[48.88993,8.78663],[48.8739,8.78911],[48.86609,8.79185],[48.85357,8.79913],[48.84855,8.8047],[48.84096,8.82488],[48.83849,8.84704],[48.83017,8.8651],[48.82013,8.90295],[48.80138,8.93972],[48.78693,8.98418],[48.78248,9.00901],[48.77775,9.02007],[48.77295,9.0272],[48.76456,9.0318],[48.74423,9.0347],[48.73963,9.03669],[48.73552,9.04137],[48.72968,9.05341],[48.7258,9.06923],[48.71619,9.08539],[48.71307,9.09542],[48.70853,9.11811],[48.70945,9.14139],[48.7052,9.16955],[48.69349,9.20045],[48.69321,9.21929],[48.69761,9.25005],[48.69192,9.30632],[48.66472,9.36963],[48.65638,9.38358],[48.64895,9.41515],[48.63504,9.44229],[48.6314,9.45394],[48.62827,9.51493],[48.6334,9.5519],[48.63196,9.56385],[48.62691,9.57865],[48.61942,9.5884],[48.61505,9.60252],[48.61279,9.61956],[48.60607,9.63474],[48.60125,9.64129],[48.58758,9.65297],[48.58242,9.665],[48.57905,9.6651],[48.57487,9.6596],[48.57065,9.64931],[48.56641,9.64508],[48.5649,9.63429],[48.55895,9.63023],[48.5517,9.63359],[48.54479,9.64574],[48.53688,9.65106],[48.53399,9.65649],[48.53111,9.67905],[48.5311,9.69472],[48.52764,9.7176],[48.51854,9.75362],[48.51802,9.77798],[48.52022,9.79961],[48.51866,9.8125],[48.51131,9.83356],[48.50671,9.83873],[48.49241,9.84844],[48.48316,9.86616],[48.47523,9.88537],[48.46218,9.92662],[48.4565,9.95001],[48.45763,10.01312],[48.45311,10.05375],[48.4557,10.07261],[48.4641,10.09817],[48.46448,10.12091],[48.45644,10.19205],[48.45129,10.20191],[48.43314,10.21971],[48.42917,10.22996],[48.43032,10.28504],[48.43631,10.31046],[48.43669,10.32409],[48.43291,10.34375],[48.43032,10.37247],[48.41517,10.4047],[48.41295,10.41231],[48.4128,10.4673],[48.4078,10.51046],[48.39987,10.54378],[48.40038,10.55111],[48.40525,10.56481],[48.40916,10.58542],[48.40951,10.63383],[48.41249,10.65566],[48.41983,10.67944],[48.41667,10.72853],[48.42001,10.80055],[48.41365,10.89535],[48.40171,10.96913],[48.39966,11.01313],[48.39599,11.0316],[48.39576,11.05881],[48.39217,11.06863],[48.37044,11.1022],[48.36335,11.12262],[48.34903,11.13778],[48.34038,11.15495],[48.32872,11.16878],[48.31548,11.19558],[48.29989,11.2192],[48.28706,11.25608],[48.27511,11.28162],[48.25582,11.3037],[48.23649,11.34285],[48.20644,11.394],[48.16618,11.45428],[48.16465,11.45872],[48.16378,11.48663],[48.16541,11.49283],[48.16367,11.50691],[48.16975,11.51085],[48.16772,11.51655],[48.16974,11.53277],[48.15024,11.5574],[48.14402,11.58347],[48.14121,11.58289],[48.13905,11.58471],[48.13519,11.58213]],"distanceKmOsrm":831.8}]} \ No newline at end of file diff --git a/App/dashboard.html b/App/dashboard.html index b1cc41b..e2ffb06 100644 --- a/App/dashboard.html +++ b/App/dashboard.html @@ -4,8 +4,8 @@ GeoGraSim — Lehrkräfte-Dashboard - - + + + + +
+ +

📊 Erfolgsparameter & Wirkungsklammer

+

Übersicht aller Simulationen: Was wird gemessen, wie wird bewertet, welche Fragen werden vor und nach der Simulation gestellt.

+ + +

1. Erfolgsparameter pro Simulation

+

Diese Parameter werden während des Spiels erfasst und der Lehrperson + Schüler*innen angezeigt.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SimulationVerstehen (Kompetenz)Handeln (Entscheidungen)Geschicklichkeit
🌡️Klimawächter 2D/3D + Verstehen
+ • Temperaturanstieg vs. CO₂-Ausstoß (Zusammenhang erkannt?)
+ • Meeresspiegel-Prognose vs. tatsächlicher Anstieg
+ • Kipppunkte erkannt (wann wird es kritisch?) +
+ Handeln
+ • Maßnahmen-Mix (einseitig vs. vielfältig?)
+ • Budget-Effizienz (€ pro verhinderte °C)
+ • Timing (früh investiert vs. Panik am Ende)
+ • Balance Wirtschaft ↔ Umwelt +
+ Geschick
+ • Endtemperatur (< 17°C = Klimaziel)
+ • Bevölkerung überlebt (%)
+ • Budget nicht negativ +
🏞️Flussmanagement + Verstehen
+ • Zusammenhang Begradigung ↔ Hochwasser
+ • Renaturierung ↔ Biodiversität
+ • Deiche = kurzfristiger Schutz, langfristiges Risiko +
+ Handeln
+ • Maßnahmen-Balance (Schutz ↔ Ökologie ↔ Wirtschaft)
+ • Reaktion auf Hochwasser-Events
+ • Langfristplanung vs. Kurzfrist-Reaktion +
+ Geschick
+ • Hochwasserschäden minimiert
+ • Biodiversitäts-Index
+ • Bevölkerungswachstum +
🏗️Stadt & Raumplanung + Verstehen
+ • Nutzungskonflikte erkannt (Wohnen neben Industrie)
+ • Infrastruktur-Bedarf (Schule, Krankenhaus, Feuerwehr)
+ • Versorgungslogik (Supermarkt-Erreichbarkeit) +
+ Handeln
+ • Zoning-Qualität (sinnvolle Anordnung?)
+ • Verkehrsanbindung
+ • Grünflächen-Anteil
+ • Budget-Nutzung +
+ Geschick
+ • Zufriedenheits-Index
+ • Wirtschafts-Index
+ • Umwelt-Index
+ • Erreichbarkeit +
🌋Erdbeben + Verstehen
+ • Naturgefahr ≠ Naturkatastrophe
+ • Bauqualität entscheidet über Schäden
+ • Vorsorge vs. Reaktion +
+ Handeln
+ • Investition in Bauqualität vs. Quantität
+ • Frühwarnsystem gebaut?
+ • Notfallplan vorhanden? +
+ Geschick
+ • Überlebensrate bei Erdbeben
+ • Wirtschaftlicher Schaden
+ • Wiederaufbauzeit +
Energiemix + Verstehen
+ • Erneuerbare vs. fossile Energieträger
+ • Grundlast vs. Spitzenlast
+ • CO₂-Bilanz verschiedener Quellen +
+ Handeln
+ • Erneuerbaren-Anteil am Ende
+ • Blackout vermieden?
+ • CO₂-Reduktion erreicht? +
+ Geschick
+ • Stromversorgung stabil (%)
+ • CO₂-Emission gesunken
+ • Budget im Rahmen +
👕Lieferketten + Verstehen
+ • Wahre Kosten eines Produkts
+ • Zusammenhang Preis ↔ Arbeitsbedingungen ↔ CO₂
+ • Globale Verflechtungen +
+ Handeln
+ • Balance Preis ↔ Fairness ↔ CO₂
+ • Lieferanten-Diversifikation
+ • Transparenz-Entscheidungen +
+ Geschick
+ • Produkt fertig produziert
+ • Fairness-Score
+ • CO₂-Fußabdruck +
🌴Regenwald + Verstehen
+ • Stockwerke des Regenwalds
+ • Artenvielfalt und Abhängigkeiten
+ • Bedrohung durch Rodung +
+ Handeln
+ • Entdeckungen gemacht (Tiere, Pflanzen)
+ • Wissen über Zusammenhänge +
+ Geschick
+ • Arten entdeckt (%)
+ • Expedition abgeschlossen +
🚁Helikopter-Navigation + Orientierung
+ • Himmelsrichtungen korrekt?
+ • Entfernungen auf der Karte einschätzen
+ • Topografie: Täler, Pässe, Flüsse als Orientierung
+ • Ortskenntnisse (Städte, Berge) +
+ Handeln
+ • Routenplanung-Genauigkeit (%)
+ • H/V-Diagramm verstanden (Startverhalten)
+ • Spritmanagement +
+ Geschick
+ • Start-Sterne (1–5)
+ • Landung-Sterne (1–5)
+ • G-Kraft-Durchschnitt
+ • Berührungen +
+ + +

2. Wirkungsklammer — Pre/Post-Fragen

+

5 Fragen vor und 5 Fragen nach der Simulation. Gleiche Kompetenzen, leicht veränderte Formulierung.

+ + +

🌡️ Klimawächter

+
+
+

VOR der Simulation

+
+
1. Was passiert, wenn der CO₂-Gehalt in der Atmosphäre steigt?
+
+
a) Es wird kälter
+
b) Die Durchschnittstemperatur steigt
+
c) Es regnet weniger
+
d) Es passiert nichts Besonderes
+
+
+
+
2. Warum steigt der Meeresspiegel?
+
+
a) Weil mehr Regen fällt
+
b) Weil Gletscher schmelzen und Wasser sich bei Wärme ausdehnt
+
c) Weil Flüsse schneller fließen
+
d) Weil der Mond die Gezeiten verändert
+
+
+
+
3. Was ist ein Treibhausgas?
+
+
a) Ein Gas, das nur in Gewächshäusern vorkommt
+
b) Ein Gas, das die Luft reinigt
+
c) Ein Gas, das Wärme in der Atmosphäre zurückhält
+
d) Ein Gas, das nur bei Hitze entsteht
+
+
+
+
4. Welche Maßnahme hilft gegen den Klimawandel?
+
+
a) Mehr Straßen bauen
+
b) Mehr Kohle verbrennen
+
c) Erneuerbare Energie nutzen
+
d) Mehr Autos produzieren
+
+
+
+
5. Was bedeutet „Klimaziel 1,5°C"?
+
+
a) Die Temperatur soll auf 1,5°C sinken
+
b) Die Erderwärmung soll maximal 1,5°C über dem vorindustriellen Niveau bleiben
+
c) Es darf nur 1,5°C warm sein
+
d) Die Temperatur soll jedes Jahr um 1,5°C steigen
+
+
+
+
+

NACH der Simulation

+
+
1. In der Simulation: Was ist passiert, als du den CO₂-Ausstoß nicht gesenkt hast?
+
+
a) Nichts hat sich verändert
+
b) Die Temperatur ist gestiegen und der Meeresspiegel auch
+
c) Es wurde kälter
+
d) Die Bevölkerung ist gewachsen
+
+
+
+
2. Was war die wirksamste Maßnahme gegen den Meeresspiegelanstieg in der Simulation?
+
+
a) Deiche bauen
+
b) CO₂-Ausstoß senken (Ursache bekämpfen)
+
c) Mehr Häuser bauen
+
d) Nichts tun und abwarten
+
+
+
+
3. Welcher Zusammenhang wurde in der Simulation sichtbar?
+
+
a) Mehr CO₂ → höhere Temperatur → steigender Meeresspiegel
+
b) Mehr CO₂ → niedrigere Temperatur
+
c) Weniger CO₂ → mehr Regen
+
d) Temperatur und Meeresspiegel haben nichts miteinander zu tun
+
+
+
+
4. Warum war es in der Simulation schwierig, die Temperatur unter 17°C zu halten?
+
+
a) Die Maßnahmen waren zu billig
+
b) Die Simulation war fehlerhaft
+
c) CO₂ bleibt lange in der Atmosphäre — Maßnahmen wirken verzögert
+
d) Man konnte nichts dagegen tun
+
+
+
+
5. Was hast du über die Balance zwischen Wirtschaft und Klimaschutz gelernt?
+
+
a) Man muss sich immer für eines entscheiden
+
b) Wirtschaft ist wichtiger als Klimaschutz
+
c) Man muss beides gleichzeitig berücksichtigen — es gibt Zielkonflikte
+
d) Klimaschutz schadet immer der Wirtschaft
+
+
+
+
+ + +

🏞️ Flussmanagement

+
+
+

VOR der Simulation

+
1. Was passiert, wenn man einen Fluss begradigt?
a) Das Hochwasserrisiko sinkt
b) Das Wasser fließt schneller und das Hochwasserrisiko flussabwärts steigt
c) Der Fluss trocknet aus
d) Fische vermehren sich
+
2. Was bedeutet „Renaturierung"?
a) Einen Fluss mit Beton auskleiden
b) Einen Fluss in seinen natürlichen Zustand zurückversetzen
c) Einen Fluss umleiten
d) Einen Damm bauen
+
3. Warum sind Auen wichtig?
a) Sie speichern Hochwasser und sind Lebensraum für viele Arten
b) Sie sind gutes Bauland
c) Sie verhindern Regen
d) Sie sind nur Sumpf ohne Nutzen
+
4. Was schützt am besten vor Hochwasser?
a) Nur Deiche bauen
b) Flüsse begradigen
c) Kombination aus Deichen, Rückhaltebecken und natürlichen Überschwemmungsflächen
d) Gar nichts tun
+
5. Was ist der Unterschied zwischen Hochwasserschutz und Hochwasservorsorge?
a) Schutz = technische Bauten, Vorsorge = Planung und Raumordnung
b) Es gibt keinen Unterschied
c) Vorsorge ist teurer
d) Schutz funktioniert nicht
+
+
+

NACH der Simulation

+
1. Was ist in der Simulation passiert, als du den Fluss begradigt hast?
a) Alles wurde besser
b) Das Hochwasserrisiko ist langfristig gestiegen
c) Die Fische wurden mehr
d) Nichts hat sich verändert
+
2. Welche Maßnahme hat in der Simulation die Biodiversität am meisten verbessert?
a) Deiche bauen
b) Renaturierung und Aufforstung
c) Felder anlegen
d) Den Fluss begradigen
+
3. Warum war es schwierig, gleichzeitig Hochwasserschutz UND Biodiversität zu haben?
a) Weil technische Schutzbauten oft die natürlichen Lebensräume zerstören
b) Weil Biodiversität Hochwasser verursacht
c) Weil beides zu teuer ist
d) Es war gar nicht schwierig
+
4. Was hast du über die Rolle von Siedlungen am Fluss gelernt?
a) Siedlungen am Fluss sind immer sicher
b) Siedlungen in Überschwemmungsgebieten erhöhen das Schadensrisiko
c) Flüsse fließen um Siedlungen herum
d) Siedlungen haben keinen Einfluss
+
5. Was war die wichtigste Erkenntnis der Simulation?
a) Man soll nur Deiche bauen
b) Natur ist nicht wichtig
c) Hochwasserschutz braucht eine Kombination aus technischen und natürlichen Maßnahmen
d) Der Mensch kann Hochwasser komplett verhindern
+
+
+ + +

🚁 Helikopter-Navigation

+
+
+

VOR der Simulation

+
1. Wo liegt Innsbruck?
a) An der Donau
b) Im Inntal, umgeben von Bergen
c) Am Bodensee
d) In Kärnten
+
2. In welche Himmelsrichtung fliegt man von Bregenz nach Innsbruck?
a) Nach Westen
b) Nach Norden
c) Nach Osten
d) Nach Süden
+
3. Was ist ein Tal in den Alpen?
a) Eine Vertiefung zwischen zwei Bergen, oft mit einem Fluss
b) Ein Berggipfel
c) Ein See
d) Eine Ebene ohne Berge
+
4. Wie schätzt man Entfernungen auf einer Karte?
a) Man zählt die Häuser
b) Man verwendet den Maßstab der Karte
c) Man fragt jemanden
d) Das geht nicht
+
5. Warum startet ein Hubschrauber schräg und nicht senkrecht?
a) Weil der Motor zu schwach ist
b) Weil es so schneller geht
c) Weil Vorwärtsgeschwindigkeit im Notfall eine sichere Landung ermöglicht
d) Weil der Pilot es so gelernt hat
+
+
+

NACH der Simulation

+
1. Durch welches Tal bist du geflogen, um von Nenzing nach Damüls zu kommen?
a) Inntal
b) Laternsertal / Großes Walsertal
c) Zillertal
d) Montafon
+
2. Warum konnte man nicht direkt über die Berge zum Einsatzort fliegen?
a) Weil der Hubschrauber nicht hoch genug fliegen kann
b) Weil Wetter, Wind und Sicherheit einen Umweg über Talrouten erfordern
c) Weil es verboten ist
d) Weil Berge nicht existieren
+
3. Was hast du über die Orientierung mit Hilfe von Flüssen und Tälern gelernt?
a) Flüsse und Täler sind wichtige Orientierungslinien in den Alpen
b) Flüsse sind Hindernisse, die man vermeiden soll
c) Täler sind gefährlich zum Fliegen
d) Orientierung braucht man nicht
+
4. Warum braucht ein Hubschrauber in großer Höhe mehr Treibstoff?
a) Weil die Luft dünner ist und der Rotor mehr Leistung braucht
b) Weil es kälter ist
c) Weil der Wind stärker ist
d) Das stimmt nicht, der Verbrauch ist gleich
+
5. Was war die wichtigste geografische Erkenntnis aus der Simulation?
a) Österreich ist flach
b) Hubschrauber können überall hinfliegen
c) Die Topografie (Berge, Täler, Gewässer) bestimmt die möglichen Flugrouten
d) Wetter spielt keine Rolle
+
+
+ +
+ + diff --git a/App/sims/heli/game.html b/App/sims/heli/game.html new file mode 100644 index 0000000..583dfe5 --- /dev/null +++ b/App/sims/heli/game.html @@ -0,0 +1,1497 @@ + + + + + +GeoGraSim — 🚁 Helikopter-Navigation + + + + + + + + + +
+
+

🚁 Einsatzzentrale

+

Wähle deinen Einsatz — der Hubschrauber wartet.

+
+
+
+ + +
+
+

📍 Routenplanung

+ Wegpunkt 1/5 +
+
+
+
+
Mission:
+
Hubschrauber:
+
Wegpunkte:
+
Region:
+
+
+ + +
+ +
+ + +
+
+

🗺️ Navigation

+ +
+ + + +
+
+
+
+
+
+ + +
+ +
+ + +
+
+
+ + + + + + diff --git a/App/sims/heli/landing.html b/App/sims/heli/landing.html new file mode 100644 index 0000000..eedd9aa --- /dev/null +++ b/App/sims/heli/landing.html @@ -0,0 +1,4068 @@ + + + + +Helikopter-Simulator · Bergrettung + + + + +
+
+
+

🚁 Helikopter-Simulator · Bergrettung

+
Co-Pilot im Gebirgseinsatz · Flugretter abseilen, Patient bergen, sicher zurück ins Tal
+
+ Lektion · Bergrettung +
+ +
+ + + + +
+
+
Höhe
0 m
+
Sinken
0 m/s
+
Speed
0 km/h
+
Treibst.
100 %
+
Verbrauch
— l/min
+
G-Kraft
1.0 g
+
Berührungen: 0 / 3
+ +
+
+
+
+
Wind — kn
+
Ziel
+
+
🔊
+
+
+
+ +
Ziel: Wähle Schwierigkeitsstufe & Helikopter.
+ + + + +
← Westen
+
Osten →
+ +
+
+ Bergrettung · Briefing +

Notruf aus dem Gebirge

+

Eine verletzte Person liegt an einem Felsband. Du bist Co-Pilot und fliegst die heikle Bergung. Drei Phasen warten auf dich:

+
    +
  1. Anflug & Abseilen: Schwebeflug über dem Patienten halten, der Flugretter wird abgeseilt.
  2. +
  3. Warteschleife: Während der Retter versorgt, kreist du in sicherem Abstand. Nicht stehenbleiben — frisst Treibstoff!
  4. +
  5. Aufnahme & Ausflug: Erneut Position halten, beide hochziehen, sicher zur Klinik fliegen.
  6. +
+ +
+
Schwierigkeitsstufe
+
+
+ +
+
Helikopter
+
+
+ +
+
+ ⚙️ Erweiterte Parameter (Admin) + +
+
+

Diese Werte überschreiben die Standard-Einstellungen des gewählten Levels. Bei Level-Wechsel werden sie zurückgesetzt.

+
+
+
+ +
+
+ 📚 Wie funktioniert eine Bergrettung mit der Winde? + +
+
+

Bei einem Bergunfall kann der Hubschrauber meist nicht landen — der Hang ist zu steil, Geröll oder Schnee zu instabil. Stattdessen arbeitet die Crew mit einer Rettungswinde und einem 20 bis 90 Meter langen Tau.

+
    +
  1. Stationärer Schwebeflug: Der Pilot bringt den Heli neben (nicht direkt über!) den Patienten. Direkt darüber wäre der Rotorabwind zu gefährlich für den Verletzten.
  2. +
  3. Flugretter abseilen: Der ausgebildete Bergretter wird zum Patienten heruntergelassen, versorgt ihn und bereitet ihn für den Transport vor (Bergesack, Trage oder Rettungsschlinge).
  4. +
  5. Warteschleife: In der Schwebe verbraucht der Heli sehr viel Treibstoff und der Pilot muss höchst konzentriert arbeiten. Deshalb fliegt er meist eine Warteschleife — oft talabwärts — bis der Retter „bereit" funkt.
  6. +
  7. Zweiter Anflug: Erneuter Schwebeflug, Winde herunterlassen, Retter und Patient hochziehen.
  8. +
  9. Ausflug: Talwärts beschleunigen — das gibt sofort Höhenreserve und Sicherheit.
  10. +
+

Was es im Gebirge so schwierig macht: Der Wind ist nicht konstant! An der Luvseite (windzugewandt) gibt es Aufwinde, an der Leeseite dagegen tückische Abwinde und Walzen. In engen Tälern wird der Wind beschleunigt (Düseneffekt). Dazu kommen Wolken, die plötzlich die Sicht nehmen, und versteckte Hindernisse wie Seilbahnkabel.

+
+
+ +
+ +
+
+
+
+ + +
+ + + + diff --git a/App/sims/heli/scripts/generate-audio-batch.sh b/App/sims/heli/scripts/generate-audio-batch.sh new file mode 100644 index 0000000..c8c49db --- /dev/null +++ b/App/sims/heli/scripts/generate-audio-batch.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Generisches Audio-Batch-Script — nimmt JSON-Textbuch mit Struktur +# { "lines": [ {"id": "...", "text": "..."} ] } +# und generiert MP3s via ElevenLabs in sounds/radio/.mp3 +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/heli/scripts/generate-audio-batch.sh tower-geo-lines.json +# App/sims/heli/scripts/generate-audio-batch.sh tower-start-lines.json +# FORCE=1 … überschreibt bestehende Files +set -e +: "${ELEVENLABS_API_KEY:?ELEVENLABS_API_KEY muss gesetzt sein}" + +VOICE_ID="${ELEVENLABS_VOICE_ID:-EXAVITQu4vr4xnSDxMaL}" +MODEL_ID="${ELEVENLABS_MODEL_ID:-eleven_multilingual_v2}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUT_DIR="$SCRIPT_DIR/../sounds/radio" +mkdir -p "$OUT_DIR" + +JSON_INPUT="$1" +if [ -z "$JSON_INPUT" ]; then + echo "Usage: $0 " + exit 2 +fi +JSON_FILE="$SCRIPT_DIR/$JSON_INPUT" +if [ ! -f "$JSON_FILE" ]; then + echo "✗ Datei nicht gefunden: $JSON_FILE"; exit 2 +fi + +generate() { + local id="$1" + local text="$2" + local out="$OUT_DIR/${id}.mp3" + if [ -f "$out" ] && [ -z "$FORCE" ]; then + echo "✓ ${id}.mp3 schon da"; return 0 + fi + echo "→ ${id}" + local payload + payload=$(TEXT="$text" MODEL="$MODEL_ID" python -c ' +import json, os +print(json.dumps({ + "text": os.environ["TEXT"], + "model_id": os.environ["MODEL"], + "voice_settings": {"stability": 0.58, "similarity_boost": 0.78, "style": 0.30, "use_speaker_boost": True} +}))') + curl -sS -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VOICE_ID" \ + -H "xi-api-key: $ELEVENLABS_API_KEY" \ + -H "accept: audio/mpeg" \ + -H "content-type: application/json" \ + -d "$payload" \ + --output "$out" + local size; size=$(wc -c <"$out") + if [ "$size" -lt 1500 ]; then + echo "✗ ${id}: Fehler — Datei zu klein ($size B). Inhalt:" + cat "$out"; rm -f "$out"; return 1 + fi + echo "✓ ${id}.mp3 ($size B)" + sleep 0.4 +} + +ok=0; fail=0 +python -c " +import json, sys +d = json.load(sys.stdin) +for line in d['lines']: print(line['id'] + '\t' + line['text']) +" < "$JSON_FILE" | while IFS=$'\t' read -r id text; do + [ -z "$id" ] && continue + if generate "$id" "$text"; then ok=$((ok+1)); else fail=$((fail+1)); echo "STOP bei Fehler"; break; fi +done + +echo +echo "Ablage: $OUT_DIR" +ls -1 "$OUT_DIR"/*.mp3 2>/dev/null | wc -l | xargs printf "Dateien insgesamt: %s\n" diff --git a/App/sims/heli/scripts/generate-audio-openai.sh b/App/sims/heli/scripts/generate-audio-openai.sh new file mode 100644 index 0000000..37be9d6 --- /dev/null +++ b/App/sims/heli/scripts/generate-audio-openai.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Generisches Audio-Batch-Script via OpenAI TTS. +# Nimmt JSON-Textbuch mit Struktur: +# { "lines": [ {"id": "...", "text": "..."} ] } +# und generiert MP3s in sounds/radio/.mp3 +# +# Voraussetzung: OPENAI_API_KEY (aus App/.env.local) +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/heli/scripts/generate-audio-openai.sh tower-geo-lines.json +# App/sims/heli/scripts/generate-audio-openai.sh tower-start-lines.json +# FORCE=1 … überschreibt bestehende +# OPENAI_VOICE=shimmer … andere Stimme +set -e +: "${OPENAI_API_KEY:?OPENAI_API_KEY muss gesetzt sein}" + +VOICE="${OPENAI_VOICE:-nova}" +MODEL="${OPENAI_MODEL:-gpt-4o-mini-tts}" +# Stil-Instruktion nur bei gpt-4o-mini-tts wirksam. Warmherzige Fluglotsin, +# ruhig, klare deutsche Aussprache, als ob sie Schuelerinnen begleitet. +INSTRUCTIONS="${OPENAI_TTS_INSTRUCTIONS:-Speak warmly and patiently like an encouraging female flight controller guiding a young student co-pilot. Unhurried, clear, friendly German pronunciation. Gentle reassuring tone, never robotic.}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUT_DIR="$SCRIPT_DIR/../sounds/radio" +mkdir -p "$OUT_DIR" + +JSON_INPUT="$1" +if [ -z "$JSON_INPUT" ]; then + echo "Usage: $0 " + exit 2 +fi +JSON_FILE="$SCRIPT_DIR/$JSON_INPUT" +if [ ! -f "$JSON_FILE" ]; then + echo "Datei nicht gefunden: $JSON_FILE"; exit 2 +fi + +echo "Modell: $MODEL · Stimme: $VOICE" +echo "Quelle: $JSON_INPUT" +echo + +generate() { + local id="$1" + local text="$2" + local out="$OUT_DIR/${id}.mp3" + if [ -f "$out" ] && [ -z "$FORCE" ]; then + echo "ok ${id}.mp3 schon da" + return 0 + fi + echo "gen ${id}" + local payload + payload=$(TEXT="$text" MODEL="$MODEL" VOICE="$VOICE" INSTR="$INSTRUCTIONS" python -c ' +import json, os +body = { + "model": os.environ["MODEL"], + "voice": os.environ["VOICE"], + "input": os.environ["TEXT"], + "response_format": "mp3" +} +# instructions nur bei gpt-4o-mini-tts unterstuetzt +if "mini-tts" in os.environ["MODEL"]: + body["instructions"] = os.environ["INSTR"] +print(json.dumps(body, ensure_ascii=False))') + local http + http=$(curl -sS -w "%{http_code}" -o "$out" \ + -X POST https://api.openai.com/v1/audio/speech \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json; charset=utf-8" \ + -d "$payload") + if [ "$http" != "200" ]; then + echo "FEHLER ${id}: HTTP $http" + cat "$out"; echo; rm -f "$out" + return 1 + fi + local size; size=$(wc -c <"$out") + if [ "$size" -lt 1500 ]; then + echo "FEHLER ${id}: Datei zu klein ($size B)" + cat "$out"; rm -f "$out" + return 1 + fi + echo "ok ${id}.mp3 (${size} B)" + sleep 0.2 +} + +ok=0; fail=0 +while IFS=$'\t' read -r id text; do + [ -z "$id" ] && continue + if generate "$id" "$text"; then ok=$((ok+1)); else fail=$((fail+1)); fi +done < <(python -c " +import json, sys +d = json.load(sys.stdin) +for line in d['lines']: print(line['id'] + '\t' + line['text']) +" < "$JSON_FILE") + +echo +echo "Zusammenfassung: ok=$ok, fail=$fail" +ls -1 "$OUT_DIR"/*.mp3 2>/dev/null | wc -l | xargs printf "Dateien im Ordner: %s\n" diff --git a/App/sims/heli/scripts/generate-mission-images.sh b/App/sims/heli/scripts/generate-mission-images.sh new file mode 100644 index 0000000..6147083 --- /dev/null +++ b/App/sims/heli/scripts/generate-mission-images.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Generiert die 9 Mission-Card-Bilder via DALL-E 3. +# Stil: Flat Scandinavian Alpine, 1792x1024 landscape (später auf 1024x576 verkleinern). +# +# Voraussetzung: +# - OPENAI_API_KEY als Env-Variable (aus App/.env.local) +# - curl + python (für JSON-Handling; jq nicht verfügbar auf Git Bash / Windows) +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/heli/scripts/generate-mission-images.sh # alle +# App/sims/heli/scripts/generate-mission-images.sh m1 m5 m7 # Auswahl +# FORCE=1 … # überschreibt bestehende +set -e +: "${OPENAI_API_KEY:?Env-Variable OPENAI_API_KEY muss gesetzt sein}" + +cd "$(dirname "$0")/../assets/missions" + +PROMPT_BASE="Flat Scandinavian alpine illustration, wide panoramic landscape filling the entire 16:9 frame edge-to-edge, NOT a circular composition, painted style with clean vector shapes, dark forest green (#1f4b37) and sage green (#4a7c4e) mountains, yellow-mustard (#e8c547) sun accents, beige (#e8e4d8) valley, white snow highlights. Medium-sized rescue helicopter with orange and red markings clearly visible in the sky, recognizable shape with rotor. Recognizable geographic landmarks in the scene. No text, no watermark, no logos, no borders, no frame." + +declare -A PROMPTS=( + [m1]="$PROMPT_BASE Scene: ski area at Damuels in Bregenzerwald, Vorarlberg — Damuels-Mittagsspitze mountain ridge in background, Uga ski lift towers, snowy slopes with ski tracks, small Alpine village in foreground valley." + [m2]="$PROMPT_BASE Scene: A14 Rheintal motorway near Rankweil, Vorarlberg — wide valley floor with straight highway cutting through, small cars, Rhaetikon mountains in background, Feldkirch visible in distance." + [m3]="$PROMPT_BASE Scene: Brandnertal with the Zimba peak (pyramid-shaped mountain) in Raetikon range, Vorarlberg — steep rocky cliff, alpine meadow at base, small climber figure, narrow valley road." + [m4]="$PROMPT_BASE Scene: Rettenbachferner glacier ski area above Soelden in Otztal, Tyrol — high-altitude snow, ski lift towers, cable car station, vast glacier with mountain ridges." + [m5]="$PROMPT_BASE Scene: Lake Constance (Bodensee) at Bregenz harbour — calm turquoise water, Pfander mountain silhouette on the right, Seebühne floating stage visible on the water, shoreline with town roofs." + [m6]="$PROMPT_BASE Scene: Rappenloch canyon or Kanisfluh gorge near Mellau in Bregenzerwald, Vorarlberg — deep V-shaped valley with rushing river, green moss-covered cliffs on both sides, small waterfall." + [m7]="$PROMPT_BASE Scene: Wilder Kaiser mountain range with Kufstein fortress on a rocky hill in the foreground valley, Tyrol — recognizable jagged Kaiser peaks in background, Inn river, hiking trails with small hiker figures." + [m8]="$PROMPT_BASE Scene: Gaschurn village in hinteres Montafon valley, Vorarlberg — narrow valley with steep mountains on both sides (Silvretta range), village rooftops with church tower, alpine meadow, landing pad." + [m9]="$PROMPT_BASE Scene: Zillertal near Mayrhofen, Tyrol — Penken mountain in background, fresh avalanche debris on steep slope, snow-covered forest, alpine village in valley." + [m10]="$PROMPT_BASE Scene: A2 Suedautobahn motorway bridge near Baden south of Vienna — wide curved highway cutting through low wooded hills (Wienerwald), kurpark spa town rooftops in the middle distance, small cars on the road." + [m11]="$PROMPT_BASE Scene: Donauinsel and Neue Donau in Vienna — long narrow island park between two river arms, reed and willow shoreline, city skyline with St. Stephens cathedral tower in background, calm blue water." + [m12]="$PROMPT_BASE Scene: Schneeberg mountain north face in Lower Austria — pale limestone cliffs with snow patches, the Schneebergbahn cog railway visible on a ridge, forested foothills below, easternmost 2000er alpine summit." + [m13]="$PROMPT_BASE Scene: Rax high plateau in Lower Austria — broad grassy karst plateau with limestone outcrops, hiking trail winding through, pine mountain scrub, deep valley Hoellental dropping off to one side." + [m14]="$PROMPT_BASE Scene: Neusiedler See at Podersdorf in Burgenland — shallow steppe lake with tall reed belt, a windsurfer figure bent over in strong wind, horizon almost flat, pannonian plain stretching east." + [m15]="$PROMPT_BASE Scene: A1 Westautobahn motorway intersection at Sattledt in Upper Austria — flat agricultural landscape, two highways crossing on overpasses, small cars, distant Alps on the southern horizon." + [m16]="$PROMPT_BASE Scene: Traunsee lake at Gmunden in Salzkammergut — deep blue alpine lake, steep pyramid-shaped Traunstein peak rising directly from the shore, small sailing boat on the water, Gmunden town with castle Schloss Ort in foreground." + [m17]="$PROMPT_BASE Scene: A10 Tauern motorway approaching Tauerntunnel north portal near Flachau-Zederhaus — highway curving into mountainside tunnel entrance, steep wooded Tauern mountains, snow on highest peaks, alpine valley." + [m18]="$PROMPT_BASE Scene: Wolfgangsee at St. Wolfgang im Salzkammergut — alpine lake with the iconic St. Wolfgang pilgrimage church by the shore, Schafberg mountain in background with cog railway, traditional village houses." + [m19]="$PROMPT_BASE Scene: Grossglockner summit pyramid from the north (Kaiser-Franz-Josefs-Hoehe viewpoint) — sharp snow-ice pyramid peak (highest mountain of Austria 3798m), Pasterze glacier tongue below, moraine ridges, thin cloud wisps." + [m20]="$PROMPT_BASE Scene: Dachstein massif southern face in Styria — massive limestone cliff wall dropping 800 meters vertically, glacier plateau on top, mountaineer figures roped up, hanging valley in foreground with alpine meadow." + [m21]="$PROMPT_BASE Scene: Hochschwab karst plateau in Styria — undulating limestone plateau with dwarf pine (Latschen), rocky outcrops, wild chamois grazing, distant view into Mariazell valley, source of Vienna spring water aqueduct." + [m22]="$PROMPT_BASE Scene: A9 Pyhrn motorway near Bosrucktunnel south portal — highway emerging from tunnel into Ennstal valley, steep forested Bosruck mountain, traditional Styrian farmhouses with wooden shingle roofs." + [m23]="$PROMPT_BASE Scene: Woerthersee at Poertschach in Carinthia — turquoise warm bathing lake with sailing boats and swimmers near the promenade, Karawanken mountain range in background, Mediterranean-style villas along the shore." + [m24]="$PROMPT_BASE Scene: Grossglockner southern face from Heiligenblut side in Carinthia — view up to the glockner pyramid across the Pasterze glacier, historical Heiligenblut pilgrimage church with tall needle spire in foreground, Grossglockner panoramic road visible." + [m25]="$PROMPT_BASE Scene: A2 Suedautobahn motorway near Klagenfurt with the Karawanken mountain range in background — wide valley with highway curving south toward the Slovenian border Karawanken tunnel, Woerthersee visible to the west." +) + +TARGETS=("$@") +if [ ${#TARGETS[@]} -eq 0 ]; then + TARGETS=(m1 m2 m3 m4 m5 m6 m7 m8 m9 m10 m11 m12 m13 m14 m15 m16 m17 m18 m19 m20 m21 m22 m23 m24 m25) +fi + +for id in "${TARGETS[@]}"; do + prompt="${PROMPTS[$id]}" + if [ -z "$prompt" ]; then + echo "✗ $id: kein Prompt definiert, überspringe" + continue + fi + if [ -f "${id}.png" ] && [ -z "$FORCE" ]; then + echo "✓ ${id}.png schon vorhanden (FORCE=1 zum Überschreiben)" + continue + fi + echo "→ ${id}: generiere …" + payload=$(PROMPT="$prompt" python -c 'import json,os; print(json.dumps({"model":"dall-e-3","prompt":os.environ["PROMPT"],"n":1,"size":"1792x1024","quality":"standard"}))') + resp=$(curl -s https://api.openai.com/v1/images/generations \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload") + url=$(echo "$resp" | python -c 'import sys,json +d=json.load(sys.stdin) +print(d.get("data",[{}])[0].get("url",""))' 2>/dev/null) + if [ -z "$url" ]; then + err=$(echo "$resp" | python -c 'import sys,json +try: print(json.load(sys.stdin).get("error",{}).get("message","?")) +except: print("unparseable response")' 2>/dev/null) + echo "✗ ${id}: Fehler — $err" + continue + fi + curl -s "$url" -o "${id}.png" + echo "✓ ${id}.png gespeichert ($(du -h "${id}.png" | cut -f1))" + sleep 2 +done + +echo +echo "Fertig. Optional auf 1024x576 verkleinern:" +echo " for f in *.png; do magick \"\$f\" -resize 1024x576 -strip \"\$f\"; done" diff --git a/App/sims/heli/scripts/generate-tower-lines.sh b/App/sims/heli/scripts/generate-tower-lines.sh new file mode 100644 index 0000000..1444617 --- /dev/null +++ b/App/sims/heli/scripts/generate-tower-lines.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Generiert Tower-Funkansagen pro Hubschrauber via ElevenLabs. +# Liest tower-lines.json, speichert MP3s in App/sims/heli/sounds/radio/ +# +# Voraussetzung: ELEVENLABS_API_KEY + curl + python +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/heli/scripts/generate-tower-lines.sh +# FORCE=1 … überschreibt bestehende +set -e +: "${ELEVENLABS_API_KEY:?Env-Variable ELEVENLABS_API_KEY muss gesetzt sein}" + +# Default-Voice: 'Sarah' — klar, weiblich, multilingual-fähig. +# Eigene Stimme: ELEVENLABS_VOICE_ID=xxx vor dem Aufruf. +VOICE_ID="${ELEVENLABS_VOICE_ID:-EXAVITQu4vr4xnSDxMaL}" +MODEL_ID="${ELEVENLABS_MODEL_ID:-eleven_multilingual_v2}" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUT_DIR="$SCRIPT_DIR/../sounds/radio" +JSON_FILE="$SCRIPT_DIR/tower-lines.json" +mkdir -p "$OUT_DIR" + +generate() { + local id="$1" + local text="$2" + local out="$OUT_DIR/${id}.mp3" + if [ -f "$out" ] && [ -z "$FORCE" ]; then + echo "✓ ${id}.mp3 schon da"; return + fi + echo "→ ${id}: generiere …" + local payload + payload=$(TEXT="$text" MODEL="$MODEL_ID" python -c ' +import json, os +print(json.dumps({ + "text": os.environ["TEXT"], + "model_id": os.environ["MODEL"], + "voice_settings": {"stability": 0.55, "similarity_boost": 0.78, "style": 0.25, "use_speaker_boost": True} +}))') + curl -sS -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VOICE_ID" \ + -H "xi-api-key: $ELEVENLABS_API_KEY" \ + -H "accept: audio/mpeg" \ + -H "content-type: application/json" \ + -d "$payload" \ + --output "$out" + local size; size=$(wc -c <"$out") + if [ "$size" -lt 1500 ]; then + echo "✗ ${id}: Fehler — Datei zu klein ($size B), Inhalt:" + cat "$out"; rm -f "$out"; return 1 + fi + echo "✓ ${id}.mp3 ($size B)" + sleep 0.4 +} + +# Liste laden und pro Zeile generieren (via stdin — umgeht Windows-Pfad-Probleme) +python -c " +import json, sys +d = json.load(sys.stdin) +for line in d['lines']: print(line['id'] + '\t' + line['text']) +" < "$JSON_FILE" | while IFS=$'\t' read -r id text; do + [ -z "$id" ] && continue + generate "$id" "$text" +done + +echo +echo "Fertig. Ablage: $OUT_DIR" +ls -1 "$OUT_DIR"/r_tower_*.mp3 2>/dev/null | wc -l | xargs printf "Tower-Dateien vorhanden: %s\n" diff --git a/App/sims/heli/scripts/nav-flight-lines.json b/App/sims/heli/scripts/nav-flight-lines.json new file mode 100644 index 0000000..6cfa53b --- /dev/null +++ b/App/sims/heli/scripts/nav-flight-lines.json @@ -0,0 +1,32 @@ +{ + "_note": "Audio-Kommentare fuer Routenplanung, Kartenflug und Landung. Gesprochen von Tower-Lehrmeisterin (Nova, Speed 1.12) bzw. Pilot/Retter fuer Landing-Dialog. Funk-Filter wird live ueber den Bandpass gelegt.", + "lines": [ + { "id": "r_nav_plan_start", "text": "Routenplanung beginnt. Der Pilot wartet auf deine Eingabe — klicke die Wegpunkte der Reihe nach ab." }, + { "id": "r_nav_plan_correct", "text": "Wegpunkt erreicht. Weiter zum naechsten." }, + { "id": "r_nav_plan_wrong_near", "text": "Fast — du bist in der richtigen Gegend, aber nicht exakt am Wegpunkt. Schau nochmal auf die Karte." }, + { "id": "r_nav_plan_wrong_far", "text": "Das ist nicht der richtige Wegpunkt. Orientier dich an der Himmelsrichtung und der Distanz." }, + { "id": "r_nav_plan_done", "text": "Route vollstaendig. Briefing beendet, Freigabe zum Start." }, + + { "id": "r_nav_route_check", "text": "Route wird geprueft." }, + { "id": "r_nav_departure_ok", "text": "Abflug freigegeben. Kurs stimmt, Treibstoff voll." }, + { "id": "r_nav_departure_takeover","text": "Chefpilot hat uebernommen. Wir bringen dich sicher zum Einsatzort." }, + + { "id": "r_nav_seg_start", "text": "Neuer Streckenabschnitt. Kurs gesetzt, Geschwindigkeit konstant." }, + { "id": "r_nav_seg_arrive", "text": "Wegpunkt passiert — ich uebergebe ans Navigationsmodul." }, + { "id": "r_nav_fuel_half", "text": "Treibstoff halbvoll. Alles im gruenen Bereich." }, + { "id": "r_nav_fuel_quarter", "text": "Treibstoff auf einem Viertel. Direktkurs halten." }, + { "id": "r_nav_weather_enter", "text": "Wir durchfliegen eine Schlechtwetterzone — leichte Turbulenzen sind moeglich." }, + { "id": "r_nav_weather_exit", "text": "Schlechtwetter hinter uns, klare Sicht voraus." }, + { "id": "r_nav_altitude_mountain","text": "Ueberflughoehe Bergkette — der Rotor arbeitet in duenner Luft, mehr Leistung noetig." }, + { "id": "r_nav_altitude_valley", "text": "Wir bleiben auf Talhoehe — der Wind ist hier ruhiger." }, + + { "id": "r_land_approach", "text": "Zielanflug laeuft. Schwebeflug halten und Abstand zum Patient." }, + { "id": "r_land_winch_down", "text": "Seil laeuft, Flugretter wird abgesetzt." }, + { "id": "r_land_rescuer_down", "text": "Retter ist am Boden, Versorgung laeuft. Kreise in der Warteschleife." }, + { "id": "r_land_rescuer_ready", "text": "Patient stabilisiert, bereit zur Aufnahme. Ich brauche dich wieder ueber mir." }, + { "id": "r_land_hook_connected", "text": "Haken eingeklinkt, Seil wird eingezogen. Schwebeflug halten!" }, + { "id": "r_land_lift_success", "text": "Patient an Bord — Kurs Klinik." }, + { "id": "r_land_clinic_ahead", "text": "Klinik voraus. Sanft aufsetzen, Sinkrate unter zehn." }, + { "id": "r_land_disembark", "text": "Saubere Landung. Patient steigt aus, Einsatz abgeschlossen." } + ] +} diff --git a/App/sims/heli/scripts/pilot-situations-lines.json b/App/sims/heli/scripts/pilot-situations-lines.json new file mode 100644 index 0000000..8c0a45f --- /dev/null +++ b/App/sims/heli/scripts/pilot-situations-lines.json @@ -0,0 +1,19 @@ +{ + "_note": "Pilot-Situationsmeldungen im Start-Minispiel, die bisher nur als Textblase erschienen. Werden zusaetzlich zum Text abgespielt. Sarah/Nova-Stimme, Funk-Filter live ueber den Bandpass.", + "lines": [ + { "id": "r_pilot_wind_turn", "text": "Wind dreht! Gegenhalten!" }, + { "id": "r_pilot_bird_ahead", "text": "Vogelschwarm voraus!" }, + { "id": "r_pilot_plane_ahead", "text": "Verkehrsflugzeug im Anflug!" }, + { "id": "r_pilot_sportplane_level", "text": "Sportflieger auf gleicher Höhe!" }, + { "id": "r_pilot_storm_detour", "text": "Gewitter voraus, umfliegen!" }, + { "id": "r_pilot_fog_thickens", "text": "Nebel wird dichter, vorsichtig!" }, + { "id": "r_pilot_time_up", "text": "Zeit abgelaufen. Ich übernehme — Einsatzfenster verpasst." }, + { "id": "r_pilot_too_high", "text": "Nicht so hoch, das braucht zu viel Sprit!" }, + { "id": "r_pilot_three_strikes", "text": "Drei Berührungen. Ich übernehme — Absturzgefahr!" }, + { "id": "r_pilot_strike_1", "text": "Vorsicht! Das war knapp." }, + { "id": "r_pilot_strike_2", "text": "Aufpassen, Co-Pilot." }, + { "id": "r_pilot_strike_3", "text": "Zu nah dran!" }, + { "id": "r_pilot_strike_4", "text": "Konzentriert bleiben." }, + { "id": "r_pilot_strike_5", "text": "Ruhig, ich bin daneben." } + ] +} diff --git a/App/sims/heli/scripts/tower-geo-lines.json b/App/sims/heli/scripts/tower-geo-lines.json new file mode 100644 index 0000000..63d0eda --- /dev/null +++ b/App/sims/heli/scripts/tower-geo-lines.json @@ -0,0 +1,48 @@ +{ + "_note": "Pilot-Anruf + Tower-Antwort im Zielanflug. Standard-Funkprotokoll Adressat-Rufzeichen-Anliegen. Tower antwortet gespiegelt mit Roger und einem Geografie-Lernhinweis. Freundlicher Ton.", + "_voice_pilot": "Pilot-Calls sind knapp; Tower-Antworten warm und lehrreich. Beide mit Sarah (ElevenLabs, multilingual).", + "lines": [ + { "id": "r_pilot_call_c1", "text": "Innsbruck Tower, Christophorus eins, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c2", "text": "Wien Tower, Christophorus zwei, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c3", "text": "Wien Tower, Christophorus drei, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c4", "text": "Innsbruck Tower, Christophorus vier, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c5", "text": "Innsbruck Tower, Christophorus fünf, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c6", "text": "Salzburg Tower, Christophorus sechs, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c7", "text": "Klagenfurt Tower, Christophorus sieben, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c8", "text": "Bregenz Tower, Christophorus acht, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c9", "text": "Wien Tower, Christophorus neun, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c10", "text": "Linz Tower, Christophorus zehn, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c11", "text": "Klagenfurt Tower, Christophorus elf, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c12", "text": "Graz Tower, Christophorus zwölf, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c14", "text": "Graz Tower, Christophorus vierzehn, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_c16", "text": "Wien Tower, Christophorus sechzehn, Einsatzort erreicht, im Endanflug." }, + { "id": "r_pilot_call_ara", "text": "Bregenz Tower, ARA Rettungsflug eins, Einsatzort erreicht, im Endanflug." }, + + { "id": "r_tower_geo_m1", "text": "Christophorus acht, Bregenz Tower, Roger. Damüls ist der schneereichste Ort der Alpen — Stauregen vom Bodensee trifft hier auf die erste Kette des Bregenzerwalds, elf Meter Neuschnee im Jahresmittel. Bergung freigegeben, viel Erfolg." }, + { "id": "r_tower_geo_m2", "text": "ARA Rettungsflug eins, Bregenz Tower, Roger. Die A vierzehn ist die Lebensader des Rheintals — sie verbindet Vorarlberg mit Deutschland und der Schweiz. Wir haben die Strecke für dich freigegeben." }, + { "id": "r_tower_geo_m3", "text": "Christophorus acht, Bregenz Tower, Roger. Das Brandnertal liegt in der Rätikon-Kalkzone — die hellen Felswände sind Wettersteinkalk aus der Trias, zweihundert Millionen Jahre alt. Bergung freigegeben, wir sind bei dir." }, + { "id": "r_tower_geo_m4", "text": "Christophorus eins, Innsbruck Tower, Roger. Sölden liegt im Ötztal, einem klassischen Trogtal — U-förmig ausgeschliffen von einem Eiszeit-Gletscher. Bergung freigegeben, viel Erfolg." }, + { "id": "r_tower_geo_m5", "text": "ARA Rettungsflug eins, Bregenz Tower, Roger. Der Bodensee ist der drittgrößte See Mitteleuropas — und Trinkwasserspeicher für fünf Millionen Menschen. Bergung freigegeben, wir begleiten dich." }, + { "id": "r_tower_geo_m6", "text": "ARA Rettungsflug eins, Bregenz Tower, Roger. Der Bregenzerwald ist ein Flysch-Gebirge — weiche Schichtgesteine, deshalb viele enge Schluchten durch Erosion. Bergung freigegeben, sei vorsichtig im Kessel." }, + { "id": "r_tower_geo_m7", "text": "Christophorus eins, Innsbruck Tower, Roger. Das Kaisergebirge ist eine isolierte Kalkstock-Gruppe in den Nordalpen — schroffe Wände aus Wettersteinkalk, ein Paradies für Kletterer. Bergung freigegeben, wir sind bei dir." }, + { "id": "r_tower_geo_m8", "text": "Christophorus acht, Bregenz Tower, Roger. Das Montafon ist ein langes Trogtal — nach hinten immer enger, deshalb nur ein Zugang. Gaschurn liegt auf neunhundertsechzig Metern, höchstes ganzjährig besiedeltes Dorf im Tal. Bergung freigegeben." }, + { "id": "r_tower_geo_m9", "text": "Christophorus eins, Innsbruck Tower, Roger. Das Zillertal ist ein glaziales Trogtal — die Seitentäler hängen etwa dreihundert Meter höher, sogenannte Hängetäler. Achtung lawinenanfälliger Nordhang. Bergung freigegeben, viel Erfolg." }, + + { "id": "r_tower_geo_m10", "text": "Christophorus neun, Wien Tower, Roger. Die A zwei ist Österreichs wichtigste Süd-Ost-Achse — Wien, Graz, Klagenfurt, Grenze Slowenien. Der Wiener Speckgürtel mit den meisten Pendlern liegt genau hier. Strecke ist frei für dich." }, + { "id": "r_tower_geo_m11", "text": "Christophorus neun, Wien Tower, Roger. Die Neue Donau ist ein Hochwasserentlastungsarm — eigens ausgebaggert, um Wien vor Schmelzwasserfluten aus den Alpen zu schützen. Bergung freigegeben, wir sind bei dir." }, + { "id": "r_tower_geo_m12", "text": "Christophorus drei, Wien Tower, Roger. Der Schneeberg ist der östlichste Zweitausender der Alpen — dahinter beginnt das Pannonische Tiefland. Deshalb der harte Wetterwechsel hier. Bergung freigegeben, viel Erfolg." }, + { "id": "r_tower_geo_m13", "text": "Christophorus drei, Wien Tower, Roger. Die Rax ist ein großes Karstplateau — Regen versickert sofort im Kalk und tritt erst an der Höllentalklamm wieder aus. Deshalb ist oben kein Wasser zu finden. Bergung freigegeben." }, + { "id": "r_tower_geo_m14", "text": "Christophorus sechzehn, Wien Tower, Roger. Der Neusiedler See ist der westlichste Steppensee Europas — durchschnittlich nur eineinhalb Meter tief, deshalb kippt die Temperatur schnell. Teile des Sees trockneten in der Geschichte mehrfach komplett aus. Bergung freigegeben." }, + { "id": "r_tower_geo_m15", "text": "Christophorus zehn, Linz Tower, Roger. Sattledt ist der zentrale Autobahnknoten Oberösterreichs — A eins West und A acht Richtung Deutschland kreuzen sich. Vierzigtausend Durchfahrten am Tag. Strecke ist für dich frei." }, + { "id": "r_tower_geo_m16", "text": "Christophorus zehn, Linz Tower, Roger. Der Traunsee ist ein glazialer Zungenbeckensee — einhundertneunzig Meter tief. Der Traunstein dahinter ist eine Kalkklippe, typisch für den Nordrand der Alpen. Bergung freigegeben, wir sind bei dir." }, + { "id": "r_tower_geo_m17", "text": "Christophorus sechs, Salzburg Tower, Roger. Der Tauerntunnel durchsticht die Hauptkette der Alpen — sechs Komma vier Kilometer lang, verkürzt Salzburg und Kärnten um eine ganze Stunde. Strecke ist für dich frei." }, + { "id": "r_tower_geo_m18", "text": "Christophorus sechs, Salzburg Tower, Roger. Der Wolfgangsee liegt im Salzkammergut — seinen Namen verdankt er dem Salzhandel. Sole wurde über den See von Hallstatt nach Ebensee transportiert. Bergung freigegeben, viel Erfolg." }, + { "id": "r_tower_geo_m19", "text": "Christophorus sechs, Salzburg Tower, Roger. Der Großglockner liegt auf der Hauptkette der Hohen Tauern, Grenze zwischen Salzburg und Kärnten. Die Pasterze darunter ist Österreichs längster Gletscher — sie verliert jedes Jahr etwa zehn Meter. Bergung freigegeben." }, + { "id": "r_tower_geo_m20", "text": "Christophorus vierzehn, Graz Tower, Roger. Der Dachstein ist ein Kalkstock mit Gletscher obenauf — eine echte Seltenheit. Die Südwand fällt achthundert Meter senkrecht ab, die Nordseite ist ein Hochplateau. Bergung freigegeben, sei vorsichtig." }, + { "id": "r_tower_geo_m21", "text": "Christophorus zwölf, Graz Tower, Roger. Der Hochschwab versorgt Wien mit Trinkwasser — die Wiener Hochquellenleitung beginnt hier, einhundertfünfzig Kilometer Gefälletransport ohne Pumpen. Bergung freigegeben, wir sind bei dir." }, + { "id": "r_tower_geo_m22", "text": "Christophorus zwölf, Graz Tower, Roger. Der Bosrucktunnel verbindet das Ennstal mit dem Pyhrnpass — ältester Alpenübergang zwischen Salzkammergut und Obersteiermark, schon die Römer nutzten die Route. Strecke ist frei." }, + { "id": "r_tower_geo_m23", "text": "Christophorus elf, Klagenfurt Tower, Roger. Der Wörthersee ist Österreichs wärmster Badesee — bis achtundzwanzig Grad. Grund ist die geschützte Föhnlage südlich der Gurktaler Alpen, kaum kalter Abwind. Bergung freigegeben." }, + { "id": "r_tower_geo_m24", "text": "Christophorus elf, Klagenfurt Tower, Roger. Du fliegst zur Südseite des Großglockners — Heiligenblut liegt am Fuß der Pasterze. Der Gipfel ist vereist, Wolken können schnell heraufziehen. Bergung freigegeben, viel Erfolg." }, + { "id": "r_tower_geo_m25", "text": "Christophorus elf, Klagenfurt Tower, Roger. Die A zwei ist hier die Zufahrt zum Karawankentunnel — Grenze zu Slowenien, einziger Ganzjahres-Autobahnübergang Kärnten-Slowenien, achtkommacht Kilometer lang. Strecke ist frei für dich." } + ] +} diff --git a/App/sims/heli/scripts/tower-heliintro-lines.json b/App/sims/heli/scripts/tower-heliintro-lines.json new file mode 100644 index 0000000..03ef63b --- /dev/null +++ b/App/sims/heli/scripts/tower-heliintro-lines.json @@ -0,0 +1,20 @@ +{ + "_note": "Pro Hubschrauber ein Flug-Intro mit Geografie-Lernstoff des Einsatzraums. Wird einmal am Beginn des Kartenflugs abgespielt — statt generischer Segment-Floskeln.", + "lines": [ + { "id": "r_nav_heli_intro_c1", "text": "Christophorus eins aus Innsbruck, Einsatzraum Nord- und Zentral-Tirol. Unter uns das Inntal — der Inn entwässert das gesamte Tiroler Zentralalpenland und mündet bei Passau in die Donau." }, + { "id": "r_nav_heli_intro_c2", "text": "Christophorus zwei aus Krems an der Donau, Einsatzraum Wachau und Waldviertel. Die Wachau ist UNESCO-Welterbe — Weinbau an den sonnigen Terrassen seit der Römerzeit." }, + { "id": "r_nav_heli_intro_c3", "text": "Christophorus drei aus Wiener Neustadt, Einsatzraum Industrieviertel und Wiener Alpen. Hier geht das Wiener Becken in die letzten Ausläufer der Nordalpen über." }, + { "id": "r_nav_heli_intro_c4", "text": "Christophorus vier aus Reith bei Kitzbühel, Einsatzraum Kitzbüheler Alpen und Pinzgau. Die Kitzbüheler Alpen liegen in der Grauwackenzone — weichere Gesteine als die nördlichen Kalkalpen, daher sanftere Formen." }, + { "id": "r_nav_heli_intro_c5", "text": "Christophorus fünf aus Zams im Oberinntal, Einsatzraum westliches Tirol. Der Inn zieht unter uns nach Osten, bis er bei Innsbruck ins Unterinntal dreht." }, + { "id": "r_nav_heli_intro_c6", "text": "Christophorus sechs vom Flughafen Salzburg, Einsatzraum Flachgau und Salzkammergut. Die Salzach unter uns ist Namensgeberin der Stadt und des Landes — einst Transportweg fürs Hallein-Salz." }, + { "id": "r_nav_heli_intro_c7", "text": "Christophorus sieben aus Lienz in Osttirol, Einsatzraum Osttirol und Oberkärnten. Die Drau hier fließt nach Süden — einer der wenigen Flüsse Österreichs, der nicht zur Donau entwässert, sondern zur Adria." }, + { "id": "r_nav_heli_intro_c8", "text": "Christophorus acht aus Nenzing im Walgau, Einsatzraum Vorarlberg. Der Walgau trennt den Rätikon im Süden vom Lechquellengebirge im Norden — der Ill-Fluss hat ihn ausgehobelt." }, + { "id": "r_nav_heli_intro_c9", "text": "Christophorus neun aus Wien-Aspern, Einsatzraum Wiener Becken. Das Becken ist ein tektonisches Senkungsgebiet zwischen Alpen und Karpaten — mit Pannonischer Tiefebene nach Osten." }, + { "id": "r_nav_heli_intro_c10", "text": "Christophorus zehn vom Flughafen Linz-Hörsching, Einsatzraum Oberösterreich. Unter uns das Alpenvorland — eine ehemalige Meeresbucht der Paratethys, heute fruchtbares Ackerland." }, + { "id": "r_nav_heli_intro_c11", "text": "Christophorus elf vom Flughafen Klagenfurt, Einsatzraum Kärnten. Das Klagenfurter Becken ist im Süden von den Karawanken begrenzt — zugleich die Staatsgrenze zu Slowenien." }, + { "id": "r_nav_heli_intro_c12", "text": "Christophorus zwölf vom Flughafen Graz-Thalerhof, Einsatzraum Steiermark. Das Grazer Becken öffnet sich nach Osten zur Pannonischen Tiefebene — wärmere und trockenere Klimazone." }, + { "id": "r_nav_heli_intro_c14", "text": "Christophorus vierzehn vom Flugplatz Niederöblarn im Ennstal, Einsatzraum obere Steiermark. Die Enns trennt die Kalkalpen im Norden von den Tauern im Süden." }, + { "id": "r_nav_heli_intro_c16", "text": "Christophorus sechzehn vom Flugplatz Pinkafeld, Einsatzraum Burgenland und Südoststeiermark. Die Ebene hier ist Pannonisches Tiefland — geologisch schon nicht mehr Alpen." }, + { "id": "r_nav_heli_intro_ara", "text": "ARA Rettungsflug eins aus Hohenems, Einsatzraum Vorarlberg und Bodensee. Der Bodensee ist ein Zungenbeckensee — ausgehobelt vom Rheingletscher der letzten Eiszeit." } + ] +} diff --git a/App/sims/heli/scripts/tower-lines.json b/App/sims/heli/scripts/tower-lines.json new file mode 100644 index 0000000..038c856 --- /dev/null +++ b/App/sims/heli/scripts/tower-lines.json @@ -0,0 +1,63 @@ +{ + "_note": "Tower-Ansagen pro Hubschrauber und Phase. Wird von generate-tower-lines.sh an ElevenLabs geschickt.", + "_voice": "empfohlen: freundliche junge weibliche deutsche Stimme (ElevenLabs: z. B. 'Sarah' EXAVITQu4vr4xnSDxMaL). Voice-ID über ELEVENLABS_VOICE_ID env.", + "lines": [ + { "id": "r_tower_approach_c1", "text": "Tower an ÖAMTC Christophorus eins — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c2", "text": "Tower an ÖAMTC Christophorus zwei — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c3", "text": "Tower an ÖAMTC Christophorus drei — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c4", "text": "Tower an ÖAMTC Christophorus vier — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c5", "text": "Tower an ÖAMTC Christophorus fünf — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c6", "text": "Tower an ÖAMTC Christophorus sechs — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c7", "text": "Tower an ÖAMTC Christophorus sieben — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c8", "text": "Tower an ÖAMTC Christophorus acht — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c9", "text": "Tower an ÖAMTC Christophorus neun — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c10", "text": "Tower an ÖAMTC Christophorus zehn — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c11", "text": "Tower an ÖAMTC Christophorus elf — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c12", "text": "Tower an ÖAMTC Christophorus zwölf — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c14", "text": "Tower an ÖAMTC Christophorus vierzehn — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_c16", "text": "Tower an ÖAMTC Christophorus sechzehn — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + { "id": "r_tower_approach_ara", "text": "Tower an ARA Rettungsflug eins — Einsatzort erreicht. Zielanflug freigegeben, viel Erfolg." }, + + { "id": "r_tower_extract_c1", "text": "ÖAMTC Christophorus eins, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c2", "text": "ÖAMTC Christophorus zwei, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c3", "text": "ÖAMTC Christophorus drei, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c4", "text": "ÖAMTC Christophorus vier, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c5", "text": "ÖAMTC Christophorus fünf, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c6", "text": "ÖAMTC Christophorus sechs, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c7", "text": "ÖAMTC Christophorus sieben, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c8", "text": "ÖAMTC Christophorus acht, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c9", "text": "ÖAMTC Christophorus neun, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c10", "text": "ÖAMTC Christophorus zehn, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c11", "text": "ÖAMTC Christophorus elf, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c12", "text": "ÖAMTC Christophorus zwölf, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c14", "text": "ÖAMTC Christophorus vierzehn, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_c16", "text": "ÖAMTC Christophorus sechzehn, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + { "id": "r_tower_extract_ara", "text": "ARA Rettungsflug eins, Direktfreigabe zur Klinik. Patient hat oberste Priorität." }, + + { "id": "r_tower_hw_approach_ara", "text": "Tower an ARA Rettungsflug eins — Sperrzone auf der A vierzehn erreicht. Landezone ist markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_ara", "text": "ARA Rettungsflug eins, Strecke frei — Ausflug zur Klinik Feldkirch freigegeben." }, + { "id": "r_tower_hw_approach_c6", "text": "Tower an ÖAMTC Christophorus sechs — Sperrzone auf der A zehn erreicht. Landezone markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_c6", "text": "ÖAMTC Christophorus sechs, Tauernstrecke frei — Ausflug zur Klinik Salzburg freigegeben." }, + { "id": "r_tower_hw_approach_c9", "text": "Tower an ÖAMTC Christophorus neun — Sperrzone auf der A zwei erreicht. Landezone markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_c9", "text": "ÖAMTC Christophorus neun, Strecke frei — Ausflug zur Klinik Wien freigegeben." }, + { "id": "r_tower_hw_approach_c10","text": "Tower an ÖAMTC Christophorus zehn — Sperrzone auf der A eins erreicht. Landezone markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_c10","text": "ÖAMTC Christophorus zehn, Westautobahn frei — Ausflug zur Klinik Linz freigegeben." }, + { "id": "r_tower_hw_approach_c11","text": "Tower an ÖAMTC Christophorus elf — Sperrzone auf der A zwei erreicht. Landezone markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_c11","text": "ÖAMTC Christophorus elf, Südautobahn frei — Ausflug zur Klinik Klagenfurt freigegeben." }, + { "id": "r_tower_hw_approach_c12","text": "Tower an ÖAMTC Christophorus zwölf — Sperrzone auf der A neun erreicht. Landezone markiert, Fahrbahn frei." }, + { "id": "r_tower_hw_extract_c12","text": "ÖAMTC Christophorus zwölf, Pyhrnstrecke frei — Ausflug zur Klinik Graz freigegeben." }, + + { "id": "r_tower_sea_approach_ara", "text": "Tower an ARA Rettungsflug eins — Seenotlage im Blick. Schwebeflug auf dreißig Meter, Rotorabwind beachten." }, + { "id": "r_tower_sea_extract_ara", "text": "ARA Rettungsflug eins, Patient an Bord — Anflug auf die Uferklinik freigegeben." }, + { "id": "r_tower_sea_approach_c6", "text": "Tower an ÖAMTC Christophorus sechs — Seenotlage Wolfgangsee im Blick. Schwebeflug auf dreißig Meter." }, + { "id": "r_tower_sea_extract_c6", "text": "ÖAMTC Christophorus sechs, Patient an Bord — Anflug auf die Klinik Salzburg freigegeben." }, + { "id": "r_tower_sea_approach_c9", "text": "Tower an ÖAMTC Christophorus neun — Einsatzlage Neue Donau im Blick. Schwebeflug, Strömung beachten." }, + { "id": "r_tower_sea_extract_c9", "text": "ÖAMTC Christophorus neun, Patient an Bord — Anflug auf die Klinik Wien freigegeben." }, + { "id": "r_tower_sea_approach_c10","text": "Tower an ÖAMTC Christophorus zehn — Seenotlage Traunsee im Blick. Schwebeflug auf dreißig Meter." }, + { "id": "r_tower_sea_extract_c10","text": "ÖAMTC Christophorus zehn, Patient an Bord — Anflug auf die Klinik Gmunden freigegeben." }, + { "id": "r_tower_sea_approach_c11","text": "Tower an ÖAMTC Christophorus elf — Seenotlage Wörthersee im Blick. Schwebeflug auf dreißig Meter." }, + { "id": "r_tower_sea_extract_c11","text": "ÖAMTC Christophorus elf, Patient an Bord — Anflug auf die Klinik Klagenfurt freigegeben." }, + { "id": "r_tower_sea_approach_c16","text": "Tower an ÖAMTC Christophorus sechzehn — Seenotlage Neusiedler See im Blick. Achtung starker Seewind." }, + { "id": "r_tower_sea_extract_c16","text": "ÖAMTC Christophorus sechzehn, Patient an Bord — Anflug auf die Klinik Eisenstadt freigegeben." } + ] +} diff --git a/App/sims/heli/scripts/tower-sea-lines.json b/App/sims/heli/scripts/tower-sea-lines.json new file mode 100644 index 0000000..5734140 --- /dev/null +++ b/App/sims/heli/scripts/tower-sea-lines.json @@ -0,0 +1,15 @@ +{ + "_note": "Seerettung-Tower-Audios fuer c6/c9/c10/c11/c16. Ergaenzt die bestehenden ARA-Sea-Audios. Muster analog zu tower_sea_approach_ara bzw. tower_sea_extract_ara.", + "lines": [ + { "id": "r_tower_sea_approach_c6", "text": "Christophorus sechs, Salzburg Tower, Roger. Wasserlage im Blick — Schwebeflug auf dreißig Meter, Rotorabwind beachten." }, + { "id": "r_tower_sea_extract_c6", "text": "Christophorus sechs, Salzburg Tower, Patient an Bord — Anflug auf die Klinik Salzburg freigegeben." }, + { "id": "r_tower_sea_approach_c9", "text": "Christophorus neun, Wien Tower, Roger. Einsatzlage Neue Donau im Blick — Schwebeflug, Stroemung und Sog beachten." }, + { "id": "r_tower_sea_extract_c9", "text": "Christophorus neun, Wien Tower, Patient an Bord — Anflug auf die Klinik Wien freigegeben." }, + { "id": "r_tower_sea_approach_c10", "text": "Christophorus zehn, Linz Tower, Roger. Seenotlage Traunsee im Blick — Schwebeflug auf dreißig Meter, Rotorabwind beachten." }, + { "id": "r_tower_sea_extract_c10", "text": "Christophorus zehn, Linz Tower, Patient an Bord — Anflug auf die Klinik Gmunden freigegeben." }, + { "id": "r_tower_sea_approach_c11", "text": "Christophorus elf, Klagenfurt Tower, Roger. Seenotlage im Blick — Schwebeflug auf dreißig Meter, Rotorabwind beachten." }, + { "id": "r_tower_sea_extract_c11", "text": "Christophorus elf, Klagenfurt Tower, Patient an Bord — Anflug auf die Klinik Klagenfurt freigegeben." }, + { "id": "r_tower_sea_approach_c16", "text": "Christophorus sechzehn, Eisenstadt Tower, Roger. Seenotlage Neusiedler See im Blick — Achtung starker Seewind." }, + { "id": "r_tower_sea_extract_c16", "text": "Christophorus sechzehn, Eisenstadt Tower, Patient an Bord — Anflug auf die Klinik Eisenstadt freigegeben." } + ] +} diff --git a/App/sims/heli/scripts/tower-start-lines.json b/App/sims/heli/scripts/tower-start-lines.json new file mode 100644 index 0000000..6dc7197 --- /dev/null +++ b/App/sims/heli/scripts/tower-start-lines.json @@ -0,0 +1,78 @@ +{ + "_note": "Tower-Lehrmeisterin fuer das Start-Minispiel. Ebene A = pro Heli personalisierte Funksprueche (Startfreigabe, nach Beruehrung, Chefpilot-Uebernahme). Ebene B = kontextuelle Lehrinhalte, generisch ohne Helinamen.", + "_voice": "Sarah (ElevenLabs multilingual v2), freundlich, warme Stimme. Pilot-Calls kommen als Textblase, Tower antwortet mit Audio.", + "lines": [ + { "id": "r_tower_start_c1", "text": "Christophorus eins, Stützpunkt Innsbruck-Flughafen, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c2", "text": "Christophorus zwei, Stützpunkt Krems-Gneixendorf, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c3", "text": "Christophorus drei, Flugplatz Wiener Neustadt Ost, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c4", "text": "Christophorus vier, Stützpunkt Reith bei Kitzbühel, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c5", "text": "Christophorus fünf, Stützpunkt Zams, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c6", "text": "Christophorus sechs, Flughafen Salzburg, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c7", "text": "Christophorus sieben, Stützpunkt Lienz, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c8", "text": "Christophorus acht, Stützpunkt Nenzing, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c9", "text": "Christophorus neun, Stützpunkt Wien-Aspern, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c10", "text": "Christophorus zehn, Flughafen Linz-Hörsching, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c11", "text": "Christophorus elf, Flughafen Klagenfurt, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c12", "text": "Christophorus zwölf, Flughafen Graz-Thalerhof, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c14", "text": "Christophorus vierzehn, Flugplatz Niederöblarn, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_c16", "text": "Christophorus sechzehn, Flugplatz Pinkafeld, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + { "id": "r_tower_start_ara", "text": "ARA Rettungsflug eins, Flugplatz Hohenems, Roger. Startplatz frei, Wind im Rahmen — Startfreigabe erteilt. Guter Flug." }, + + { "id": "r_tower_strike_c1", "text": "Christophorus eins, Innsbruck Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c2", "text": "Christophorus zwei, Wien Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c3", "text": "Christophorus drei, Wien Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c4", "text": "Christophorus vier, Innsbruck Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c5", "text": "Christophorus fünf, Innsbruck Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c6", "text": "Christophorus sechs, Salzburg Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c7", "text": "Christophorus sieben, Klagenfurt Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c8", "text": "Christophorus acht, Bregenz Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c9", "text": "Christophorus neun, Wien Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c10", "text": "Christophorus zehn, Linz Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c11", "text": "Christophorus elf, Klagenfurt Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c12", "text": "Christophorus zwölf, Graz Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c14", "text": "Christophorus vierzehn, Graz Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_c16", "text": "Christophorus sechzehn, Wien Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + { "id": "r_tower_strike_ara", "text": "ARA Rettungsflug eins, Bregenz Tower. Kein Drama, das passiert — noch zwei Versuche, du kriegst das hin." }, + + { "id": "r_tower_handover_c1", "text": "Christophorus eins, Innsbruck Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c2", "text": "Christophorus zwei, Wien Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c3", "text": "Christophorus drei, Wien Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c4", "text": "Christophorus vier, Innsbruck Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c5", "text": "Christophorus fünf, Innsbruck Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c6", "text": "Christophorus sechs, Salzburg Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c7", "text": "Christophorus sieben, Klagenfurt Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c8", "text": "Christophorus acht, Bregenz Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c9", "text": "Christophorus neun, Wien Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c10", "text": "Christophorus zehn, Linz Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c11", "text": "Christophorus elf, Klagenfurt Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c12", "text": "Christophorus zwölf, Graz Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c14", "text": "Christophorus vierzehn, Graz Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_c16", "text": "Christophorus sechzehn, Wien Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + { "id": "r_tower_handover_ara", "text": "ARA Rettungsflug eins, Bregenz Tower. Heute übernimmt der Chefpilot — wir üben das nächste Mal wieder. Alles gut, du lernst dazu." }, + + { "id": "r_tower_lesson_wind", "text": "Achtung, Seitenwind über zwanzig Knoten — der Heck-Rotor schiebt weg, leichter Pedal-Gegendruck genügt. Du machst das gut." }, + { "id": "r_tower_lesson_foehn", "text": "Föhn ist ein warmer Fallwind über einem Gebirgskamm — die Luft wird beim Absteigen komprimiert und erwärmt, kann zwanzig Grad Unterschied bringen. Bei Föhn Turbulenzen erwarten." }, + { "id": "r_tower_lesson_fog", "text": "Advektionsnebel entsteht, wenn warme feuchte Luft über kalten Boden zieht. Die Sicht kann in Sekunden zusammenbrechen — Abstand zu Hindernissen halten." }, + { "id": "r_tower_lesson_thermic", "text": "Thermik steigt über dunklen Flächen auf — Asphalt, Felsen, Dächern. Über Wasser und Wald dagegen kaum. Deshalb werden Flüge am Nachmittag unruhiger." }, + { "id": "r_tower_lesson_temperature","text": "Pro hundert Meter Höhe sinkt die Temperatur um rund zwei Drittel Grad. Auf dreitausend Metern ist es zwanzig Grad kälter als im Tal — denk dran im Einsatzgebiet." }, + + { "id": "r_tower_lesson_powerline","text": "Freileitungen sieht man von oben kaum — die Masten verraten sie. Zwischen zwei Masten immer Mindestabstand siebzig Meter zur Leitung." }, + { "id": "r_tower_lesson_antenna", "text": "Sendemasten stehen oft auf Kuppen — höchster Punkt der Umgebung, Blitzableiter. Kommt dir eine Spitze entgegen, beim Steigflug lieber einen Bogen fliegen." }, + { "id": "r_tower_lesson_crane", "text": "Baukräne haben bis achtzig Meter Radius und sind oft kaum markiert. Vor dem Bauland lieber großzügig ausweichen, Wind kann sie drehen." }, + { "id": "r_tower_lesson_chimney", "text": "Fabrikschlote ragen teils über einhundert Meter auf — der Rauch verrät die Windrichtung am Boden. Lehrstück ohne Kosten." }, + { "id": "r_tower_lesson_cable", "text": "Seilbahnen verlaufen oft quer zu Tälern und sind dünn, aber tödlich. Immer über der obersten Station bleiben, niemals quer zur Trasse durchs Tal." }, + + { "id": "r_tower_lesson_hover", "text": "Im Schwebeflug verbraucht der Heli sechs Liter pro Minute — fast doppelt so viel wie im Vorwärtsflug. Jede Minute über dem Patienten kostet Reichweite." }, + { "id": "r_tower_lesson_vortex", "text": "Achtung Vortex-Ring: wenn die Sinkrate den Rotorabwind überholt, verliert der Heli Auftrieb. Immer leicht schräg sinken, nicht senkrecht." }, + { "id": "r_tower_lesson_torque", "text": "Der Hauptrotor erzeugt ein Drehmoment, das den Heli in die Gegenrichtung drehen will. Dafür ist der Heck-Rotor da — fällt er aus, dreht sich die ganze Maschine." }, + { "id": "r_tower_lesson_downwash", "text": "Der Rotorabwind drückt mit sechzig Stundenkilometern nach unten. Über Wasser sofort Wellen, über Schnee Blizzard, über Geröll Steinschlag. Abstand halten." }, + { "id": "r_tower_lesson_density", "text": "In großer Höhe ist die Luft dünn — weniger Auftrieb, mehr Leistung nötig. Über dreitausend Meter wird die Luft so dünn, dass manche Helis gar nicht mehr steigen können." }, + + { "id": "r_tower_lesson_crows", "text": "Krähenschwärme starten bei Lärm schlagartig auf — vor dem Schwarm runter oder weit ausweichen. Vogelschlag ist die häufigste Absturzursache zivil." }, + { "id": "r_tower_lesson_gulls", "text": "Möwen fliegen in Wellenbewegung dicht über der Wasseroberfläche — bei Rotorabwind irritieren sie. Erst in mittlerer Höhe anfliegen." }, + + { "id": "r_tower_lesson_banking", "text": "Ab dreißig Grad Schräglage wird der Rotor ineffizient — nie aggressiv kurven, vor allem nicht talseitig. Sanfte Bögen sind schneller als harte Kurven." }, + { "id": "r_tower_lesson_pilot", "text": "Pilot und Co-Pilot wechseln sich in der Führung ab — immer einer fliegt, einer beobachtet Instrumente und Umgebung. Niemals beide gleichzeitig navigieren." }, + { "id": "r_tower_lesson_autorot", "text": "Bei Triebwerksausfall fällt der Heli nicht wie ein Stein — der Rotor dreht weiter, das nennt man Autorotation. Geradeaus gleiten, am Boden kurz vor Bodenberührung nochmal ziehen." } + ] +} diff --git a/App/sims/heli/sounds/bigPlane.mp3 b/App/sims/heli/sounds/bigPlane.mp3 new file mode 100644 index 0000000..92aa505 Binary files /dev/null and b/App/sims/heli/sounds/bigPlane.mp3 differ diff --git a/App/sims/heli/sounds/birdCrash.mp3 b/App/sims/heli/sounds/birdCrash.mp3 new file mode 100644 index 0000000..e78da90 Binary files /dev/null and b/App/sims/heli/sounds/birdCrash.mp3 differ diff --git a/App/sims/heli/sounds/birds1.mp3 b/App/sims/heli/sounds/birds1.mp3 new file mode 100644 index 0000000..dc4aaa2 Binary files /dev/null and b/App/sims/heli/sounds/birds1.mp3 differ diff --git a/App/sims/heli/sounds/birds2.mp3 b/App/sims/heli/sounds/birds2.mp3 new file mode 100644 index 0000000..bcc62ae Binary files /dev/null and b/App/sims/heli/sounds/birds2.mp3 differ diff --git a/App/sims/heli/sounds/birds3.mp3 b/App/sims/heli/sounds/birds3.mp3 new file mode 100644 index 0000000..0258192 Binary files /dev/null and b/App/sims/heli/sounds/birds3.mp3 differ diff --git a/App/sims/heli/sounds/captiainSpeaking.mp3 b/App/sims/heli/sounds/captiainSpeaking.mp3 new file mode 100644 index 0000000..f5b3e4e Binary files /dev/null and b/App/sims/heli/sounds/captiainSpeaking.mp3 differ diff --git a/App/sims/heli/sounds/crash1.mp3 b/App/sims/heli/sounds/crash1.mp3 new file mode 100644 index 0000000..74661b5 Binary files /dev/null and b/App/sims/heli/sounds/crash1.mp3 differ diff --git a/App/sims/heli/sounds/crash2.mp3 b/App/sims/heli/sounds/crash2.mp3 new file mode 100644 index 0000000..273b853 Binary files /dev/null and b/App/sims/heli/sounds/crash2.mp3 differ diff --git a/App/sims/heli/sounds/crash3.mp3 b/App/sims/heli/sounds/crash3.mp3 new file mode 100644 index 0000000..7225aa1 Binary files /dev/null and b/App/sims/heli/sounds/crash3.mp3 differ diff --git a/App/sims/heli/sounds/crashBirds.mp3 b/App/sims/heli/sounds/crashBirds.mp3 new file mode 100644 index 0000000..1528ace Binary files /dev/null and b/App/sims/heli/sounds/crashBirds.mp3 differ diff --git a/App/sims/heli/sounds/crashBuilding.mp3 b/App/sims/heli/sounds/crashBuilding.mp3 new file mode 100644 index 0000000..0eb3b3b Binary files /dev/null and b/App/sims/heli/sounds/crashBuilding.mp3 differ diff --git a/App/sims/heli/sounds/heliFly.mp3 b/App/sims/heli/sounds/heliFly.mp3 new file mode 100644 index 0000000..ac0e754 Binary files /dev/null and b/App/sims/heli/sounds/heliFly.mp3 differ diff --git a/App/sims/heli/sounds/nav/dir_nord.mp3 b/App/sims/heli/sounds/nav/dir_nord.mp3 new file mode 100644 index 0000000..4bc3850 Binary files /dev/null and b/App/sims/heli/sounds/nav/dir_nord.mp3 differ diff --git a/App/sims/heli/sounds/nav/dir_nordost.mp3 b/App/sims/heli/sounds/nav/dir_nordost.mp3 new file mode 100644 index 0000000..27c1748 Binary files /dev/null and b/App/sims/heli/sounds/nav/dir_nordost.mp3 differ diff --git a/App/sims/heli/sounds/nav/dir_nordwest.mp3 b/App/sims/heli/sounds/nav/dir_nordwest.mp3 new file mode 100644 index 0000000..9db76f4 Binary files /dev/null and b/App/sims/heli/sounds/nav/dir_nordwest.mp3 differ diff --git a/App/sims/heli/sounds/nav/dir_ost.mp3 b/App/sims/heli/sounds/nav/dir_ost.mp3 new file mode 100644 index 0000000..7302dd7 Binary files /dev/null and b/App/sims/heli/sounds/nav/dir_ost.mp3 differ diff --git a/App/sims/heli/sounds/nav/dir_sued.mp3 b/App/sims/heli/sounds/nav/dir_sued.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/dir_sued.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/dir_suedost.mp3 b/App/sims/heli/sounds/nav/dir_suedost.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/dir_suedost.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/dir_suedwest.mp3 b/App/sims/heli/sounds/nav/dir_suedwest.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/dir_suedwest.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/dir_west.mp3 b/App/sims/heli/sounds/nav/dir_west.mp3 new file mode 100644 index 0000000..67d59ac Binary files /dev/null and b/App/sims/heli/sounds/nav/dir_west.mp3 differ diff --git a/App/sims/heli/sounds/nav/fly_berg.mp3 b/App/sims/heli/sounds/nav/fly_berg.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/fly_berg.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/fly_fluss.mp3 b/App/sims/heli/sounds/nav/fly_fluss.mp3 new file mode 100644 index 0000000..0df426c Binary files /dev/null and b/App/sims/heli/sounds/nav/fly_fluss.mp3 differ diff --git a/App/sims/heli/sounds/nav/fly_hoch.mp3 b/App/sims/heli/sounds/nav/fly_hoch.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/fly_hoch.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/fly_ort.mp3 b/App/sims/heli/sounds/nav/fly_ort.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/fly_ort.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/fly_see.mp3 b/App/sims/heli/sounds/nav/fly_see.mp3 new file mode 100644 index 0000000..802a9a0 Binary files /dev/null and b/App/sims/heli/sounds/nav/fly_see.mp3 differ diff --git a/App/sims/heli/sounds/nav/fly_tal.mp3 b/App/sims/heli/sounds/nav/fly_tal.mp3 new file mode 100644 index 0000000..acafa39 Binary files /dev/null and b/App/sims/heli/sounds/nav/fly_tal.mp3 differ diff --git a/App/sims/heli/sounds/nav/korr_allg.mp3 b/App/sims/heli/sounds/nav/korr_allg.mp3 new file mode 100644 index 0000000..90b1d62 Binary files /dev/null and b/App/sims/heli/sounds/nav/korr_allg.mp3 differ diff --git a/App/sims/heli/sounds/nav/korr_nah.mp3 b/App/sims/heli/sounds/nav/korr_nah.mp3 new file mode 100644 index 0000000..f455ede Binary files /dev/null and b/App/sims/heli/sounds/nav/korr_nah.mp3 differ diff --git a/App/sims/heli/sounds/nav/korr_nord.mp3 b/App/sims/heli/sounds/nav/korr_nord.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/korr_nord.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/korr_ost.mp3 b/App/sims/heli/sounds/nav/korr_ost.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/korr_ost.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/korr_sued.mp3 b/App/sims/heli/sounds/nav/korr_sued.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/korr_sued.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/korr_west.mp3 b/App/sims/heli/sounds/nav/korr_west.mp3 new file mode 100644 index 0000000..306d597 Binary files /dev/null and b/App/sims/heli/sounds/nav/korr_west.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob1.mp3 b/App/sims/heli/sounds/nav/lob1.mp3 new file mode 100644 index 0000000..70d0dc2 Binary files /dev/null and b/App/sims/heli/sounds/nav/lob1.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob10.mp3 b/App/sims/heli/sounds/nav/lob10.mp3 new file mode 100644 index 0000000..7767d5c Binary files /dev/null and b/App/sims/heli/sounds/nav/lob10.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob2.mp3 b/App/sims/heli/sounds/nav/lob2.mp3 new file mode 100644 index 0000000..e0efa18 Binary files /dev/null and b/App/sims/heli/sounds/nav/lob2.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob3.mp3 b/App/sims/heli/sounds/nav/lob3.mp3 new file mode 100644 index 0000000..0a3dede Binary files /dev/null and b/App/sims/heli/sounds/nav/lob3.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob4.mp3 b/App/sims/heli/sounds/nav/lob4.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/lob4.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/lob5.mp3 b/App/sims/heli/sounds/nav/lob5.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/lob5.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/lob6.mp3 b/App/sims/heli/sounds/nav/lob6.mp3 new file mode 100644 index 0000000..02ddf77 Binary files /dev/null and b/App/sims/heli/sounds/nav/lob6.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob7.mp3 b/App/sims/heli/sounds/nav/lob7.mp3 new file mode 100644 index 0000000..0a1ec46 Binary files /dev/null and b/App/sims/heli/sounds/nav/lob7.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob8.mp3 b/App/sims/heli/sounds/nav/lob8.mp3 new file mode 100644 index 0000000..9fb648b Binary files /dev/null and b/App/sims/heli/sounds/nav/lob8.mp3 differ diff --git a/App/sims/heli/sounds/nav/lob9.mp3 b/App/sims/heli/sounds/nav/lob9.mp3 new file mode 100644 index 0000000..f74fd22 Binary files /dev/null and b/App/sims/heli/sounds/nav/lob9.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_au.mp3 b/App/sims/heli/sounds/nav/ort_au.mp3 new file mode 100644 index 0000000..959892b Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_au.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_bezau.mp3 b/App/sims/heli/sounds/nav/ort_bezau.mp3 new file mode 100644 index 0000000..9ba5613 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_bezau.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_bludenz.mp3 b/App/sims/heli/sounds/nav/ort_bludenz.mp3 new file mode 100644 index 0000000..8ee1181 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_bludenz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_brand.mp3 b/App/sims/heli/sounds/nav/ort_brand.mp3 new file mode 100644 index 0000000..099660a Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_brand.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_bregenz.mp3 b/App/sims/heli/sounds/nav/ort_bregenz.mp3 new file mode 100644 index 0000000..2d3d987 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_bregenz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_brenner.mp3 b/App/sims/heli/sounds/nav/ort_brenner.mp3 new file mode 100644 index 0000000..3052bf2 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_brenner.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_buers.mp3 b/App/sims/heli/sounds/nav/ort_buers.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_buers.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_damuels.mp3 b/App/sims/heli/sounds/nav/ort_damuels.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_damuels.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_dornbirn.mp3 b/App/sims/heli/sounds/nav/ort_dornbirn.mp3 new file mode 100644 index 0000000..06cec04 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_dornbirn.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_egg.mp3 b/App/sims/heli/sounds/nav/ort_egg.mp3 new file mode 100644 index 0000000..0f00daa Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_egg.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_feldkirch.mp3 b/App/sims/heli/sounds/nav/ort_feldkirch.mp3 new file mode 100644 index 0000000..b60ef0c Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_feldkirch.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_fontanella.mp3 b/App/sims/heli/sounds/nav/ort_fontanella.mp3 new file mode 100644 index 0000000..f9930d1 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_fontanella.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_fulpmes.mp3 b/App/sims/heli/sounds/nav/ort_fulpmes.mp3 new file mode 100644 index 0000000..96441c3 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_fulpmes.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_gaschurn.mp3 b/App/sims/heli/sounds/nav/ort_gaschurn.mp3 new file mode 100644 index 0000000..2ebbfa6 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_gaschurn.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_goetzis.mp3 b/App/sims/heli/sounds/nav/ort_goetzis.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_goetzis.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_hall.mp3 b/App/sims/heli/sounds/nav/ort_hall.mp3 new file mode 100644 index 0000000..9645c38 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_hall.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_hintertux.mp3 b/App/sims/heli/sounds/nav/ort_hintertux.mp3 new file mode 100644 index 0000000..c9088aa Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_hintertux.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_hohenems.mp3 b/App/sims/heli/sounds/nav/ort_hohenems.mp3 new file mode 100644 index 0000000..ae5248a Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_hohenems.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_imst.mp3 b/App/sims/heli/sounds/nav/ort_imst.mp3 new file mode 100644 index 0000000..cc767a3 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_imst.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_innerbraz.mp3 b/App/sims/heli/sounds/nav/ort_innerbraz.mp3 new file mode 100644 index 0000000..1f915af Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_innerbraz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_innsbruck.mp3 b/App/sims/heli/sounds/nav/ort_innsbruck.mp3 new file mode 100644 index 0000000..1677f4c Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_innsbruck.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_jenbach.mp3 b/App/sims/heli/sounds/nav/ort_jenbach.mp3 new file mode 100644 index 0000000..d3cfe88 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_jenbach.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_kals.mp3 b/App/sims/heli/sounds/nav/ort_kals.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_kals.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_kitzbuehel.mp3 b/App/sims/heli/sounds/nav/ort_kitzbuehel.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_kitzbuehel.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_kloesterle.mp3 b/App/sims/heli/sounds/nav/ort_kloesterle.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_kloesterle.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_kufstein.mp3 b/App/sims/heli/sounds/nav/ort_kufstein.mp3 new file mode 100644 index 0000000..c1615df Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_kufstein.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_landeck.mp3 b/App/sims/heli/sounds/nav/ort_landeck.mp3 new file mode 100644 index 0000000..c6371a7 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_landeck.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_langen.mp3 b/App/sims/heli/sounds/nav/ort_langen.mp3 new file mode 100644 index 0000000..a65fd4f Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_langen.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_laterns.mp3 b/App/sims/heli/sounds/nav/ort_laterns.mp3 new file mode 100644 index 0000000..44c9301 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_laterns.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_lech.mp3 b/App/sims/heli/sounds/nav/ort_lech.mp3 new file mode 100644 index 0000000..e6afe1d Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_lech.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_lienz.mp3 b/App/sims/heli/sounds/nav/ort_lienz.mp3 new file mode 100644 index 0000000..7dd95df Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_lienz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_lustenau.mp3 b/App/sims/heli/sounds/nav/ort_lustenau.mp3 new file mode 100644 index 0000000..44dc484 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_lustenau.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_matrei.mp3 b/App/sims/heli/sounds/nav/ort_matrei.mp3 new file mode 100644 index 0000000..335c5bf Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_matrei.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_mayrhofen.mp3 b/App/sims/heli/sounds/nav/ort_mayrhofen.mp3 new file mode 100644 index 0000000..d8c87e7 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_mayrhofen.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_mellau.mp3 b/App/sims/heli/sounds/nav/ort_mellau.mp3 new file mode 100644 index 0000000..fe03dbe Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_mellau.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_mittelberg.mp3 b/App/sims/heli/sounds/nav/ort_mittelberg.mp3 new file mode 100644 index 0000000..a555319 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_mittelberg.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_nenzing.mp3 b/App/sims/heli/sounds/nav/ort_nenzing.mp3 new file mode 100644 index 0000000..ec546ae Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_nenzing.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_neustift.mp3 b/App/sims/heli/sounds/nav/ort_neustift.mp3 new file mode 100644 index 0000000..aea2986 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_neustift.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_obergurgl.mp3 b/App/sims/heli/sounds/nav/ort_obergurgl.mp3 new file mode 100644 index 0000000..4653c4b Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_obergurgl.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_oetz.mp3 b/App/sims/heli/sounds/nav/ort_oetz.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_oetz.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_partenen.mp3 b/App/sims/heli/sounds/nav/ort_partenen.mp3 new file mode 100644 index 0000000..0235b2c Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_partenen.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_rankweil.mp3 b/App/sims/heli/sounds/nav/ort_rankweil.mp3 new file mode 100644 index 0000000..40695c7 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_rankweil.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_reutte.mp3 b/App/sims/heli/sounds/nav/ort_reutte.mp3 new file mode 100644 index 0000000..a30f8d5 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_reutte.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_riezlern.mp3 b/App/sims/heli/sounds/nav/ort_riezlern.mp3 new file mode 100644 index 0000000..8a6232d Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_riezlern.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_scharnitz.mp3 b/App/sims/heli/sounds/nav/ort_scharnitz.mp3 new file mode 100644 index 0000000..b9947b6 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_scharnitz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_schoppernau.mp3 b/App/sims/heli/sounds/nav/ort_schoppernau.mp3 new file mode 100644 index 0000000..cdaddea Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_schoppernau.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_schroecken.mp3 b/App/sims/heli/sounds/nav/ort_schroecken.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_schroecken.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_schruns.mp3 b/App/sims/heli/sounds/nav/ort_schruns.mp3 new file mode 100644 index 0000000..45eca5b Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_schruns.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_schwaz.mp3 b/App/sims/heli/sounds/nav/ort_schwaz.mp3 new file mode 100644 index 0000000..5c5dabf Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_schwaz.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_seefeld.mp3 b/App/sims/heli/sounds/nav/ort_seefeld.mp3 new file mode 100644 index 0000000..f67c4d0 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_seefeld.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_sillian.mp3 b/App/sims/heli/sounds/nav/ort_sillian.mp3 new file mode 100644 index 0000000..1183042 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_sillian.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_silvretta.mp3 b/App/sims/heli/sounds/nav/ort_silvretta.mp3 new file mode 100644 index 0000000..43cec43 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_silvretta.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_soelden.mp3 b/App/sims/heli/sounds/nav/ort_soelden.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_soelden.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_sonntag.mp3 b/App/sims/heli/sounds/nav/ort_sonntag.mp3 new file mode 100644 index 0000000..bee15e8 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_sonntag.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_st_anton.mp3 b/App/sims/heli/sounds/nav/ort_st_anton.mp3 new file mode 100644 index 0000000..771c5f8 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_st_anton.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_st_johann.mp3 b/App/sims/heli/sounds/nav/ort_st_johann.mp3 new file mode 100644 index 0000000..7580c33 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_st_johann.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_steinach.mp3 b/App/sims/heli/sounds/nav/ort_steinach.mp3 new file mode 100644 index 0000000..934e3de Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_steinach.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_stgallenkirch.mp3 b/App/sims/heli/sounds/nav/ort_stgallenkirch.mp3 new file mode 100644 index 0000000..da6f84e Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_stgallenkirch.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_stubaier_gletscher.mp3 b/App/sims/heli/sounds/nav/ort_stubaier_gletscher.mp3 new file mode 100644 index 0000000..b60ceb0 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_stubaier_gletscher.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_stuben.mp3 b/App/sims/heli/sounds/nav/ort_stuben.mp3 new file mode 100644 index 0000000..c5061cb Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_stuben.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_telfs.mp3 b/App/sims/heli/sounds/nav/ort_telfs.mp3 new file mode 100644 index 0000000..53730d3 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_telfs.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_thueringen.mp3 b/App/sims/heli/sounds/nav/ort_thueringen.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_thueringen.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_tschagguns.mp3 b/App/sims/heli/sounds/nav/ort_tschagguns.mp3 new file mode 100644 index 0000000..83dd7dc Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_tschagguns.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_warth.mp3 b/App/sims/heli/sounds/nav/ort_warth.mp3 new file mode 100644 index 0000000..1297afc Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_warth.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_woergl.mp3 b/App/sims/heli/sounds/nav/ort_woergl.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_woergl.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_zell_ziller.mp3 b/App/sims/heli/sounds/nav/ort_zell_ziller.mp3 new file mode 100644 index 0000000..25d7861 Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_zell_ziller.mp3 differ diff --git a/App/sims/heli/sounds/nav/ort_zuers.mp3 b/App/sims/heli/sounds/nav/ort_zuers.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/ort_zuers.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/ort_zwischenwasser.mp3 b/App/sims/heli/sounds/nav/ort_zwischenwasser.mp3 new file mode 100644 index 0000000..b23eeab Binary files /dev/null and b/App/sims/heli/sounds/nav/ort_zwischenwasser.mp3 differ diff --git a/App/sims/heli/sounds/nav/takeover2.mp3 b/App/sims/heli/sounds/nav/takeover2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/takeover2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/nav/takeover3.mp3 b/App/sims/heli/sounds/nav/takeover3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/nav/takeover3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/ouch1.mp3 b/App/sims/heli/sounds/radio/ouch1.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/ouch1.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/ouch1_alloy.mp3 b/App/sims/heli/sounds/radio/ouch1_alloy.mp3 new file mode 100644 index 0000000..130b7a5 Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch1_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch1_echo.mp3 b/App/sims/heli/sounds/radio/ouch1_echo.mp3 new file mode 100644 index 0000000..d87351a Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch1_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch2.mp3 b/App/sims/heli/sounds/radio/ouch2.mp3 new file mode 100644 index 0000000..f18c5c7 Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch2.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch2_alloy.mp3 b/App/sims/heli/sounds/radio/ouch2_alloy.mp3 new file mode 100644 index 0000000..70c6ed5 Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch2_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch2_echo.mp3 b/App/sims/heli/sounds/radio/ouch2_echo.mp3 new file mode 100644 index 0000000..dea88c4 Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch2_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch3.mp3 b/App/sims/heli/sounds/radio/ouch3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/ouch3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/ouch3_alloy.mp3 b/App/sims/heli/sounds/radio/ouch3_alloy.mp3 new file mode 100644 index 0000000..ee9b713 Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch3_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/ouch3_echo.mp3 b/App/sims/heli/sounds/radio/ouch3_echo.mp3 new file mode 100644 index 0000000..bf48a7e Binary files /dev/null and b/App/sims/heli/sounds/radio/ouch3_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_antenna.mp3 b/App/sims/heli/sounds/radio/pilot_antenna.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_antenna.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_calm1.mp3 b/App/sims/heli/sounds/radio/pilot_calm1.mp3 new file mode 100644 index 0000000..f612ba2 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm1.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm1_alloy.mp3 b/App/sims/heli/sounds/radio/pilot_calm1_alloy.mp3 new file mode 100644 index 0000000..0592b53 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm1_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm1_echo.mp3 b/App/sims/heli/sounds/radio/pilot_calm1_echo.mp3 new file mode 100644 index 0000000..dae6887 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm1_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm2.mp3 b/App/sims/heli/sounds/radio/pilot_calm2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_calm2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_calm2_alloy.mp3 b/App/sims/heli/sounds/radio/pilot_calm2_alloy.mp3 new file mode 100644 index 0000000..c381330 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm2_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm2_echo.mp3 b/App/sims/heli/sounds/radio/pilot_calm2_echo.mp3 new file mode 100644 index 0000000..3b4d1c9 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm2_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm3.mp3 b/App/sims/heli/sounds/radio/pilot_calm3.mp3 new file mode 100644 index 0000000..0d08c55 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm3.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm3_alloy.mp3 b/App/sims/heli/sounds/radio/pilot_calm3_alloy.mp3 new file mode 100644 index 0000000..f854f7b Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm3_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_calm3_echo.mp3 b/App/sims/heli/sounds/radio/pilot_calm3_echo.mp3 new file mode 100644 index 0000000..a93746c Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_calm3_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_chimney.mp3 b/App/sims/heli/sounds/radio/pilot_chimney.mp3 new file mode 100644 index 0000000..6e3a9b3 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_chimney.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_crane.mp3 b/App/sims/heli/sounds/radio/pilot_crane.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_crane.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_flock.mp3 b/App/sims/heli/sounds/radio/pilot_flock.mp3 new file mode 100644 index 0000000..724959e Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_flock.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_fog1.mp3 b/App/sims/heli/sounds/radio/pilot_fog1.mp3 new file mode 100644 index 0000000..376ebc2 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_fog1.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_fog2.mp3 b/App/sims/heli/sounds/radio/pilot_fog2.mp3 new file mode 100644 index 0000000..a1b3f2d Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_fog2.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_fog3.mp3 b/App/sims/heli/sounds/radio/pilot_fog3.mp3 new file mode 100644 index 0000000..3345800 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_fog3.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_gap.mp3 b/App/sims/heli/sounds/radio/pilot_gap.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_gap.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_gusts.mp3 b/App/sims/heli/sounds/radio/pilot_gusts.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_gusts.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_narrow.mp3 b/App/sims/heli/sounds/radio/pilot_narrow.mp3 new file mode 100644 index 0000000..c1df4b5 Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_narrow.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_powerline.mp3 b/App/sims/heli/sounds/radio/pilot_powerline.mp3 new file mode 100644 index 0000000..48949ff Binary files /dev/null and b/App/sims/heli/sounds/radio/pilot_powerline.mp3 differ diff --git a/App/sims/heli/sounds/radio/pilot_time.mp3 b/App/sims/heli/sounds/radio/pilot_time.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_time.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_trees.mp3 b/App/sims/heli/sounds/radio/pilot_trees.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_trees.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/pilot_wind.mp3 b/App/sims/heli/sounds/radio/pilot_wind.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/pilot_wind.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/r_land_approach.mp3 b/App/sims/heli/sounds/radio/r_land_approach.mp3 new file mode 100644 index 0000000..d73fbc1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_approach.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_clinic_ahead.mp3 b/App/sims/heli/sounds/radio/r_land_clinic_ahead.mp3 new file mode 100644 index 0000000..a138735 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_clinic_ahead.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_disembark.mp3 b/App/sims/heli/sounds/radio/r_land_disembark.mp3 new file mode 100644 index 0000000..ed475e9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_disembark.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_hook_connected.mp3 b/App/sims/heli/sounds/radio/r_land_hook_connected.mp3 new file mode 100644 index 0000000..8fee5f6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_hook_connected.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_lift_success.mp3 b/App/sims/heli/sounds/radio/r_land_lift_success.mp3 new file mode 100644 index 0000000..5a1c5e8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_lift_success.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_rescuer_down.mp3 b/App/sims/heli/sounds/radio/r_land_rescuer_down.mp3 new file mode 100644 index 0000000..da41206 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_rescuer_down.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_rescuer_ready.mp3 b/App/sims/heli/sounds/radio/r_land_rescuer_ready.mp3 new file mode 100644 index 0000000..226011d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_rescuer_ready.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_land_winch_down.mp3 b/App/sims/heli/sounds/radio/r_land_winch_down.mp3 new file mode 100644 index 0000000..4e45360 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_land_winch_down.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_altitude_mountain.mp3 b/App/sims/heli/sounds/radio/r_nav_altitude_mountain.mp3 new file mode 100644 index 0000000..314f050 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_altitude_mountain.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_altitude_valley.mp3 b/App/sims/heli/sounds/radio/r_nav_altitude_valley.mp3 new file mode 100644 index 0000000..0521aad Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_altitude_valley.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_departure_ok.mp3 b/App/sims/heli/sounds/radio/r_nav_departure_ok.mp3 new file mode 100644 index 0000000..f9902ad Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_departure_ok.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_departure_takeover.mp3 b/App/sims/heli/sounds/radio/r_nav_departure_takeover.mp3 new file mode 100644 index 0000000..6fc3c4a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_departure_takeover.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_fuel_half.mp3 b/App/sims/heli/sounds/radio/r_nav_fuel_half.mp3 new file mode 100644 index 0000000..c5269ca Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_fuel_half.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_fuel_quarter.mp3 b/App/sims/heli/sounds/radio/r_nav_fuel_quarter.mp3 new file mode 100644 index 0000000..3ef3e47 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_fuel_quarter.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_ara.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_ara.mp3 new file mode 100644 index 0000000..6171b20 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c1.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c1.mp3 new file mode 100644 index 0000000..5cd587e Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c10.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c10.mp3 new file mode 100644 index 0000000..4eaa865 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c11.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c11.mp3 new file mode 100644 index 0000000..64b0280 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c12.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c12.mp3 new file mode 100644 index 0000000..3af65c4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c14.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c14.mp3 new file mode 100644 index 0000000..d541f28 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c16.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c16.mp3 new file mode 100644 index 0000000..de75f6e Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c2.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c2.mp3 new file mode 100644 index 0000000..915e7f2 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c3.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c3.mp3 new file mode 100644 index 0000000..4ff6090 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c4.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c4.mp3 new file mode 100644 index 0000000..8eee500 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c5.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c5.mp3 new file mode 100644 index 0000000..1d164c6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c6.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c6.mp3 new file mode 100644 index 0000000..c31adbb Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c7.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c7.mp3 new file mode 100644 index 0000000..039d066 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c8.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c8.mp3 new file mode 100644 index 0000000..2759a9a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_heli_intro_c9.mp3 b/App/sims/heli/sounds/radio/r_nav_heli_intro_c9.mp3 new file mode 100644 index 0000000..e6aab30 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_heli_intro_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_plan_correct.mp3 b/App/sims/heli/sounds/radio/r_nav_plan_correct.mp3 new file mode 100644 index 0000000..3913586 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_plan_correct.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_plan_done.mp3 b/App/sims/heli/sounds/radio/r_nav_plan_done.mp3 new file mode 100644 index 0000000..5420f14 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_plan_done.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_plan_start.mp3 b/App/sims/heli/sounds/radio/r_nav_plan_start.mp3 new file mode 100644 index 0000000..bacec95 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_plan_start.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_plan_wrong_far.mp3 b/App/sims/heli/sounds/radio/r_nav_plan_wrong_far.mp3 new file mode 100644 index 0000000..e28156b Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_plan_wrong_far.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_plan_wrong_near.mp3 b/App/sims/heli/sounds/radio/r_nav_plan_wrong_near.mp3 new file mode 100644 index 0000000..70501c4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_plan_wrong_near.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_route_check.mp3 b/App/sims/heli/sounds/radio/r_nav_route_check.mp3 new file mode 100644 index 0000000..fecea37 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_route_check.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_seg_arrive.mp3 b/App/sims/heli/sounds/radio/r_nav_seg_arrive.mp3 new file mode 100644 index 0000000..6f187d6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_seg_arrive.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_seg_start.mp3 b/App/sims/heli/sounds/radio/r_nav_seg_start.mp3 new file mode 100644 index 0000000..714f4ba Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_seg_start.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_weather_enter.mp3 b/App/sims/heli/sounds/radio/r_nav_weather_enter.mp3 new file mode 100644 index 0000000..4a790ec Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_weather_enter.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_nav_weather_exit.mp3 b/App/sims/heli/sounds/radio/r_nav_weather_exit.mp3 new file mode 100644 index 0000000..1f9519d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_nav_weather_exit.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_bird_ahead.mp3 b/App/sims/heli/sounds/radio/r_pilot_bird_ahead.mp3 new file mode 100644 index 0000000..443b7a6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_bird_ahead.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_ara.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_ara.mp3 new file mode 100644 index 0000000..e0ca6bb Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c1.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c1.mp3 new file mode 100644 index 0000000..b3a954d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c10.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c10.mp3 new file mode 100644 index 0000000..7f90f9d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c11.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c11.mp3 new file mode 100644 index 0000000..f8ed6fa Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c12.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c12.mp3 new file mode 100644 index 0000000..52695cd Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c14.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c14.mp3 new file mode 100644 index 0000000..1a86ddd Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c16.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c16.mp3 new file mode 100644 index 0000000..1bfbe1b Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c2.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c2.mp3 new file mode 100644 index 0000000..8d3b0db Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c3.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c3.mp3 new file mode 100644 index 0000000..8fa53fe Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c4.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c4.mp3 new file mode 100644 index 0000000..ff42aa4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c5.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c5.mp3 new file mode 100644 index 0000000..16482ba Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c6.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c6.mp3 new file mode 100644 index 0000000..e836b42 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c7.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c7.mp3 new file mode 100644 index 0000000..893f09c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c8.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c8.mp3 new file mode 100644 index 0000000..39ff0d9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_call_c9.mp3 b/App/sims/heli/sounds/radio/r_pilot_call_c9.mp3 new file mode 100644 index 0000000..06ab11c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_call_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_fog_thickens.mp3 b/App/sims/heli/sounds/radio/r_pilot_fog_thickens.mp3 new file mode 100644 index 0000000..ecb1e59 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_fog_thickens.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_plane_ahead.mp3 b/App/sims/heli/sounds/radio/r_pilot_plane_ahead.mp3 new file mode 100644 index 0000000..ebb4aa1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_plane_ahead.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_sportplane_level.mp3 b/App/sims/heli/sounds/radio/r_pilot_sportplane_level.mp3 new file mode 100644 index 0000000..2af461c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_sportplane_level.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_storm_detour.mp3 b/App/sims/heli/sounds/radio/r_pilot_storm_detour.mp3 new file mode 100644 index 0000000..e8f0cbb Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_storm_detour.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_strike_1.mp3 b/App/sims/heli/sounds/radio/r_pilot_strike_1.mp3 new file mode 100644 index 0000000..32c0b62 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_strike_1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_strike_2.mp3 b/App/sims/heli/sounds/radio/r_pilot_strike_2.mp3 new file mode 100644 index 0000000..f9266a6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_strike_2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_strike_3.mp3 b/App/sims/heli/sounds/radio/r_pilot_strike_3.mp3 new file mode 100644 index 0000000..2a459c4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_strike_3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_strike_4.mp3 b/App/sims/heli/sounds/radio/r_pilot_strike_4.mp3 new file mode 100644 index 0000000..54dd0d9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_strike_4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_strike_5.mp3 b/App/sims/heli/sounds/radio/r_pilot_strike_5.mp3 new file mode 100644 index 0000000..6d0a9ac Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_strike_5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_three_strikes.mp3 b/App/sims/heli/sounds/radio/r_pilot_three_strikes.mp3 new file mode 100644 index 0000000..edd8d51 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_three_strikes.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_time_up.mp3 b/App/sims/heli/sounds/radio/r_pilot_time_up.mp3 new file mode 100644 index 0000000..aae91a7 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_time_up.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_too_high.mp3 b/App/sims/heli/sounds/radio/r_pilot_too_high.mp3 new file mode 100644 index 0000000..2315bf6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_too_high.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_pilot_wind_turn.mp3 b/App/sims/heli/sounds/radio/r_pilot_wind_turn.mp3 new file mode 100644 index 0000000..6531aa8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_pilot_wind_turn.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_ara.mp3 new file mode 100644 index 0000000..559868f Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c1.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c1.mp3 new file mode 100644 index 0000000..e9c31f0 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c10.mp3 new file mode 100644 index 0000000..bd4da8a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c11.mp3 new file mode 100644 index 0000000..a772a19 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c12.mp3 new file mode 100644 index 0000000..3ecbe05 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c14.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c14.mp3 new file mode 100644 index 0000000..c413698 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c16.mp3 new file mode 100644 index 0000000..07e7337 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c2.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c2.mp3 new file mode 100644 index 0000000..35e83e2 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c3.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c3.mp3 new file mode 100644 index 0000000..60b9bea Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c4.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c4.mp3 new file mode 100644 index 0000000..18414b3 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c5.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c5.mp3 new file mode 100644 index 0000000..2ab719d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c6.mp3 new file mode 100644 index 0000000..902b583 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c7.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c7.mp3 new file mode 100644 index 0000000..6cf37d0 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c8.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c8.mp3 new file mode 100644 index 0000000..b48687a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_approach_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_approach_c9.mp3 new file mode 100644 index 0000000..3e92ab8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_approach_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_ara.mp3 new file mode 100644 index 0000000..ccfdaaf Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c1.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c1.mp3 new file mode 100644 index 0000000..8c57a8d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c10.mp3 new file mode 100644 index 0000000..08d4c57 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c11.mp3 new file mode 100644 index 0000000..021ebff Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c12.mp3 new file mode 100644 index 0000000..770f52a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c14.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c14.mp3 new file mode 100644 index 0000000..072c2bf Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c16.mp3 new file mode 100644 index 0000000..6e4f7b6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c2.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c2.mp3 new file mode 100644 index 0000000..28c0611 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c3.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c3.mp3 new file mode 100644 index 0000000..c4a16bd Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c4.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c4.mp3 new file mode 100644 index 0000000..efe1cc4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c5.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c5.mp3 new file mode 100644 index 0000000..ab719a3 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c6.mp3 new file mode 100644 index 0000000..13646ef Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c7.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c7.mp3 new file mode 100644 index 0000000..3c4b275 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c8.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c8.mp3 new file mode 100644 index 0000000..27a09f8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_extract_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_extract_c9.mp3 new file mode 100644 index 0000000..8ba0541 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_extract_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m1.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m1.mp3 new file mode 100644 index 0000000..63e60d3 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m10.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m10.mp3 new file mode 100644 index 0000000..db9f9f1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m11.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m11.mp3 new file mode 100644 index 0000000..c66ce87 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m12.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m12.mp3 new file mode 100644 index 0000000..5774c3d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m13.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m13.mp3 new file mode 100644 index 0000000..6c1d75f Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m13.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m14.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m14.mp3 new file mode 100644 index 0000000..2b862d1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m15.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m15.mp3 new file mode 100644 index 0000000..1b56204 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m15.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m16.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m16.mp3 new file mode 100644 index 0000000..28849f5 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m17.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m17.mp3 new file mode 100644 index 0000000..e44f604 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m17.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m18.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m18.mp3 new file mode 100644 index 0000000..250ae8c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m18.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m19.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m19.mp3 new file mode 100644 index 0000000..c28e694 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m19.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m2.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m2.mp3 new file mode 100644 index 0000000..669e236 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m20.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m20.mp3 new file mode 100644 index 0000000..fc5b5d2 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m20.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m21.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m21.mp3 new file mode 100644 index 0000000..2bd2af1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m21.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m22.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m22.mp3 new file mode 100644 index 0000000..4e9c50b Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m22.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m23.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m23.mp3 new file mode 100644 index 0000000..bc0f570 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m23.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m24.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m24.mp3 new file mode 100644 index 0000000..e268eb5 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m24.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m25.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m25.mp3 new file mode 100644 index 0000000..f0506b9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m25.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m3.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m3.mp3 new file mode 100644 index 0000000..e2e8c2e Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m4.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m4.mp3 new file mode 100644 index 0000000..5f72495 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m5.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m5.mp3 new file mode 100644 index 0000000..14c77de Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m6.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m6.mp3 new file mode 100644 index 0000000..caa5171 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m7.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m7.mp3 new file mode 100644 index 0000000..7449121 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m8.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m8.mp3 new file mode 100644 index 0000000..3d0ae6a Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_geo_m9.mp3 b/App/sims/heli/sounds/radio/r_tower_geo_m9.mp3 new file mode 100644 index 0000000..644da86 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_geo_m9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_ara.mp3 new file mode 100644 index 0000000..0105b71 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c1.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c1.mp3 new file mode 100644 index 0000000..fb87b1c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c10.mp3 new file mode 100644 index 0000000..5f5bb60 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c11.mp3 new file mode 100644 index 0000000..4f162b6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c12.mp3 new file mode 100644 index 0000000..d60d8f5 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c14.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c14.mp3 new file mode 100644 index 0000000..8af5544 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c16.mp3 new file mode 100644 index 0000000..8f2b892 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c2.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c2.mp3 new file mode 100644 index 0000000..927a389 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c3.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c3.mp3 new file mode 100644 index 0000000..3dbd042 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c4.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c4.mp3 new file mode 100644 index 0000000..59f1be9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c5.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c5.mp3 new file mode 100644 index 0000000..dc4f8a7 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c6.mp3 new file mode 100644 index 0000000..fdc24ad Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c7.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c7.mp3 new file mode 100644 index 0000000..01d5538 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c8.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c8.mp3 new file mode 100644 index 0000000..241c7ef Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_handover_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_handover_c9.mp3 new file mode 100644 index 0000000..4a9f6be Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_handover_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_ara.mp3 new file mode 100644 index 0000000..77034c9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_c10.mp3 new file mode 100644 index 0000000..adcab4d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_c11.mp3 new file mode 100644 index 0000000..2a8f1c1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_c12.mp3 new file mode 100644 index 0000000..dcc5927 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_c6.mp3 new file mode 100644 index 0000000..5d5dd61 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_approach_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_approach_c9.mp3 new file mode 100644 index 0000000..927c16d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_approach_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_ara.mp3 new file mode 100644 index 0000000..a6c608e Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_c10.mp3 new file mode 100644 index 0000000..23a20d4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_c11.mp3 new file mode 100644 index 0000000..829a65d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_c12.mp3 new file mode 100644 index 0000000..07a106c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_c6.mp3 new file mode 100644 index 0000000..def3e65 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_hw_extract_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_hw_extract_c9.mp3 new file mode 100644 index 0000000..ef4b8a1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_hw_extract_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_antenna.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_antenna.mp3 new file mode 100644 index 0000000..0c94ce2 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_antenna.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_autorot.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_autorot.mp3 new file mode 100644 index 0000000..3375b88 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_autorot.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_banking.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_banking.mp3 new file mode 100644 index 0000000..ecf00d4 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_banking.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_cable.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_cable.mp3 new file mode 100644 index 0000000..8590718 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_cable.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_chimney.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_chimney.mp3 new file mode 100644 index 0000000..a62c9a8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_chimney.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_crane.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_crane.mp3 new file mode 100644 index 0000000..c60ffaa Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_crane.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_crows.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_crows.mp3 new file mode 100644 index 0000000..30d28ea Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_crows.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_density.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_density.mp3 new file mode 100644 index 0000000..c89b494 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_density.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_downwash.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_downwash.mp3 new file mode 100644 index 0000000..3ae7a61 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_downwash.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_foehn.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_foehn.mp3 new file mode 100644 index 0000000..ee84ffe Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_foehn.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_fog.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_fog.mp3 new file mode 100644 index 0000000..e30feca Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_fog.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_gulls.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_gulls.mp3 new file mode 100644 index 0000000..e81ea1d Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_gulls.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_hover.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_hover.mp3 new file mode 100644 index 0000000..005a7f1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_hover.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_pilot.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_pilot.mp3 new file mode 100644 index 0000000..25deac1 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_pilot.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_powerline.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_powerline.mp3 new file mode 100644 index 0000000..aaac564 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_powerline.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_temperature.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_temperature.mp3 new file mode 100644 index 0000000..97a4d67 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_temperature.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_thermic.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_thermic.mp3 new file mode 100644 index 0000000..e9d9e34 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_thermic.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_torque.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_torque.mp3 new file mode 100644 index 0000000..29678ff Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_torque.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_vortex.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_vortex.mp3 new file mode 100644 index 0000000..4d33287 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_vortex.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_lesson_wind.mp3 b/App/sims/heli/sounds/radio/r_tower_lesson_wind.mp3 new file mode 100644 index 0000000..482a3d9 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_lesson_wind.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_ara.mp3 new file mode 100644 index 0000000..d7e61c8 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_c10.mp3 new file mode 100644 index 0000000..632fd9c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_c11.mp3 new file mode 100644 index 0000000..382fd03 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_c16.mp3 new file mode 100644 index 0000000..5660e3c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_c6.mp3 new file mode 100644 index 0000000..e8e6f7b Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_approach_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_approach_c9.mp3 new file mode 100644 index 0000000..b35c8cc Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_approach_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_ara.mp3 new file mode 100644 index 0000000..2a91b13 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_c10.mp3 new file mode 100644 index 0000000..ffd3e53 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_c11.mp3 new file mode 100644 index 0000000..5adbefc Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_c16.mp3 new file mode 100644 index 0000000..fbcab65 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_c6.mp3 new file mode 100644 index 0000000..68f124b Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_sea_extract_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_sea_extract_c9.mp3 new file mode 100644 index 0000000..b4c71b5 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_sea_extract_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_start_ara.mp3 new file mode 100644 index 0000000..694f652 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c1.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c1.mp3 new file mode 100644 index 0000000..eb00403 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c10.mp3 new file mode 100644 index 0000000..a048327 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c11.mp3 new file mode 100644 index 0000000..9d37d92 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c12.mp3 new file mode 100644 index 0000000..5866fe5 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c14.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c14.mp3 new file mode 100644 index 0000000..06f841f Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c16.mp3 new file mode 100644 index 0000000..37a2860 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c2.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c2.mp3 new file mode 100644 index 0000000..734f089 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c3.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c3.mp3 new file mode 100644 index 0000000..e6d617c Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c4.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c4.mp3 new file mode 100644 index 0000000..076c40e Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c5.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c5.mp3 new file mode 100644 index 0000000..3c3e371 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c6.mp3 new file mode 100644 index 0000000..946d6e6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c7.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c7.mp3 new file mode 100644 index 0000000..8e611ed Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c8.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c8.mp3 new file mode 100644 index 0000000..1a3f1e2 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_start_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_start_c9.mp3 new file mode 100644 index 0000000..9092da0 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_start_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_ara.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_ara.mp3 new file mode 100644 index 0000000..c30f223 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_ara.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c1.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c1.mp3 new file mode 100644 index 0000000..c5ff4b6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c1.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c10.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c10.mp3 new file mode 100644 index 0000000..a469080 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c10.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c11.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c11.mp3 new file mode 100644 index 0000000..3ebbb52 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c11.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c12.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c12.mp3 new file mode 100644 index 0000000..617ef31 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c12.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c14.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c14.mp3 new file mode 100644 index 0000000..29a47ee Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c14.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c16.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c16.mp3 new file mode 100644 index 0000000..3dcedf0 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c16.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c2.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c2.mp3 new file mode 100644 index 0000000..2bb9704 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c2.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c3.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c3.mp3 new file mode 100644 index 0000000..3ce37d6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c3.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c4.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c4.mp3 new file mode 100644 index 0000000..25c16e6 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c4.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c5.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c5.mp3 new file mode 100644 index 0000000..2c5eade Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c5.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c6.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c6.mp3 new file mode 100644 index 0000000..ccf77be Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c6.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c7.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c7.mp3 new file mode 100644 index 0000000..ca2cf79 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c7.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c8.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c8.mp3 new file mode 100644 index 0000000..6449467 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c8.mp3 differ diff --git a/App/sims/heli/sounds/radio/r_tower_strike_c9.mp3 b/App/sims/heli/sounds/radio/r_tower_strike_c9.mp3 new file mode 100644 index 0000000..8142092 Binary files /dev/null and b/App/sims/heli/sounds/radio/r_tower_strike_c9.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike1.mp3 b/App/sims/heli/sounds/radio/strike1.mp3 new file mode 100644 index 0000000..d312153 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike1.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike1_alloy.mp3 b/App/sims/heli/sounds/radio/strike1_alloy.mp3 new file mode 100644 index 0000000..131c2c6 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike1_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike1_echo.mp3 b/App/sims/heli/sounds/radio/strike1_echo.mp3 new file mode 100644 index 0000000..b15606f Binary files /dev/null and b/App/sims/heli/sounds/radio/strike1_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike2.mp3 b/App/sims/heli/sounds/radio/strike2.mp3 new file mode 100644 index 0000000..19795de Binary files /dev/null and b/App/sims/heli/sounds/radio/strike2.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike2_alloy.mp3 b/App/sims/heli/sounds/radio/strike2_alloy.mp3 new file mode 100644 index 0000000..d23593e Binary files /dev/null and b/App/sims/heli/sounds/radio/strike2_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike2_echo.mp3 b/App/sims/heli/sounds/radio/strike2_echo.mp3 new file mode 100644 index 0000000..9caf4d1 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike2_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike3.mp3 b/App/sims/heli/sounds/radio/strike3.mp3 new file mode 100644 index 0000000..417e67d Binary files /dev/null and b/App/sims/heli/sounds/radio/strike3.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike3_alloy.mp3 b/App/sims/heli/sounds/radio/strike3_alloy.mp3 new file mode 100644 index 0000000..36e4cb5 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike3_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike3_echo.mp3 b/App/sims/heli/sounds/radio/strike3_echo.mp3 new file mode 100644 index 0000000..feaf459 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike3_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike4.mp3 b/App/sims/heli/sounds/radio/strike4.mp3 new file mode 100644 index 0000000..773b43f Binary files /dev/null and b/App/sims/heli/sounds/radio/strike4.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike4_alloy.mp3 b/App/sims/heli/sounds/radio/strike4_alloy.mp3 new file mode 100644 index 0000000..3443c29 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike4_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike4_echo.mp3 b/App/sims/heli/sounds/radio/strike4_echo.mp3 new file mode 100644 index 0000000..7dd2f5d Binary files /dev/null and b/App/sims/heli/sounds/radio/strike4_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike5.mp3 b/App/sims/heli/sounds/radio/strike5.mp3 new file mode 100644 index 0000000..4f74268 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike5.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike5_alloy.mp3 b/App/sims/heli/sounds/radio/strike5_alloy.mp3 new file mode 100644 index 0000000..1243d1e Binary files /dev/null and b/App/sims/heli/sounds/radio/strike5_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/strike5_echo.mp3 b/App/sims/heli/sounds/radio/strike5_echo.mp3 new file mode 100644 index 0000000..f4e44c3 Binary files /dev/null and b/App/sims/heli/sounds/radio/strike5_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/success.mp3 b/App/sims/heli/sounds/radio/success.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/success.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/success2.mp3 b/App/sims/heli/sounds/radio/success2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/success2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/success2_alloy.mp3 b/App/sims/heli/sounds/radio/success2_alloy.mp3 new file mode 100644 index 0000000..68749b5 Binary files /dev/null and b/App/sims/heli/sounds/radio/success2_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/success2_echo.mp3 b/App/sims/heli/sounds/radio/success2_echo.mp3 new file mode 100644 index 0000000..a908b99 Binary files /dev/null and b/App/sims/heli/sounds/radio/success2_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover.mp3 b/App/sims/heli/sounds/radio/takeover.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/takeover.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/takeover2_alloy.mp3 b/App/sims/heli/sounds/radio/takeover2_alloy.mp3 new file mode 100644 index 0000000..b5cc24a Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover2_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover2_echo.mp3 b/App/sims/heli/sounds/radio/takeover2_echo.mp3 new file mode 100644 index 0000000..71042b8 Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover2_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover4.mp3 b/App/sims/heli/sounds/radio/takeover4.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/takeover4.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/takeover4_alloy.mp3 b/App/sims/heli/sounds/radio/takeover4_alloy.mp3 new file mode 100644 index 0000000..2b47bc7 Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover4_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover4_echo.mp3 b/App/sims/heli/sounds/radio/takeover4_echo.mp3 new file mode 100644 index 0000000..1857b4a Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover4_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover5.mp3 b/App/sims/heli/sounds/radio/takeover5.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/takeover5.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/takeover_alloy.mp3 b/App/sims/heli/sounds/radio/takeover_alloy.mp3 new file mode 100644 index 0000000..5521c48 Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/takeover_echo.mp3 b/App/sims/heli/sounds/radio/takeover_echo.mp3 new file mode 100644 index 0000000..c134547 Binary files /dev/null and b/App/sims/heli/sounds/radio/takeover_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/tower1.mp3 b/App/sims/heli/sounds/radio/tower1.mp3 new file mode 100644 index 0000000..3ce8f19 Binary files /dev/null and b/App/sims/heli/sounds/radio/tower1.mp3 differ diff --git a/App/sims/heli/sounds/radio/tower2.mp3 b/App/sims/heli/sounds/radio/tower2.mp3 new file mode 100644 index 0000000..d001505 Binary files /dev/null and b/App/sims/heli/sounds/radio/tower2.mp3 differ diff --git a/App/sims/heli/sounds/radio/tower3.mp3 b/App/sims/heli/sounds/radio/tower3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/tower3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/tower4.mp3 b/App/sims/heli/sounds/radio/tower4.mp3 new file mode 100644 index 0000000..ba90285 Binary files /dev/null and b/App/sims/heli/sounds/radio/tower4.mp3 differ diff --git a/App/sims/heli/sounds/radio/traffic1.mp3 b/App/sims/heli/sounds/radio/traffic1.mp3 new file mode 100644 index 0000000..293891e Binary files /dev/null and b/App/sims/heli/sounds/radio/traffic1.mp3 differ diff --git a/App/sims/heli/sounds/radio/traffic2.mp3 b/App/sims/heli/sounds/radio/traffic2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/traffic2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/traffic3.mp3 b/App/sims/heli/sounds/radio/traffic3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/traffic3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/traffic4.mp3 b/App/sims/heli/sounds/radio/traffic4.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/traffic4.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/voice_alloy.mp3 b/App/sims/heli/sounds/radio/voice_alloy.mp3 new file mode 100644 index 0000000..2213e4d Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_alloy.mp3 differ diff --git a/App/sims/heli/sounds/radio/voice_echo.mp3 b/App/sims/heli/sounds/radio/voice_echo.mp3 new file mode 100644 index 0000000..1b15d83 Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_echo.mp3 differ diff --git a/App/sims/heli/sounds/radio/voice_fable.mp3 b/App/sims/heli/sounds/radio/voice_fable.mp3 new file mode 100644 index 0000000..63d1d28 Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_fable.mp3 differ diff --git a/App/sims/heli/sounds/radio/voice_nova.mp3 b/App/sims/heli/sounds/radio/voice_nova.mp3 new file mode 100644 index 0000000..9d15eac Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_nova.mp3 differ diff --git a/App/sims/heli/sounds/radio/voice_onyx.mp3 b/App/sims/heli/sounds/radio/voice_onyx.mp3 new file mode 100644 index 0000000..f0d4dd0 Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_onyx.mp3 differ diff --git a/App/sims/heli/sounds/radio/voice_shimmer.mp3 b/App/sims/heli/sounds/radio/voice_shimmer.mp3 new file mode 100644 index 0000000..e39b274 Binary files /dev/null and b/App/sims/heli/sounds/radio/voice_shimmer.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_antenne.mp3 b/App/sims/heli/sounds/radio/warn_antenne.mp3 new file mode 100644 index 0000000..c7fcb9a Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_antenne.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_antenne2.mp3 b/App/sims/heli/sounds/radio/warn_antenne2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_antenne2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_antenne3.mp3 b/App/sims/heli/sounds/radio/warn_antenne3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_antenne3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_baum.mp3 b/App/sims/heli/sounds/radio/warn_baum.mp3 new file mode 100644 index 0000000..ac78409 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_baum.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_baum2.mp3 b/App/sims/heli/sounds/radio/warn_baum2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_baum2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_baum3.mp3 b/App/sims/heli/sounds/radio/warn_baum3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_baum3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_blitz.mp3 b/App/sims/heli/sounds/radio/warn_blitz.mp3 new file mode 100644 index 0000000..5bc2641 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_blitz.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_blitz2.mp3 b/App/sims/heli/sounds/radio/warn_blitz2.mp3 new file mode 100644 index 0000000..c2e80c6 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_blitz2.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_blitz3.mp3 b/App/sims/heli/sounds/radio/warn_blitz3.mp3 new file mode 100644 index 0000000..46b1785 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_blitz3.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_fabrik.mp3 b/App/sims/heli/sounds/radio/warn_fabrik.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_fabrik.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_fabrik2.mp3 b/App/sims/heli/sounds/radio/warn_fabrik2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_fabrik2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_fabrik3.mp3 b/App/sims/heli/sounds/radio/warn_fabrik3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_fabrik3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_flugzeug.mp3 b/App/sims/heli/sounds/radio/warn_flugzeug.mp3 new file mode 100644 index 0000000..d659752 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_flugzeug.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_flugzeug2.mp3 b/App/sims/heli/sounds/radio/warn_flugzeug2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_flugzeug2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_flugzeug3.mp3 b/App/sims/heli/sounds/radio/warn_flugzeug3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_flugzeug3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_fuel.mp3 b/App/sims/heli/sounds/radio/warn_fuel.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_fuel.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_fuel2.mp3 b/App/sims/heli/sounds/radio/warn_fuel2.mp3 new file mode 100644 index 0000000..9a78882 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_fuel2.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_gewitter.mp3 b/App/sims/heli/sounds/radio/warn_gewitter.mp3 new file mode 100644 index 0000000..0ea06e7 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_gewitter.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_haus.mp3 b/App/sims/heli/sounds/radio/warn_haus.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_haus.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_haus2.mp3 b/App/sims/heli/sounds/radio/warn_haus2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_haus2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_haus3.mp3 b/App/sims/heli/sounds/radio/warn_haus3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_haus3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_klima.mp3 b/App/sims/heli/sounds/radio/warn_klima.mp3 new file mode 100644 index 0000000..2b63430 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_klima.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_klima2.mp3 b/App/sims/heli/sounds/radio/warn_klima2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_klima2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_kran.mp3 b/App/sims/heli/sounds/radio/warn_kran.mp3 new file mode 100644 index 0000000..7ad6807 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_kran.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_kran2.mp3 b/App/sims/heli/sounds/radio/warn_kran2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_kran2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_kran3.mp3 b/App/sims/heli/sounds/radio/warn_kran3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_kran3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_nebel.mp3 b/App/sims/heli/sounds/radio/warn_nebel.mp3 new file mode 100644 index 0000000..c05483f Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_nebel.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_nebel2.mp3 b/App/sims/heli/sounds/radio/warn_nebel2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_nebel2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_nebel3.mp3 b/App/sims/heli/sounds/radio/warn_nebel3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_nebel3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_schornstein.mp3 b/App/sims/heli/sounds/radio/warn_schornstein.mp3 new file mode 100644 index 0000000..3445194 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_schornstein.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_schornstein2.mp3 b/App/sims/heli/sounds/radio/warn_schornstein2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_schornstein2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_sport.mp3 b/App/sims/heli/sounds/radio/warn_sport.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_sport.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_sport2.mp3 b/App/sims/heli/sounds/radio/warn_sport2.mp3 new file mode 100644 index 0000000..c3f70d5 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_sport2.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_sport3.mp3 b/App/sims/heli/sounds/radio/warn_sport3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_sport3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_strom.mp3 b/App/sims/heli/sounds/radio/warn_strom.mp3 new file mode 100644 index 0000000..055a0a5 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_strom.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_strom2.mp3 b/App/sims/heli/sounds/radio/warn_strom2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_strom2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_strom3.mp3 b/App/sims/heli/sounds/radio/warn_strom3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_strom3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_vogel.mp3 b/App/sims/heli/sounds/radio/warn_vogel.mp3 new file mode 100644 index 0000000..a49ac42 Binary files /dev/null and b/App/sims/heli/sounds/radio/warn_vogel.mp3 differ diff --git a/App/sims/heli/sounds/radio/warn_vogel2.mp3 b/App/sims/heli/sounds/radio/warn_vogel2.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_vogel2.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio/warn_vogel3.mp3 b/App/sims/heli/sounds/radio/warn_vogel3.mp3 new file mode 100644 index 0000000..0bc62ed --- /dev/null +++ b/App/sims/heli/sounds/radio/warn_vogel3.mp3 @@ -0,0 +1,8 @@ +{ + "error": { + "message": "400: There was an error parsing the body", + "type": "server_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/App/sims/heli/sounds/radio1.mp3 b/App/sims/heli/sounds/radio1.mp3 new file mode 100644 index 0000000..dc455a6 Binary files /dev/null and b/App/sims/heli/sounds/radio1.mp3 differ diff --git a/App/sims/heli/sounds/radio3.mp3 b/App/sims/heli/sounds/radio3.mp3 new file mode 100644 index 0000000..8b8bce5 Binary files /dev/null and b/App/sims/heli/sounds/radio3.mp3 differ diff --git a/App/sims/heli/sounds/radio4.mp3 b/App/sims/heli/sounds/radio4.mp3 new file mode 100644 index 0000000..e060b5f Binary files /dev/null and b/App/sims/heli/sounds/radio4.mp3 differ diff --git a/App/sims/heli/sounds/smallPlane.mp3 b/App/sims/heli/sounds/smallPlane.mp3 new file mode 100644 index 0000000..3c961ff Binary files /dev/null and b/App/sims/heli/sounds/smallPlane.mp3 differ diff --git a/App/sims/heli/sounds/thunder.mp3 b/App/sims/heli/sounds/thunder.mp3 new file mode 100644 index 0000000..4f5f281 Binary files /dev/null and b/App/sims/heli/sounds/thunder.mp3 differ diff --git a/App/sims/heli/sounds/warning.mp3 b/App/sims/heli/sounds/warning.mp3 new file mode 100644 index 0000000..d76d553 Binary files /dev/null and b/App/sims/heli/sounds/warning.mp3 differ diff --git a/App/sims/heli/start.html b/App/sims/heli/start.html new file mode 100644 index 0000000..b7fc25b --- /dev/null +++ b/App/sims/heli/start.html @@ -0,0 +1,3194 @@ + + + + +Helikopter-Simulator · Einsatzvarianten & Szenarien + + + + +
+
+
+

🚁 Helikopter-Simulator

+
Co-Pilot im Training · Ziel: Raus aus der Startzone
+
+ Lektion · Start +
+ +
+ + +
00:00
+ +
+
+
Höhe
0 m
+
Sinken
0 m/s
+
Speed
0 km/h
+
Treibst.
100 %
+
Verbrauch
— l/min
+
G-Kraft
1.0 g
+
Berührungen: 💥💥💥 0 / 3
+ +
+
+
+
+
+ + + + + + + + Wind 0 kn +
+
Ziel Raus aus Startzone
+
+
🔊
+
+
+ +
Ziel: Wähle Einsatzvariante & Szenario.
+ + + + +
+
+ Einsatz wählen +

Bereit für den Einsatz?

+

Du bist ein junger Co-Pilot auf deinen ersten Einsätzen. Der Chefpilot lässt dich den Start fliegen — das ist der heikelste Teil. Ziel: raus aus der Startumgebung. Dann übernimmt der Pilot die Flugnavigation.

Bei drei Berührungen greift der Chefpilot ein.

+ +
+
Einsatzvariante
+
+
+ +
+
Szenario
+
+
+
+ + + +
+
+ 📚 Warum nicht einfach senkrecht starten? + +
+
+

Ein Hubschrauber kann zwar in der Luft stehen wie eine Kerze. Trotzdem startet er fast nie senkrecht. Warum?

+ +
+ + + + + Geschwindigkeit → + Höhe → + + Gefahren- + zone + + + + + + + langsam + schnell + +
+

Die "Todeskurve" (H/V-Diagramm)

+

Wenn der Motor in z. B. 50 m Höhe ausfällt und der Heli senkrecht steht, gibt es kein Zurück: Er fällt. Mit Vorwärtsfahrt kann der Rotor wie ein Windrad weiterdrehen (Autorotation) und der Pilot landet sanft.

+
+
+ +

Geschwindigkeit ist Sicherheit. Deshalb fliegt man bei Rettungshubschraubern auf Krankenhausdächern ein Kategorie-A-Profil: schnell schräg nach vorne weg, dabei Geschwindigkeit aufbauen — auch wenn man dafür zuerst über die Dachkante hinaus muss.

+ +

Bonus: Krankenhausdächer in der Stadt sind besonders heikel — kaum Notlandeflächen rundherum, Turbulenzen durch Häuser. Deshalb plant der Pilot eine bestimmte Abflugrichtung mit Notlandeoptionen entlang der Flugbahn (Park, Fluss, Sportplatz).

+
+
+ +

Steuerung: flappen · neigen · R Reset

+ +
+
+
+ + +
+ + + + diff --git a/App/sims/heli/waypoints.js b/App/sims/heli/waypoints.js new file mode 100644 index 0000000..9ed590c --- /dev/null +++ b/App/sims/heli/waypoints.js @@ -0,0 +1,170 @@ +var WP = { + nenzing:{lat:47.18528,lon:9.70556,name:'C8 Nenzing (ÖAMTC)'}, + hohenems:{lat:47.33833,lon:9.69972,name:'Hohenems (Flugplatz)'}, + feldkirch:{lat:47.23528,lon:9.59806,name:'Feldkirch'}, + rankweil:{lat:47.27222,lon:9.64444,name:'Rankweil'}, + goetzis:{lat:47.33167,lon:9.63444,name:'Götzis'}, + dornbirn:{lat:47.41250,lon:9.74167,name:'Dornbirn'}, + bregenz:{lat:47.50278,lon:9.74722,name:'Bregenz'}, + lustenau:{lat:47.42083,lon:9.65833,name:'Lustenau'}, + bludenz:{lat:47.15444,lon:9.82167,name:'Bludenz'}, + buers:{lat:47.15139,lon:9.80000,name:'Bürs'}, + nueziders:{lat:47.17083,lon:9.80000,name:'Nüziders'}, + innerbraz:{lat:47.14750,lon:9.90000,name:'Innerbraz'}, + kloesterle:{lat:47.12917,lon:10.08333,name:'Klösterle'}, + stuben:{lat:47.13000,lon:10.18750,name:'Stuben am Arlberg'}, + langen:{lat:47.12833,lon:10.11667,name:'Langen am Arlberg'}, + schruns:{lat:47.07972,lon:9.91667,name:'Schruns'}, + tschagguns:{lat:47.07778,lon:9.90000,name:'Tschagguns'}, + stgallenk:{lat:47.02222,lon:9.97222,name:'St. Gallenkirch'}, + gaschurn:{lat:46.98556,lon:10.02278,name:'Gaschurn'}, + partenen:{lat:46.96889,lon:10.06389,name:'Partenen'}, + thueringen:{lat:47.21667,lon:9.76667,name:'Thüringen'}, + zwischenwasser:{lat:47.23333,lon:9.81667,name:'Zwischenwasser'}, + laterns:{lat:47.26111,lon:9.72500,name:'Laterns'}, + damuels:{lat:47.28333,lon:9.88611,name:'Damüls'}, + fontanella:{lat:47.26667,lon:9.93889,name:'Fontanella'}, + sonntag:{lat:47.23333,lon:9.91111,name:'Sonntag'}, + egg:{lat:47.43167,lon:9.89444,name:'Egg'}, + bezau:{lat:47.38611,lon:9.90000,name:'Bezau'}, + mellau:{lat:47.35000,lon:9.88056,name:'Mellau'}, + au:{lat:47.32500,lon:9.97500,name:'Au'}, + schoppernau:{lat:47.30833,lon:10.01667,name:'Schoppernau'}, + warth:{lat:47.25333,lon:10.18833,name:'Warth'}, + schroecken:{lat:47.25833,lon:10.09167,name:'Schröcken'}, + brand:{lat:47.10028,lon:9.73361,name:'Brand'}, + riezlern:{lat:47.35833,lon:10.19167,name:'Riezlern'}, + mittelberg:{lat:47.33333,lon:10.15000,name:'Mittelberg'}, + innsbruck:{lat:47.26333,lon:11.39444,name:'C1 Innsbruck (ÖAMTC)'}, + zams_c5:{lat:47.15972,lon:10.59333,name:'C5 Zams (ÖAMTC)'}, + kitzbuehel_c4:{lat:47.38011,lon:12.35800,name:'C4 Kitzbühel (ÖAMTC)'}, + lienz_c7:{lat:46.82970,lon:12.76470,name:'C7 Lienz (ÖAMTC)'}, + hall:{lat:47.28944,lon:11.50833,name:'Hall in Tirol'}, + schwaz:{lat:47.35222,lon:11.71028,name:'Schwaz'}, + woergl:{lat:47.48889,lon:12.06389,name:'Wörgl'}, + kufstein:{lat:47.58333,lon:12.16667,name:'Kufstein'}, + telfs:{lat:47.31278,lon:11.06972,name:'Telfs'}, + imst:{lat:47.24500,lon:10.73972,name:'Imst'}, + landeck:{lat:47.13944,lon:10.56667,name:'Landeck'}, + reutte:{lat:47.48583,lon:10.71806,name:'Reutte'}, + oetz:{lat:47.20833,lon:10.89333,name:'Ötz'}, + soelden:{lat:46.96667,lon:10.87500,name:'Sölden'}, + obergurgl:{lat:46.87000,lon:11.03000,name:'Obergurgl'}, + jenbach:{lat:47.39222,lon:11.77500,name:'Jenbach'}, + zell_ziller:{lat:47.22861,lon:11.88222,name:'Zell am Ziller'}, + mayrhofen:{lat:47.15944,lon:11.86111,name:'Mayrhofen'}, + hintertux:{lat:47.08333,lon:11.65000,name:'Hintertux'}, + steinach:{lat:47.08861,lon:11.46722,name:'Steinach am Brenner'}, + brenner:{lat:47.04222,lon:11.50667,name:'Brenner'}, + matrei:{lat:47.00000,lon:12.53333,name:'Matrei in Osttirol'}, + kals:{lat:47.00833,lon:12.64167,name:'Kals am Großglockner'}, + sillian:{lat:46.75194,lon:12.42194,name:'Sillian'}, + st_anton:{lat:47.12861,lon:10.26528,name:'St. Anton am Arlberg'}, + kitzbuehel_ort:{lat:47.44639,lon:12.39222,name:'Kitzbühel'}, + st_johann:{lat:47.52278,lon:12.42472,name:'St. Johann in Tirol'}, + seefeld:{lat:47.33000,lon:11.19000,name:'Seefeld'}, + scharnitz:{lat:47.38889,lon:11.26472,name:'Scharnitz'}, + fulpmes:{lat:47.15583,lon:11.35333,name:'Fulpmes'}, + neustift:{lat:47.11083,lon:11.31167,name:'Neustift im Stubaital'}, + bergrettung_muels:{lat:47.28000,lon:9.88500,name:'Skiunfall Damüls-Mellau'}, + skiunfall_soelden:{lat:46.97000,lon:10.86000,name:'Skiunfall Sölden'}, + verkehr_a14:{lat:47.26500,lon:9.64750,name:'Unfall A14 Rankweil'}, + + // ===== ÖSTERREICH-WEITE BASEN (C2/C3/C6/C9/C10/C11/C12/C14/C16) ===== + krems_c2:{lat:48.43194,lon:15.62556,name:'C2 Krems (ÖAMTC)'}, + wr_neustadt_c3:{lat:47.84278,lon:16.26472,name:'C3 Wiener Neustadt (ÖAMTC)'}, + salzburg_c6:{lat:47.79367,lon:13.00433,name:'C6 Salzburg (ÖAMTC)'}, + wien_c9:{lat:48.22220,lon:16.50470,name:'C9 Wien (ÖAMTC)'}, + linz_c10:{lat:48.23306,lon:14.18722,name:'C10 Linz (ÖAMTC)'}, + klagenfurt_c11:{lat:46.64278,lon:14.33778,name:'C11 Klagenfurt (ÖAMTC)'}, + graz_c12:{lat:46.99111,lon:15.43944,name:'C12 Graz (ÖAMTC)'}, + oberwart_c16:{lat:47.28278,lon:16.19333,name:'C16 Oberwart (ÖAMTC)'}, + niederoeblarn_c14:{lat:47.50750,lon:14.01056,name:'C14 Niederöblarn (ÖAMTC)'}, + + // ===== WIEN / NIEDERÖSTERREICH ===== + wien:{lat:48.20850,lon:16.37190,name:'Wien'}, + wien_donau:{lat:48.24000,lon:16.42000,name:'Neue Donau Wien'}, + wr_neustadt:{lat:47.81350,lon:16.24000,name:'Wiener Neustadt'}, + baden:{lat:48.00680,lon:16.23400,name:'Baden bei Wien'}, + moedling:{lat:48.08670,lon:16.28370,name:'Mödling'}, + krems:{lat:48.40900,lon:15.61000,name:'Krems an der Donau'}, + st_poelten:{lat:48.20470,lon:15.62560,name:'St. Pölten'}, + semmering:{lat:47.64310,lon:15.83330,name:'Semmering'}, + schneeberg:{lat:47.76670,lon:15.80670,name:'Schneeberg'}, + rax:{lat:47.70970,lon:15.71860,name:'Rax'}, + gutenstein:{lat:47.87190,lon:15.88190,name:'Gutenstein'}, + puchberg:{lat:47.78250,lon:15.90830,name:'Puchberg am Schneeberg'}, + + // ===== BURGENLAND ===== + oberwart:{lat:47.28330,lon:16.21670,name:'Oberwart'}, + mattersburg:{lat:47.73330,lon:16.40000,name:'Mattersburg'}, + eisenstadt:{lat:47.84500,lon:16.52330,name:'Eisenstadt'}, + neusiedl:{lat:47.95000,lon:16.84310,name:'Neusiedl am See'}, + podersdorf:{lat:47.85030,lon:16.83310,name:'Podersdorf am See'}, + rust:{lat:47.80000,lon:16.67500,name:'Rust'}, + + // ===== OBERÖSTERREICH ===== + linz:{lat:48.30690,lon:14.28580,name:'Linz'}, + wels:{lat:48.15940,lon:14.02360,name:'Wels'}, + sattledt:{lat:48.06670,lon:14.03440,name:'Sattledt'}, + gmunden:{lat:47.91780,lon:13.80000,name:'Gmunden'}, + traunsee:{lat:47.87610,lon:13.82920,name:'Traunsee'}, + hallstatt:{lat:47.56220,lon:13.64890,name:'Hallstatt'}, + bad_ischl:{lat:47.71080,lon:13.62580,name:'Bad Ischl'}, + dachstein:{lat:47.47500,lon:13.60640,name:'Dachstein'}, + + // ===== SALZBURG ===== + salzburg:{lat:47.80950,lon:13.05500,name:'Salzburg'}, + hallein:{lat:47.68360,lon:13.09700,name:'Hallein'}, + golling:{lat:47.59310,lon:13.17170,name:'Golling an der Salzach'}, + flachau:{lat:47.34500,lon:13.38970,name:'Flachau'}, + st_wolfgang:{lat:47.73920,lon:13.44280,name:'St. Wolfgang'}, + zell_see:{lat:47.32310,lon:12.79360,name:'Zell am See'}, + kaprun:{lat:47.27080,lon:12.75310,name:'Kaprun'}, + lofer:{lat:47.58690,lon:12.69330,name:'Lofer'}, + grossglockner:{lat:47.07440,lon:12.69390,name:'Großglockner'}, + wolfgangsee:{lat:47.74500,lon:13.40000,name:'Wolfgangsee'}, + + // ===== STEIERMARK ===== + graz:{lat:47.07070,lon:15.43940,name:'Graz'}, + leoben:{lat:47.38310,lon:15.09060,name:'Leoben'}, + bruck_mur:{lat:47.41060,lon:15.27310,name:'Bruck an der Mur'}, + schladming:{lat:47.39440,lon:13.68330,name:'Schladming'}, + mariazell:{lat:47.77500,lon:15.31670,name:'Mariazell'}, + hochschwab:{lat:47.61810,lon:15.14830,name:'Hochschwab'}, + niederoeblarn:{lat:47.50750,lon:14.01060,name:'Niederöblarn'}, + bad_aussee:{lat:47.60920,lon:13.78250,name:'Bad Aussee'}, + grundlsee:{lat:47.62860,lon:13.82280,name:'Grundlsee'}, + + // ===== KÄRNTEN ===== + klagenfurt:{lat:46.62450,lon:14.30830,name:'Klagenfurt'}, + villach:{lat:46.61060,lon:13.85580,name:'Villach'}, + poertschach:{lat:46.64310,lon:14.14860,name:'Pörtschach am Wörthersee'}, + velden:{lat:46.61360,lon:14.03940,name:'Velden am Wörthersee'}, + spittal:{lat:46.80060,lon:13.49640,name:'Spittal an der Drau'}, + millstatt:{lat:46.80250,lon:13.56920,name:'Millstatt am See'}, + heiligenblut:{lat:47.04000,lon:12.84420,name:'Heiligenblut'}, + woerthersee:{lat:46.62500,lon:14.14500,name:'Wörthersee'}, + + // ===== ZIELE (Autobahn/Berg/See) ===== + verkehr_a2_baden:{lat:47.99530,lon:16.24450,name:'Unfall A2 Baden'}, + verkehr_a1_sattledt:{lat:48.06670,lon:14.03440,name:'Unfall A1 Sattledt'}, + verkehr_a10_tauern:{lat:47.26500,lon:13.47670,name:'Unfall A10 Tauerntunnel'}, + verkehr_a9_pyhrn:{lat:47.50330,lon:14.33330,name:'Unfall A9 Bosruck'}, + verkehr_s6_semmering:{lat:47.60000,lon:15.90000,name:'Unfall S6 Semmering'}, + verkehr_a2_klagenfurt:{lat:46.65000,lon:14.30000,name:'Unfall A2 bei Klagenfurt'}, + berg_schneeberg:{lat:47.77000,lon:15.80000,name:'Bergunfall Schneeberg'}, + berg_rax:{lat:47.71000,lon:15.72000,name:'Wanderunfall Rax'}, + berg_wechsel:{lat:47.60330,lon:15.91500,name:'Bergunfall Wechsel'}, + berg_dachstein:{lat:47.47500,lon:13.60500,name:'Bergrettung Dachstein'}, + berg_hochschwab:{lat:47.61810,lon:15.14830,name:'Bergrettung Hochschwab'}, + berg_glockner:{lat:47.07440,lon:12.69390,name:'Bergrettung Großglockner'}, + berg_tennengebirge:{lat:47.55000,lon:13.30000,name:'Bergrettung Tennengebirge'}, + see_neusiedl:{lat:47.85030,lon:16.77000,name:'Seerettung Neusiedler See'}, + see_woerth:{lat:46.64310,lon:14.14860,name:'Seerettung Wörthersee'}, + see_wolfgang:{lat:47.74500,lon:13.43000,name:'Seerettung Wolfgangsee'}, + see_traun:{lat:47.87610,lon:13.82920,name:'Seerettung Traunsee'}, + see_hallstatt:{lat:47.56220,lon:13.66000,name:'Seerettung Hallstätter See'}, + see_donau_wien:{lat:48.24000,lon:16.42000,name:'Donau-Rettung Wien'}, + see_millstatt:{lat:46.80000,lon:13.59000,name:'Seerettung Millstätter See'}, +}; \ No newline at end of file diff --git a/App/sims/klima/game-2d.html b/App/sims/klima/game-2d.html index 092ea88..eef0604 100644 --- a/App/sims/klima/game-2d.html +++ b/App/sims/klima/game-2d.html @@ -1160,6 +1160,14 @@ function showEventUI(ev) { n.addEventListener('click', () => openInfoOverlay(infoKey)); } vp.appendChild(n); + // Stapel-Obergrenze: maximal 6 sichtbare Toasts. Kommt ein siebter, fliegt + // der älteste sofort raus, damit die Canvas-Szene nicht zugedeckt wird. + const MAX_TOASTS = 6; + while (vp.childElementCount > MAX_TOASTS) { + const oldest = vp.firstElementChild; + if (!oldest) break; + oldest.remove(); + } setTimeout(() => n.remove(), 11400); } diff --git a/App/sims/logistik/assets/audio/voice-lines.json b/App/sims/logistik/assets/audio/voice-lines.json new file mode 100644 index 0000000..65128e4 --- /dev/null +++ b/App/sims/logistik/assets/audio/voice-lines.json @@ -0,0 +1,61 @@ +{ + "_comment": "Voice-Line-Definitionen fuer Logistik L1. 'std' und 'easy' werden getrennt via OpenAI TTS generiert. Pro Eintrag entstehen 2 MP3s: .mp3 (std) und _easy.mp3 (easy). Stimme: nova (weiblich, warm). Modell: tts-1.", + "_voice": "nova", + "_model": "tts-1", + "lines": [ + { "id": "welcome", + "std": "Willkommen bei deiner Spedition. Dein Lieferwagen steht in Wien bereit. Klick einen Auftrag an und dein Fahrzeug wird beladen. Dann klickst du auf Los-gehts, und die Fahrt startet.", + "easy": "Guten Tag. Dein LKW ist in Wien. Klick einen Auftrag. Dann klick auf Los-gehts." }, + + { "id": "order_to_wien", "std": "Neuer Auftrag nach Wien geladen.", "easy": "Auftrag nach Wien." }, + { "id": "order_to_salzburg", "std": "Neuer Auftrag nach Salzburg geladen.", "easy": "Auftrag nach Salzburg." }, + { "id": "order_to_muenchen", "std": "Neuer Auftrag nach München geladen.", "easy": "Auftrag nach München." }, + { "id": "order_to_innsbruck", "std": "Neuer Auftrag nach Innsbruck geladen.", "easy": "Auftrag nach Innsbruck." }, + { "id": "order_to_graz", "std": "Neuer Auftrag nach Graz geladen.", "easy": "Auftrag nach Graz." }, + { "id": "order_to_mailand", "std": "Neuer Auftrag nach Mailand geladen.", "easy": "Auftrag nach Mailand." }, + { "id": "order_to_berlin", "std": "Neuer Auftrag nach Berlin geladen.", "easy": "Auftrag nach Berlin." }, + + { "id": "delivered_at_wien", "std": "Lieferung in Wien abgeschlossen. Gut gemacht.", "easy": "Fertig in Wien." }, + { "id": "delivered_at_salzburg", "std": "Lieferung in Salzburg abgeschlossen. Gut gemacht.", "easy": "Fertig in Salzburg." }, + { "id": "delivered_at_muenchen", "std": "Lieferung in München abgeschlossen. Gut gemacht.", "easy": "Fertig in München." }, + { "id": "delivered_at_innsbruck", "std": "Lieferung in Innsbruck abgeschlossen. Gut gemacht.", "easy": "Fertig in Innsbruck." }, + { "id": "delivered_at_graz", "std": "Lieferung in Graz abgeschlossen. Gut gemacht.", "easy": "Fertig in Graz." }, + { "id": "delivered_at_mailand", "std": "Lieferung in Mailand abgeschlossen. Gut gemacht.", "easy": "Fertig in Mailand." }, + { "id": "delivered_at_berlin", "std": "Lieferung in Berlin abgeschlossen. Gut gemacht.", "easy": "Fertig in Berlin." }, + { "id": "delivered_late", "std": "Lieferung angekommen — aber leider zu spät. Eine Strafe wird abgezogen.", "easy": "Zu spät. Das kostet Geld." }, + + { "id": "verfahren_anna", "std": "Fahrerin Anna hat sich verfahren. Klick die Zielstadt auf der Karte!", "easy": "Anna hat sich verirrt. Klick die Stadt auf der Karte." }, + { "id": "verfahren_tom", "std": "Fahrer Tom hat sich verfahren. Klick die Zielstadt auf der Karte!", "easy": "Tom hat sich verirrt. Klick die Stadt auf der Karte." }, + { "id": "verfahren_ok", "std": "Richtig! Weiter geht's.", "easy": "Richtig! Weiter." }, + { "id": "verfahren_wrong", "std": "Leider falsch. Anderthalb Stunden verloren.", "easy": "Falsch. Zeit weg." }, + + { "id": "second_truck_rented", "std": "Dein Konto ist über zehntausend Euro. Ein zweiter Lieferwagen wird automatisch gemietet. Die Tagesmiete steigt entsprechend.", "easy": "Noch ein LKW. Miete wird höher." }, + { "id": "mandatory_break_start", "std": "Lenkzeit-Pause. Dein Fahrzeug macht 45 Minuten Pause.", "easy": "Pause. 45 Minuten warten." }, + + { "id": "minigame_perfect", "std": "Sauber eingeparkt! Das gibt einen Bonus für die nächste Beladung.", "easy": "Super eingeparkt! Bonus!" }, + { "id": "minigame_touch", "std": "Auah, angefahren! Das kostet tausend Euro Reparatur.", "easy": "Kaputt. Kostet tausend Euro." }, + { "id": "minigame_timeout", "std": "Zu langsam. Dein Kollege übernimmt das Einparken.", "easy": "Zu langsam. Ein Kollege macht es." }, + + { "id": "day_1_end", "std": "Tag eins ist vorbei.", "easy": "Ein Tag vorbei." }, + { "id": "day_2_end", "std": "Tag zwei ist geschafft.", "easy": "Zwei Tage vorbei." }, + { "id": "day_3_end", "std": "Tag drei ist um.", "easy": "Drei Tage vorbei." }, + { "id": "day_4_end", "std": "Tag vier ist vorbei.", "easy": "Vier Tage vorbei." }, + { "id": "day_5_end", "std": "Tag fünf ist um — letzter Tag steht an.", "easy": "Fünf Tage vorbei. Letzter Tag." }, + { "id": "halftime", "std": "Halbzeit.", "easy": "Halbzeit." }, + { "id": "last_day", "std": "Nur noch ein Tag.", "easy": "Ein Tag noch." }, + { "id": "last_hour", "std": "Achtung: letzte Stunde.", "easy": "Letzte Stunde." }, + + { "id": "stars_5", "std": "Spitzenleistung! Fünf Sterne. Du hast das Maximum herausgeholt.", "easy": "Top! Fünf Sterne." }, + { "id": "stars_4", "std": "Sehr gut! Vier Sterne.", "easy": "Toll! Vier Sterne." }, + { "id": "stars_3", "std": "Solide! Drei Sterne.", "easy": "Gut! Drei Sterne." }, + { "id": "stars_2", "std": "Knapp. Zwei Sterne.", "easy": "Zwei Sterne." }, + { "id": "stars_1", "std": "Überlebt — ein Stern.", "easy": "Ein Stern." }, + { "id": "bankrupt", "std": "Das Konto ist leer. Die Simulation endet hier. Versuch es nochmal!", "easy": "Pleite. Probier es nochmal." }, + + { "id": "minigame_hint", "std": "Tipp: probier das Einpark-Minispiel oben mit dem Spielcontroller-Symbol. Wenn du sauber andockst, gibt es Bonus-Zeit beim Laden. In späteren Stufen kommen weitere Minispiele für Entladung und Fahrt dazu.", "easy": "Tipp: Klick oben den Spielcontroller. Bonus-Zeit beim Laden. Später noch mehr Minispiele." }, + + { "id": "all_negative_hint", "std": "Hinweis: alle aktuellen Aufträge wären Verlustgeschäfte. Warte ein paar Stunden ab — bessere kommen nach.", "easy": "Alle Aufträge bringen Verlust. Warte. Bessere kommen." }, + + { "id": "train_rented", "std": "Güterzug gemietet. Tagesmiete läuft — achte auf große Aufträge mit dem Zug-Symbol.", "easy": "Zug gemietet. Achte auf große Aufträge." } + ] +} diff --git a/App/sims/logistik/assets/audio/voice/de/all_negative_hint.mp3 b/App/sims/logistik/assets/audio/voice/de/all_negative_hint.mp3 new file mode 100644 index 0000000..4aaa8bf Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/all_negative_hint.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/all_negative_hint_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/all_negative_hint_easy.mp3 new file mode 100644 index 0000000..c206964 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/all_negative_hint_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/bankrupt.mp3 b/App/sims/logistik/assets/audio/voice/de/bankrupt.mp3 new file mode 100644 index 0000000..0898129 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/bankrupt.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/bankrupt_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/bankrupt_easy.mp3 new file mode 100644 index 0000000..b8b4526 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/bankrupt_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_1_end.mp3 b/App/sims/logistik/assets/audio/voice/de/day_1_end.mp3 new file mode 100644 index 0000000..536ecda Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_1_end.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_1_end_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/day_1_end_easy.mp3 new file mode 100644 index 0000000..2d5a7fb Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_1_end_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_2_end.mp3 b/App/sims/logistik/assets/audio/voice/de/day_2_end.mp3 new file mode 100644 index 0000000..d83a28a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_2_end.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_2_end_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/day_2_end_easy.mp3 new file mode 100644 index 0000000..dec39b5 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_2_end_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_3_end.mp3 b/App/sims/logistik/assets/audio/voice/de/day_3_end.mp3 new file mode 100644 index 0000000..01fbc97 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_3_end.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_3_end_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/day_3_end_easy.mp3 new file mode 100644 index 0000000..bb3d735 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_3_end_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_4_end.mp3 b/App/sims/logistik/assets/audio/voice/de/day_4_end.mp3 new file mode 100644 index 0000000..cf20f64 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_4_end.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_4_end_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/day_4_end_easy.mp3 new file mode 100644 index 0000000..a19d174 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_4_end_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_5_end.mp3 b/App/sims/logistik/assets/audio/voice/de/day_5_end.mp3 new file mode 100644 index 0000000..2bec4ac Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_5_end.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/day_5_end_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/day_5_end_easy.mp3 new file mode 100644 index 0000000..1203681 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/day_5_end_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin.mp3 new file mode 100644 index 0000000..b5bd84b Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin_easy.mp3 new file mode 100644 index 0000000..7072113 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_berlin_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_graz.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_graz.mp3 new file mode 100644 index 0000000..40e815a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_graz.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_graz_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_graz_easy.mp3 new file mode 100644 index 0000000..e5ab8f9 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_graz_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck.mp3 new file mode 100644 index 0000000..e0a7eda Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck_easy.mp3 new file mode 100644 index 0000000..5c4b89c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_innsbruck_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand.mp3 new file mode 100644 index 0000000..fbb029c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand_easy.mp3 new file mode 100644 index 0000000..51e8298 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_mailand_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen.mp3 new file mode 100644 index 0000000..77e692c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen_easy.mp3 new file mode 100644 index 0000000..1f1273a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_muenchen_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg.mp3 new file mode 100644 index 0000000..4cfdb0e Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg_easy.mp3 new file mode 100644 index 0000000..3f5a2d3 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_salzburg_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_wien.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_wien.mp3 new file mode 100644 index 0000000..f419bfe Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_wien.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_at_wien_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_at_wien_easy.mp3 new file mode 100644 index 0000000..d463d6f Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_at_wien_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_late.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_late.mp3 new file mode 100644 index 0000000..c2db766 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_late.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/delivered_late_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/delivered_late_easy.mp3 new file mode 100644 index 0000000..f025acc Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/delivered_late_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/halftime.mp3 b/App/sims/logistik/assets/audio/voice/de/halftime.mp3 new file mode 100644 index 0000000..03708bf Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/halftime.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/halftime_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/halftime_easy.mp3 new file mode 100644 index 0000000..50e95ad Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/halftime_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/last_day.mp3 b/App/sims/logistik/assets/audio/voice/de/last_day.mp3 new file mode 100644 index 0000000..3cea6c1 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/last_day.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/last_day_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/last_day_easy.mp3 new file mode 100644 index 0000000..415b16c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/last_day_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/last_hour.mp3 b/App/sims/logistik/assets/audio/voice/de/last_hour.mp3 new file mode 100644 index 0000000..f1c1648 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/last_hour.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/last_hour_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/last_hour_easy.mp3 new file mode 100644 index 0000000..fd0ecb8 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/last_hour_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/mandatory_break_start.mp3 b/App/sims/logistik/assets/audio/voice/de/mandatory_break_start.mp3 new file mode 100644 index 0000000..6f59f7c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/mandatory_break_start.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/mandatory_break_start_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/mandatory_break_start_easy.mp3 new file mode 100644 index 0000000..adea8b9 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/mandatory_break_start_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_hint.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_hint.mp3 new file mode 100644 index 0000000..f80043f Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_hint.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_hint_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_hint_easy.mp3 new file mode 100644 index 0000000..d717a4b Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_hint_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_perfect.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_perfect.mp3 new file mode 100644 index 0000000..34c100d Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_perfect.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_perfect_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_perfect_easy.mp3 new file mode 100644 index 0000000..032854f Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_perfect_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_timeout.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_timeout.mp3 new file mode 100644 index 0000000..5018e56 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_timeout.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_timeout_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_timeout_easy.mp3 new file mode 100644 index 0000000..b129b03 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_timeout_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_touch.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_touch.mp3 new file mode 100644 index 0000000..fed1f2a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_touch.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/minigame_touch_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/minigame_touch_easy.mp3 new file mode 100644 index 0000000..4b7af2e Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/minigame_touch_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_berlin.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_berlin.mp3 new file mode 100644 index 0000000..6e69995 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_berlin.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_berlin_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_berlin_easy.mp3 new file mode 100644 index 0000000..6bd1f62 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_berlin_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_graz.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_graz.mp3 new file mode 100644 index 0000000..1b5e2b7 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_graz.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_graz_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_graz_easy.mp3 new file mode 100644 index 0000000..eb2ba37 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_graz_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck.mp3 new file mode 100644 index 0000000..35f7e1f Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck_easy.mp3 new file mode 100644 index 0000000..b7f77e3 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_innsbruck_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_mailand.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_mailand.mp3 new file mode 100644 index 0000000..a2ceb9a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_mailand.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_mailand_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_mailand_easy.mp3 new file mode 100644 index 0000000..8729115 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_mailand_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_muenchen.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_muenchen.mp3 new file mode 100644 index 0000000..9824593 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_muenchen.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_muenchen_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_muenchen_easy.mp3 new file mode 100644 index 0000000..90c90cf Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_muenchen_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_salzburg.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_salzburg.mp3 new file mode 100644 index 0000000..7a5b2a6 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_salzburg.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_salzburg_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_salzburg_easy.mp3 new file mode 100644 index 0000000..654a370 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_salzburg_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_wien.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_wien.mp3 new file mode 100644 index 0000000..033ad7c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_wien.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/order_to_wien_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/order_to_wien_easy.mp3 new file mode 100644 index 0000000..63acc83 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/order_to_wien_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/second_truck_rented.mp3 b/App/sims/logistik/assets/audio/voice/de/second_truck_rented.mp3 new file mode 100644 index 0000000..8a13d39 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/second_truck_rented.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/second_truck_rented_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/second_truck_rented_easy.mp3 new file mode 100644 index 0000000..cb4b55e Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/second_truck_rented_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_1.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_1.mp3 new file mode 100644 index 0000000..d0b9f9d Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_1.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_1_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_1_easy.mp3 new file mode 100644 index 0000000..92d1eda Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_1_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_2.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_2.mp3 new file mode 100644 index 0000000..06290be Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_2.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_2_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_2_easy.mp3 new file mode 100644 index 0000000..fe1352c Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_2_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_3.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_3.mp3 new file mode 100644 index 0000000..08983ab Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_3.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_3_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_3_easy.mp3 new file mode 100644 index 0000000..3ffcc86 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_3_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_4.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_4.mp3 new file mode 100644 index 0000000..0503f9e Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_4.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_4_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_4_easy.mp3 new file mode 100644 index 0000000..4f57c8f Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_4_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_5.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_5.mp3 new file mode 100644 index 0000000..eb015c3 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_5.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/stars_5_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/stars_5_easy.mp3 new file mode 100644 index 0000000..a5cd305 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/stars_5_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/train_rented.mp3 b/App/sims/logistik/assets/audio/voice/de/train_rented.mp3 new file mode 100644 index 0000000..c377fdd Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/train_rented.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/train_rented_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/train_rented_easy.mp3 new file mode 100644 index 0000000..1547b9a Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/train_rented_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_anna.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_anna.mp3 new file mode 100644 index 0000000..f70a9b7 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_anna.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_anna_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_anna_easy.mp3 new file mode 100644 index 0000000..a49e359 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_anna_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_ok.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_ok.mp3 new file mode 100644 index 0000000..bfa4aad Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_ok.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_ok_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_ok_easy.mp3 new file mode 100644 index 0000000..f0e1724 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_ok_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_tom.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_tom.mp3 new file mode 100644 index 0000000..b6f86e4 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_tom.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_tom_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_tom_easy.mp3 new file mode 100644 index 0000000..d03e7e5 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_tom_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_wrong.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_wrong.mp3 new file mode 100644 index 0000000..9fe2b86 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_wrong.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/verfahren_wrong_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/verfahren_wrong_easy.mp3 new file mode 100644 index 0000000..fa867a5 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/verfahren_wrong_easy.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/welcome.mp3 b/App/sims/logistik/assets/audio/voice/de/welcome.mp3 new file mode 100644 index 0000000..6d3eb57 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/welcome.mp3 differ diff --git a/App/sims/logistik/assets/audio/voice/de/welcome_easy.mp3 b/App/sims/logistik/assets/audio/voice/de/welcome_easy.mp3 new file mode 100644 index 0000000..b3f9ae1 Binary files /dev/null and b/App/sims/logistik/assets/audio/voice/de/welcome_easy.mp3 differ diff --git a/App/sims/logistik/assets/cities/_original/amsterdam.png b/App/sims/logistik/assets/cities/_original/amsterdam.png new file mode 100644 index 0000000..6ab51af Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/amsterdam.png differ diff --git a/App/sims/logistik/assets/cities/_original/antwerpen_hafen.png b/App/sims/logistik/assets/cities/_original/antwerpen_hafen.png new file mode 100644 index 0000000..8b535d8 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/antwerpen_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/athen.png b/App/sims/logistik/assets/cities/_original/athen.png new file mode 100644 index 0000000..26e854d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/athen.png differ diff --git a/App/sims/logistik/assets/cities/_original/barcelona.png b/App/sims/logistik/assets/cities/_original/barcelona.png new file mode 100644 index 0000000..e1b1d59 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/barcelona.png differ diff --git a/App/sims/logistik/assets/cities/_original/barcelona_hafen.png b/App/sims/logistik/assets/cities/_original/barcelona_hafen.png new file mode 100644 index 0000000..0ae2271 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/barcelona_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/basel_terminal.png b/App/sims/logistik/assets/cities/_original/basel_terminal.png new file mode 100644 index 0000000..2721966 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/basel_terminal.png differ diff --git a/App/sims/logistik/assets/cities/_original/berlin.png b/App/sims/logistik/assets/cities/_original/berlin.png new file mode 100644 index 0000000..89e647c Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/berlin.png differ diff --git a/App/sims/logistik/assets/cities/_original/bern.png b/App/sims/logistik/assets/cities/_original/bern.png new file mode 100644 index 0000000..5ab1b1d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/bern.png differ diff --git a/App/sims/logistik/assets/cities/_original/bremerhaven_hafen.png b/App/sims/logistik/assets/cities/_original/bremerhaven_hafen.png new file mode 100644 index 0000000..7576350 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/bremerhaven_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/bruessel.png b/App/sims/logistik/assets/cities/_original/bruessel.png new file mode 100644 index 0000000..1f734ce Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/bruessel.png differ diff --git a/App/sims/logistik/assets/cities/_original/budapest.png b/App/sims/logistik/assets/cities/_original/budapest.png new file mode 100644 index 0000000..f0cb270 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/budapest.png differ diff --git a/App/sims/logistik/assets/cities/_original/dublin.png b/App/sims/logistik/assets/cities/_original/dublin.png new file mode 100644 index 0000000..daa29da Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/dublin.png differ diff --git a/App/sims/logistik/assets/cities/_original/duisburg_terminal.png b/App/sims/logistik/assets/cities/_original/duisburg_terminal.png new file mode 100644 index 0000000..9c8c52d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/duisburg_terminal.png differ diff --git a/App/sims/logistik/assets/cities/_original/frankfurt.png b/App/sims/logistik/assets/cities/_original/frankfurt.png new file mode 100644 index 0000000..7792713 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/frankfurt.png differ diff --git a/App/sims/logistik/assets/cities/_original/gdansk_hafen.png b/App/sims/logistik/assets/cities/_original/gdansk_hafen.png new file mode 100644 index 0000000..eb5746d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/gdansk_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/genua_hafen.png b/App/sims/logistik/assets/cities/_original/genua_hafen.png new file mode 100644 index 0000000..c392e52 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/genua_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/goeteborg_hafen.png b/App/sims/logistik/assets/cities/_original/goeteborg_hafen.png new file mode 100644 index 0000000..10ee61a Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/goeteborg_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/graz.png b/App/sims/logistik/assets/cities/_original/graz.png new file mode 100644 index 0000000..deaf74b Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/graz.png differ diff --git a/App/sims/logistik/assets/cities/_original/hamburg.png b/App/sims/logistik/assets/cities/_original/hamburg.png new file mode 100644 index 0000000..51bdba0 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/hamburg.png differ diff --git a/App/sims/logistik/assets/cities/_original/hamburg_hafen.png b/App/sims/logistik/assets/cities/_original/hamburg_hafen.png new file mode 100644 index 0000000..3314249 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/hamburg_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/helsinki.png b/App/sims/logistik/assets/cities/_original/helsinki.png new file mode 100644 index 0000000..6be3564 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/helsinki.png differ diff --git a/App/sims/logistik/assets/cities/_original/innsbruck.png b/App/sims/logistik/assets/cities/_original/innsbruck.png new file mode 100644 index 0000000..5a162e5 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/innsbruck.png differ diff --git a/App/sims/logistik/assets/cities/_original/koeln.png b/App/sims/logistik/assets/cities/_original/koeln.png new file mode 100644 index 0000000..7f5c96c Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/koeln.png differ diff --git a/App/sims/logistik/assets/cities/_original/kopenhagen.png b/App/sims/logistik/assets/cities/_original/kopenhagen.png new file mode 100644 index 0000000..631af9e Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/kopenhagen.png differ diff --git a/App/sims/logistik/assets/cities/_original/krakau.png b/App/sims/logistik/assets/cities/_original/krakau.png new file mode 100644 index 0000000..d90f487 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/krakau.png differ diff --git a/App/sims/logistik/assets/cities/_original/leipzig.png b/App/sims/logistik/assets/cities/_original/leipzig.png new file mode 100644 index 0000000..63c9e6f Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/leipzig.png differ diff --git a/App/sims/logistik/assets/cities/_original/lissabon.png b/App/sims/logistik/assets/cities/_original/lissabon.png new file mode 100644 index 0000000..9486062 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/lissabon.png differ diff --git a/App/sims/logistik/assets/cities/_original/london.png b/App/sims/logistik/assets/cities/_original/london.png new file mode 100644 index 0000000..a0d87ed Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/london.png differ diff --git a/App/sims/logistik/assets/cities/_original/lyon.png b/App/sims/logistik/assets/cities/_original/lyon.png new file mode 100644 index 0000000..5983349 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/lyon.png differ diff --git a/App/sims/logistik/assets/cities/_original/madrid.png b/App/sims/logistik/assets/cities/_original/madrid.png new file mode 100644 index 0000000..036f8b0 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/madrid.png differ diff --git a/App/sims/logistik/assets/cities/_original/mailand.png b/App/sims/logistik/assets/cities/_original/mailand.png new file mode 100644 index 0000000..ec9a40c Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/mailand.png differ diff --git a/App/sims/logistik/assets/cities/_original/manchester.png b/App/sims/logistik/assets/cities/_original/manchester.png new file mode 100644 index 0000000..bf583b4 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/manchester.png differ diff --git a/App/sims/logistik/assets/cities/_original/marseille.png b/App/sims/logistik/assets/cities/_original/marseille.png new file mode 100644 index 0000000..a002cc5 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/marseille.png differ diff --git a/App/sims/logistik/assets/cities/_original/marseille_hafen.png b/App/sims/logistik/assets/cities/_original/marseille_hafen.png new file mode 100644 index 0000000..74e78be Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/marseille_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/muenchen.png b/App/sims/logistik/assets/cities/_original/muenchen.png new file mode 100644 index 0000000..d9719c6 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/muenchen.png differ diff --git a/App/sims/logistik/assets/cities/_original/neapel.png b/App/sims/logistik/assets/cities/_original/neapel.png new file mode 100644 index 0000000..33d3885 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/neapel.png differ diff --git a/App/sims/logistik/assets/cities/_original/nuernberg.png b/App/sims/logistik/assets/cities/_original/nuernberg.png new file mode 100644 index 0000000..79fbb71 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/nuernberg.png differ diff --git a/App/sims/logistik/assets/cities/_original/oslo.png b/App/sims/logistik/assets/cities/_original/oslo.png new file mode 100644 index 0000000..b2ed3a8 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/oslo.png differ diff --git a/App/sims/logistik/assets/cities/_original/paris.png b/App/sims/logistik/assets/cities/_original/paris.png new file mode 100644 index 0000000..7c84796 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/paris.png differ diff --git a/App/sims/logistik/assets/cities/_original/piraeus_hafen.png b/App/sims/logistik/assets/cities/_original/piraeus_hafen.png new file mode 100644 index 0000000..7a5ce0d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/piraeus_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/porto.png b/App/sims/logistik/assets/cities/_original/porto.png new file mode 100644 index 0000000..cdc9652 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/porto.png differ diff --git a/App/sims/logistik/assets/cities/_original/prag.png b/App/sims/logistik/assets/cities/_original/prag.png new file mode 100644 index 0000000..8175a92 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/prag.png differ diff --git a/App/sims/logistik/assets/cities/_original/rom.png b/App/sims/logistik/assets/cities/_original/rom.png new file mode 100644 index 0000000..671b246 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/rom.png differ diff --git a/App/sims/logistik/assets/cities/_original/rotterdam.png b/App/sims/logistik/assets/cities/_original/rotterdam.png new file mode 100644 index 0000000..15b6808 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/rotterdam.png differ diff --git a/App/sims/logistik/assets/cities/_original/rotterdam_hafen.png b/App/sims/logistik/assets/cities/_original/rotterdam_hafen.png new file mode 100644 index 0000000..44dc0b0 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/rotterdam_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/salzburg.png b/App/sims/logistik/assets/cities/_original/salzburg.png new file mode 100644 index 0000000..29f1fa3 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/salzburg.png differ diff --git a/App/sims/logistik/assets/cities/_original/stockholm.png b/App/sims/logistik/assets/cities/_original/stockholm.png new file mode 100644 index 0000000..81aa392 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/stockholm.png differ diff --git a/App/sims/logistik/assets/cities/_original/stuttgart.png b/App/sims/logistik/assets/cities/_original/stuttgart.png new file mode 100644 index 0000000..48107d2 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/stuttgart.png differ diff --git a/App/sims/logistik/assets/cities/_original/thessaloniki.png b/App/sims/logistik/assets/cities/_original/thessaloniki.png new file mode 100644 index 0000000..7e5df7d Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/thessaloniki.png differ diff --git a/App/sims/logistik/assets/cities/_original/triest_hafen.png b/App/sims/logistik/assets/cities/_original/triest_hafen.png new file mode 100644 index 0000000..b332118 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/triest_hafen.png differ diff --git a/App/sims/logistik/assets/cities/_original/turin.png b/App/sims/logistik/assets/cities/_original/turin.png new file mode 100644 index 0000000..f357323 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/turin.png differ diff --git a/App/sims/logistik/assets/cities/_original/valencia.png b/App/sims/logistik/assets/cities/_original/valencia.png new file mode 100644 index 0000000..16183dd Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/valencia.png differ diff --git a/App/sims/logistik/assets/cities/_original/warschau.png b/App/sims/logistik/assets/cities/_original/warschau.png new file mode 100644 index 0000000..b4af61a Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/warschau.png differ diff --git a/App/sims/logistik/assets/cities/_original/wien.png b/App/sims/logistik/assets/cities/_original/wien.png new file mode 100644 index 0000000..a4ab6a7 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/wien.png differ diff --git a/App/sims/logistik/assets/cities/_original/zuerich.png b/App/sims/logistik/assets/cities/_original/zuerich.png new file mode 100644 index 0000000..9166408 Binary files /dev/null and b/App/sims/logistik/assets/cities/_original/zuerich.png differ diff --git a/App/sims/logistik/assets/cities/amsterdam.png b/App/sims/logistik/assets/cities/amsterdam.png new file mode 100644 index 0000000..64cc3d0 Binary files /dev/null and b/App/sims/logistik/assets/cities/amsterdam.png differ diff --git a/App/sims/logistik/assets/cities/antwerpen_hafen.png b/App/sims/logistik/assets/cities/antwerpen_hafen.png new file mode 100644 index 0000000..3cd654e Binary files /dev/null and b/App/sims/logistik/assets/cities/antwerpen_hafen.png differ diff --git a/App/sims/logistik/assets/cities/athen.png b/App/sims/logistik/assets/cities/athen.png new file mode 100644 index 0000000..ad55a28 Binary files /dev/null and b/App/sims/logistik/assets/cities/athen.png differ diff --git a/App/sims/logistik/assets/cities/barcelona.png b/App/sims/logistik/assets/cities/barcelona.png new file mode 100644 index 0000000..968c5aa Binary files /dev/null and b/App/sims/logistik/assets/cities/barcelona.png differ diff --git a/App/sims/logistik/assets/cities/barcelona_hafen.png b/App/sims/logistik/assets/cities/barcelona_hafen.png new file mode 100644 index 0000000..190c208 Binary files /dev/null and b/App/sims/logistik/assets/cities/barcelona_hafen.png differ diff --git a/App/sims/logistik/assets/cities/basel_terminal.png b/App/sims/logistik/assets/cities/basel_terminal.png new file mode 100644 index 0000000..70a9176 Binary files /dev/null and b/App/sims/logistik/assets/cities/basel_terminal.png differ diff --git a/App/sims/logistik/assets/cities/berlin.png b/App/sims/logistik/assets/cities/berlin.png new file mode 100644 index 0000000..160643a Binary files /dev/null and b/App/sims/logistik/assets/cities/berlin.png differ diff --git a/App/sims/logistik/assets/cities/bern.png b/App/sims/logistik/assets/cities/bern.png new file mode 100644 index 0000000..44dd7e1 Binary files /dev/null and b/App/sims/logistik/assets/cities/bern.png differ diff --git a/App/sims/logistik/assets/cities/bremerhaven_hafen.png b/App/sims/logistik/assets/cities/bremerhaven_hafen.png new file mode 100644 index 0000000..db7077a Binary files /dev/null and b/App/sims/logistik/assets/cities/bremerhaven_hafen.png differ diff --git a/App/sims/logistik/assets/cities/bruessel.png b/App/sims/logistik/assets/cities/bruessel.png new file mode 100644 index 0000000..9c90179 Binary files /dev/null and b/App/sims/logistik/assets/cities/bruessel.png differ diff --git a/App/sims/logistik/assets/cities/budapest.png b/App/sims/logistik/assets/cities/budapest.png new file mode 100644 index 0000000..279f24e Binary files /dev/null and b/App/sims/logistik/assets/cities/budapest.png differ diff --git a/App/sims/logistik/assets/cities/dublin.png b/App/sims/logistik/assets/cities/dublin.png new file mode 100644 index 0000000..3682eba Binary files /dev/null and b/App/sims/logistik/assets/cities/dublin.png differ diff --git a/App/sims/logistik/assets/cities/duisburg_terminal.png b/App/sims/logistik/assets/cities/duisburg_terminal.png new file mode 100644 index 0000000..a6c50c5 Binary files /dev/null and b/App/sims/logistik/assets/cities/duisburg_terminal.png differ diff --git a/App/sims/logistik/assets/cities/frankfurt.png b/App/sims/logistik/assets/cities/frankfurt.png new file mode 100644 index 0000000..8f8d02b Binary files /dev/null and b/App/sims/logistik/assets/cities/frankfurt.png differ diff --git a/App/sims/logistik/assets/cities/gdansk_hafen.png b/App/sims/logistik/assets/cities/gdansk_hafen.png new file mode 100644 index 0000000..4db579f Binary files /dev/null and b/App/sims/logistik/assets/cities/gdansk_hafen.png differ diff --git a/App/sims/logistik/assets/cities/genua_hafen.png b/App/sims/logistik/assets/cities/genua_hafen.png new file mode 100644 index 0000000..af6530a Binary files /dev/null and b/App/sims/logistik/assets/cities/genua_hafen.png differ diff --git a/App/sims/logistik/assets/cities/goeteborg_hafen.png b/App/sims/logistik/assets/cities/goeteborg_hafen.png new file mode 100644 index 0000000..6f2b903 Binary files /dev/null and b/App/sims/logistik/assets/cities/goeteborg_hafen.png differ diff --git a/App/sims/logistik/assets/cities/graz.png b/App/sims/logistik/assets/cities/graz.png new file mode 100644 index 0000000..53f90d5 Binary files /dev/null and b/App/sims/logistik/assets/cities/graz.png differ diff --git a/App/sims/logistik/assets/cities/hamburg.png b/App/sims/logistik/assets/cities/hamburg.png new file mode 100644 index 0000000..e7b4bef Binary files /dev/null and b/App/sims/logistik/assets/cities/hamburg.png differ diff --git a/App/sims/logistik/assets/cities/hamburg_hafen.png b/App/sims/logistik/assets/cities/hamburg_hafen.png new file mode 100644 index 0000000..630bf6b Binary files /dev/null and b/App/sims/logistik/assets/cities/hamburg_hafen.png differ diff --git a/App/sims/logistik/assets/cities/helsinki.png b/App/sims/logistik/assets/cities/helsinki.png new file mode 100644 index 0000000..f2ca95d Binary files /dev/null and b/App/sims/logistik/assets/cities/helsinki.png differ diff --git a/App/sims/logistik/assets/cities/innsbruck.png b/App/sims/logistik/assets/cities/innsbruck.png new file mode 100644 index 0000000..d6fceb5 Binary files /dev/null and b/App/sims/logistik/assets/cities/innsbruck.png differ diff --git a/App/sims/logistik/assets/cities/koeln.png b/App/sims/logistik/assets/cities/koeln.png new file mode 100644 index 0000000..834beec Binary files /dev/null and b/App/sims/logistik/assets/cities/koeln.png differ diff --git a/App/sims/logistik/assets/cities/kopenhagen.png b/App/sims/logistik/assets/cities/kopenhagen.png new file mode 100644 index 0000000..f3c6d58 Binary files /dev/null and b/App/sims/logistik/assets/cities/kopenhagen.png differ diff --git a/App/sims/logistik/assets/cities/krakau.png b/App/sims/logistik/assets/cities/krakau.png new file mode 100644 index 0000000..6dcd02f Binary files /dev/null and b/App/sims/logistik/assets/cities/krakau.png differ diff --git a/App/sims/logistik/assets/cities/leipzig.png b/App/sims/logistik/assets/cities/leipzig.png new file mode 100644 index 0000000..8b2507c Binary files /dev/null and b/App/sims/logistik/assets/cities/leipzig.png differ diff --git a/App/sims/logistik/assets/cities/lissabon.png b/App/sims/logistik/assets/cities/lissabon.png new file mode 100644 index 0000000..f5acfe9 Binary files /dev/null and b/App/sims/logistik/assets/cities/lissabon.png differ diff --git a/App/sims/logistik/assets/cities/london.png b/App/sims/logistik/assets/cities/london.png new file mode 100644 index 0000000..9db2c3a Binary files /dev/null and b/App/sims/logistik/assets/cities/london.png differ diff --git a/App/sims/logistik/assets/cities/lyon.png b/App/sims/logistik/assets/cities/lyon.png new file mode 100644 index 0000000..b7db795 Binary files /dev/null and b/App/sims/logistik/assets/cities/lyon.png differ diff --git a/App/sims/logistik/assets/cities/madrid.png b/App/sims/logistik/assets/cities/madrid.png new file mode 100644 index 0000000..85dab6d Binary files /dev/null and b/App/sims/logistik/assets/cities/madrid.png differ diff --git a/App/sims/logistik/assets/cities/mailand.png b/App/sims/logistik/assets/cities/mailand.png new file mode 100644 index 0000000..69af848 Binary files /dev/null and b/App/sims/logistik/assets/cities/mailand.png differ diff --git a/App/sims/logistik/assets/cities/manchester.png b/App/sims/logistik/assets/cities/manchester.png new file mode 100644 index 0000000..c9186f2 Binary files /dev/null and b/App/sims/logistik/assets/cities/manchester.png differ diff --git a/App/sims/logistik/assets/cities/marseille.png b/App/sims/logistik/assets/cities/marseille.png new file mode 100644 index 0000000..5f45ac3 Binary files /dev/null and b/App/sims/logistik/assets/cities/marseille.png differ diff --git a/App/sims/logistik/assets/cities/marseille_hafen.png b/App/sims/logistik/assets/cities/marseille_hafen.png new file mode 100644 index 0000000..4768274 Binary files /dev/null and b/App/sims/logistik/assets/cities/marseille_hafen.png differ diff --git a/App/sims/logistik/assets/cities/muenchen.png b/App/sims/logistik/assets/cities/muenchen.png new file mode 100644 index 0000000..0e6efd0 Binary files /dev/null and b/App/sims/logistik/assets/cities/muenchen.png differ diff --git a/App/sims/logistik/assets/cities/neapel.png b/App/sims/logistik/assets/cities/neapel.png new file mode 100644 index 0000000..7c23028 Binary files /dev/null and b/App/sims/logistik/assets/cities/neapel.png differ diff --git a/App/sims/logistik/assets/cities/nuernberg.png b/App/sims/logistik/assets/cities/nuernberg.png new file mode 100644 index 0000000..22de9b1 Binary files /dev/null and b/App/sims/logistik/assets/cities/nuernberg.png differ diff --git a/App/sims/logistik/assets/cities/oslo.png b/App/sims/logistik/assets/cities/oslo.png new file mode 100644 index 0000000..f4e1f79 Binary files /dev/null and b/App/sims/logistik/assets/cities/oslo.png differ diff --git a/App/sims/logistik/assets/cities/paris.png b/App/sims/logistik/assets/cities/paris.png new file mode 100644 index 0000000..460187e Binary files /dev/null and b/App/sims/logistik/assets/cities/paris.png differ diff --git a/App/sims/logistik/assets/cities/piraeus_hafen.png b/App/sims/logistik/assets/cities/piraeus_hafen.png new file mode 100644 index 0000000..70cbe63 Binary files /dev/null and b/App/sims/logistik/assets/cities/piraeus_hafen.png differ diff --git a/App/sims/logistik/assets/cities/porto.png b/App/sims/logistik/assets/cities/porto.png new file mode 100644 index 0000000..9ea2141 Binary files /dev/null and b/App/sims/logistik/assets/cities/porto.png differ diff --git a/App/sims/logistik/assets/cities/prag.png b/App/sims/logistik/assets/cities/prag.png new file mode 100644 index 0000000..dac70a4 Binary files /dev/null and b/App/sims/logistik/assets/cities/prag.png differ diff --git a/App/sims/logistik/assets/cities/rom.png b/App/sims/logistik/assets/cities/rom.png new file mode 100644 index 0000000..7025626 Binary files /dev/null and b/App/sims/logistik/assets/cities/rom.png differ diff --git a/App/sims/logistik/assets/cities/rotterdam.png b/App/sims/logistik/assets/cities/rotterdam.png new file mode 100644 index 0000000..7cfaedc Binary files /dev/null and b/App/sims/logistik/assets/cities/rotterdam.png differ diff --git a/App/sims/logistik/assets/cities/rotterdam_hafen.png b/App/sims/logistik/assets/cities/rotterdam_hafen.png new file mode 100644 index 0000000..b4ee82d Binary files /dev/null and b/App/sims/logistik/assets/cities/rotterdam_hafen.png differ diff --git a/App/sims/logistik/assets/cities/salzburg.png b/App/sims/logistik/assets/cities/salzburg.png new file mode 100644 index 0000000..0094bdd Binary files /dev/null and b/App/sims/logistik/assets/cities/salzburg.png differ diff --git a/App/sims/logistik/assets/cities/stockholm.png b/App/sims/logistik/assets/cities/stockholm.png new file mode 100644 index 0000000..0564c8c Binary files /dev/null and b/App/sims/logistik/assets/cities/stockholm.png differ diff --git a/App/sims/logistik/assets/cities/stuttgart.png b/App/sims/logistik/assets/cities/stuttgart.png new file mode 100644 index 0000000..b5a5450 Binary files /dev/null and b/App/sims/logistik/assets/cities/stuttgart.png differ diff --git a/App/sims/logistik/assets/cities/thessaloniki.png b/App/sims/logistik/assets/cities/thessaloniki.png new file mode 100644 index 0000000..93ad7ba Binary files /dev/null and b/App/sims/logistik/assets/cities/thessaloniki.png differ diff --git a/App/sims/logistik/assets/cities/triest_hafen.png b/App/sims/logistik/assets/cities/triest_hafen.png new file mode 100644 index 0000000..bb53a5b Binary files /dev/null and b/App/sims/logistik/assets/cities/triest_hafen.png differ diff --git a/App/sims/logistik/assets/cities/turin.png b/App/sims/logistik/assets/cities/turin.png new file mode 100644 index 0000000..ce4a81f Binary files /dev/null and b/App/sims/logistik/assets/cities/turin.png differ diff --git a/App/sims/logistik/assets/cities/valencia.png b/App/sims/logistik/assets/cities/valencia.png new file mode 100644 index 0000000..3c04522 Binary files /dev/null and b/App/sims/logistik/assets/cities/valencia.png differ diff --git a/App/sims/logistik/assets/cities/warschau.png b/App/sims/logistik/assets/cities/warschau.png new file mode 100644 index 0000000..38105c3 Binary files /dev/null and b/App/sims/logistik/assets/cities/warschau.png differ diff --git a/App/sims/logistik/assets/cities/wien.png b/App/sims/logistik/assets/cities/wien.png new file mode 100644 index 0000000..2a77edd Binary files /dev/null and b/App/sims/logistik/assets/cities/wien.png differ diff --git a/App/sims/logistik/assets/cities/zuerich.png b/App/sims/logistik/assets/cities/zuerich.png new file mode 100644 index 0000000..d9b9823 Binary files /dev/null and b/App/sims/logistik/assets/cities/zuerich.png differ diff --git a/App/sims/logistik/assets/splash/_original/level-1.png b/App/sims/logistik/assets/splash/_original/level-1.png new file mode 100644 index 0000000..b51b197 Binary files /dev/null and b/App/sims/logistik/assets/splash/_original/level-1.png differ diff --git a/App/sims/logistik/assets/splash/_original/level-2.png b/App/sims/logistik/assets/splash/_original/level-2.png new file mode 100644 index 0000000..04ecaf8 Binary files /dev/null and b/App/sims/logistik/assets/splash/_original/level-2.png differ diff --git a/App/sims/logistik/assets/splash/level-1.png b/App/sims/logistik/assets/splash/level-1.png new file mode 100644 index 0000000..d640725 Binary files /dev/null and b/App/sims/logistik/assets/splash/level-1.png differ diff --git a/App/sims/logistik/assets/splash/level-2.png b/App/sims/logistik/assets/splash/level-2.png new file mode 100644 index 0000000..d465c38 Binary files /dev/null and b/App/sims/logistik/assets/splash/level-2.png differ diff --git a/App/sims/logistik/balance-corridor.md b/App/sims/logistik/balance-corridor.md new file mode 100644 index 0000000..d2886bb --- /dev/null +++ b/App/sims/logistik/balance-corridor.md @@ -0,0 +1,169 @@ +# Balance-Korridor (Regeln R-1, R-2, R-3) + +**Stand:** 2026-04-24 +**Gilt für:** Logistik Europa (Modul 11), Vorlage für andere Sim-Module + +Dieses Dokument formalisiert den Balance-Anspruch („jedes Level muss man +gewinnen UND verlieren können") als drei Regeln und beschreibt eine +mechanische Methode, sie zu prüfen. + +--- + +## 1. Anspruch + +> **Ein Level ist balanciert, wenn:** +> 1. Der/die Bearbeiter:in **Zeit zum Nachdenken** hat, bevor sie ins +> Minus rutscht (R-1 Runway). +> 2. Mit gutem Spiel **gewinnen kann** (R-2 Winnable). +> 3. Mit schlechtem oder keinem Spiel **verlieren kann** (R-3 Loseable). +> +> Keine der drei Bedingungen darf systematisch verfehlt werden — sonst +> ist das Level entweder Frustquelle (kann nur verlieren) oder Pseudo- +> Lernspiel (kann nur gewinnen, keine Konsequenz). + +--- + +## 2. Regel R-1 — Startbudget-Runway + +Bereits in [`level-progression.md` §2](level-progression.md) hergeleitet. +Hier nur die Formel: + +``` +startBudget ≥ 1.5 × ( rentalCostPerDay × vehicleCount + + idleCostPerHour × vehicleCount × 12 ) +``` + +**Interpretation:** Anfangsbudget reicht für 1.5 Tage Operating ohne +Einnahmen. Gibt Puffer für Fehltrip + strategische Entscheidung. + +--- + +## 3. Regel R-2 — Winnable + +> Bei **optimalem oder mindestens guten** Spiel muss der Level +> spätestens beim **dritten Versuch** mit positivem End-Saldo gegenüber +> `minTargetEarnings` enden. + +**Formal (Median über 5 Seeds):** +- L1: `runLevel(1, *, naive)` → `successful: true` AND `endBalance − startBudget ≥ +1.000 €` +- L1: `runLevel(1, *, greedy)` → `successful: true` AND `endBalance − startBudget ≥ +2.000 €` +- L2: `runLevel(2, *, greedy)` → `successful: true` AND `endBalance − startBudget ≥ 0` +- L2: `runLevel(2, *, optimal)` → `successful: true` AND `endBalance − startBudget ≥ +5.000 €` +- L3: `runLevel(3, *, optimal)` → `successful: true` AND `endBalance − startBudget ≥ 0` + +(Werte aus `balance-matrix.md` §3 übernommen.) + +**Was wir gerade messen können:** +- L1: ✓ naive, greedy, optimal laufen — alle prüfbar +- L2: ✓ greedy + optimal laufen — prüfbar +- L3: ✓ optimal läuft (Best-Net-Heuristik seit 2026-04-24) — prüfbar + +→ `optimal` ist keine echte globale Optimierung (VRP-TW ist NP-hart), +sondern eine **Best-Net-Heuristik**: pro Tick wird die (Contract, Vehicle)- +Paarung mit maximalem erwarteten Netto ausgewählt +(reward × 1.15 − pickup-fare − delivery-fare − erwartete Verspätungsstrafe). +Dominiert greedy in allen drei Leveln und reicht als Proxy für „gutes Spiel". + +--- + +## 4. Regel R-3 — Loseable + +> **Untätigkeit oder grobes Fehlspiel** muss zuverlässig zum Verlieren +> führen, sonst ist das Level keine Lernsituation. + +**Formal (Median über 5 Seeds):** +- L1: `runLevel(1, *, noop)` → `successful: false` AND `endBalance < minTargetEarnings` +- L2: `runLevel(2, *, noop)` → `successful: false` +- L3: `runLevel(3, *, noop)` → `successful: false` +- L2: `runLevel(2, *, naive)` → `successful: false` (zusätzlich gefordert: schlechte Disposition reicht nicht) +- L3: `runLevel(3, *, naive)` → `successful: false` +- L3: `runLevel(3, *, greedy)` → `successful: false` ODER `knapp negativ` + +**Implizite Anforderung:** Auch ohne Untätigkeit muss der **Schweregrad +mit Level steigen**. L3 darf nicht durch greedy lösbar sein (sonst ist +es nur ein L2-Klon). + +--- + +## 5. Aktuelle Verifikation (Stand 2026-04-24) + +### R-1 (mathematisch) + +| Level | Required ≥ | Hat | Verdikt | +|-------|-----------|-----|---------| +| L1 | 0 | 8.000 | ✅ | +| L2 | 1.170 | 5.000 | ✅ | +| L3 | 4.650 | **5.000** *(nach Fix vom 2026-04-24)* | ✅ | + +### R-2 / R-3 (per Headless-Runner — siehe [headless-runner.html](headless-runner.html)) + +``` +Status: VOLLSTAENDIG +- noop, naive, greedy, optimal laufen alle +- Korridor-Check-Button in headless-runner.html pruft R-1 + R-2 + R-3 + ueber alle 60 Durchlaeufe (3 Lvl × 4 Strategien × 5 Seeds) +``` + +Konkrete Runs noch ausständig — beim Server-/Browser-Test mit +[headless-runner.html](http://localhost/geograsim/App/sims/logistik/headless-runner.html) +„Balance-Korridor-Check"-Button drücken (kommt mit dem Patch dieses Dokuments). + +--- + +## 6. Methode zur Sicherstellung („was tun, wenn ein Level neu konfiguriert wird") + +### 6a. Pre-Deploy-Check (vor jeder Level-Param-Änderung) + +1. **R-1 mit Taschenrechner**: Formel oben rechnen, muss ≥ erfüllt sein. + Engine-seitig schon eingebaut: `_validateRuleR1` in `loadContent` loggt + `console.warn` bei Verletzung. +2. **R-2/R-3 via Headless-Runner**: `headless-runner.html` öffnen, + „Balance-Korridor-Check" drücken. Ergebnis-Tabelle vergleichen mit + Soll-Korridor (siehe §3 + §4). +3. **Bei Verletzung**: Param ändern (üblicherweise `startBudget`, + `minTargetEarnings`, `eventProbabilityMultiplier`), erneut prüfen. +4. **Erst dann committen**. + +### 6b. Continuous Verification (perspektivisch, sobald Atlas freigibt) + +- Test-Suite-Script (Node.js): `npm run balance-check` führt + `LogistikRunner.runMatrix({levels:[1,2,3], strategies:[...], seeds:[1..5]})` + und vergleicht gegen Korridor. Exit-Code 0 = pass, 1 = fail. +- CI-Hook beim Push: blockiert Merge, wenn Korridor nicht erreicht. +- Voraussetzung: Node-Engine-Export (engine.js läuft schon UMD-konform). + +### 6c. Manueller Sanity-Check (ergänzend) + +Reine Zahlen reichen nicht — der „Spielspaß" ist nicht messbar. +Nach jedem Param-Change zusätzlich: +- 1× Live-Test im Browser pro Level mit naive Spielstil (mehrere + Fehltrips). +- 1× Test mit gezielt-strategischem Spielstil. +- Fühlt sich der Schwierigkeitsgrad richtig an, oder kommt Frust auf? + +Die mechanischen Regeln R-1/R-2/R-3 sind **notwendig, aber nicht +hinreichend**. + +--- + +## 7. Offene Punkte + +### Kurzfristig (vor dem Schul-Pilot) +- **`optimal`-Strategie** als Best-Net-Heuristik implementiert + (2026-04-24, headless-runner.js, 120 Zeilen). Kriterium pro Tick: + max `reward × 1.15 − pickup-fare − delivery-fare − erwartete Strafe`, + dann feasible Zuweisungen in Sortierreihenfolge feuern. +- **„Balance-Korridor-Check"-Button** in `headless-runner.html` + einbauen (erledigt, prueft R-1/R-2/R-3 mechanisch). + +### Mittelfristig +- **Node-Test-Script** mit Exit-Code für CI. +- **Per-Modul-Generalisierung**: das Korridor-Pattern in + `App/docs/integration-konzept.md` als Plattform-Vertrag dokumentieren, + damit jedes Modul (Klima, Heli, …) die gleiche Disziplin hat. + +### Langfristig +- **Stochastische Robustheit**: Korridor nicht nur über 5 Seeds + prüfen, sondern Konfidenzintervall (z.B. 95 % über 100 Seeds). + Aktuell nicht nötig — die Engine ist deterministisch und 5 Seeds + geben uns schon eine gute Stichprobe. diff --git a/App/sims/logistik/balance-matrix.md b/App/sims/logistik/balance-matrix.md index 329362d..91a7180 100644 --- a/App/sims/logistik/balance-matrix.md +++ b/App/sims/logistik/balance-matrix.md @@ -197,11 +197,11 @@ Wenn nach Phase 2/3 die Akzeptanz-Korridore nicht erreicht werden: "defaultHintMode": "BLINK_EXACT", "eventProbabilityMultiplier": 0, "latePenaltyOverride": 0.10, - "rentalCostPerDay": 0, + "rentalCostPerDay": 500, "idleCostPerHour": 0, "containerStandCostPerHour": 0, "minTargetEarnings": 3000, - "timeLimitHours": 24, + "timeLimitHours": 72, "initialTimeScale": 1, "railEnabled": false, "portsEnabled": false, diff --git a/App/sims/logistik/engine.js b/App/sims/logistik/engine.js index 604aba7..a18ecf4 100644 --- a/App/sims/logistik/engine.js +++ b/App/sims/logistik/engine.js @@ -60,6 +60,7 @@ }); const CONTRACT_STATE = Object.freeze({ + OFFERED: 'OFFERED', // L1-Progression: Angebot, muss erst akzeptiert werden OPEN: 'OPEN', RESERVED: 'RESERVED', ASSIGNED: 'ASSIGNED', @@ -71,6 +72,7 @@ LATE: 'LATE', FAILED: 'FAILED', CANCELLED: 'CANCELLED', + EXPIRED: 'EXPIRED', // OFFERED abgelaufen / abgelehnt }); const LOCATION_TYPE = Object.freeze({ @@ -108,11 +110,45 @@ ============================================================ */ const VEHICLE_DEFAULTS = Object.freeze({ - TRUCK_SMALL: { speedKmh: 70, capacity: 1, costPerKm: 1.2, costPerHour: 25, loadMinutes: 5, unloadMinutes: 5 }, - TRUCK_LARGE: { speedKmh: 60, capacity: 3, costPerKm: 2.5, costPerHour: 45, loadMinutes: 8, unloadMinutes: 8 }, - TRAIN: { speedKmh: 90, capacity: 20, costPerKm: 8, costPerHour: 150, loadMinutes: 20, unloadMinutes: 20 }, + TRUCK_SMALL: { speedKmh: 70, capacity: 1, costPerKm: 1.2, costPerHour: 25, loadMinutes: 5, unloadMinutes: 5, rentMult: 1.0 }, + TRUCK_MID: { speedKmh: 65, capacity: 2, costPerKm: 1.6, costPerHour: 30, loadMinutes: 6, unloadMinutes: 6, rentMult: 1.0 }, + TRUCK_LARGE: { speedKmh: 60, capacity: 3, costPerKm: 2.5, costPerHour: 45, loadMinutes: 8, unloadMinutes: 8, rentMult: 1.4 }, + TRAIN_SMALL: { speedKmh: 80, capacity: 6, costPerKm: 5, costPerHour: 90, loadMinutes: 15, unloadMinutes: 15, rentMult: 3.0 }, + TRAIN: { speedKmh: 90, capacity: 20, costPerKm: 8, costPerHour: 150, loadMinutes: 20, unloadMinutes: 20, rentMult: 4.0 }, }); + /* ============================================================ + ID-Generierung — garantiert eindeutig pro Game-Session. + Monotoner Counter (deterministisch fuer Replay), zusaetzlich + Collision-Check nach jedem Push. + ============================================================ */ + function _nextUniqId(game) { + if (!game._uniqIdCounter) game._uniqIdCounter = 0; + return ++game._uniqIdCounter; + } + + /** Wirft eine Warnung in die Konsole, wenn doppelte IDs auftauchen. + * Greift defensiv — ein Bug schluepft so nicht unbemerkt durch. */ + function _validateUniqueIds(game) { + const groups = [ + ['Contract', game.activeContracts || []], + ['Vehicle', game.vehicles || []], + ['Route', game.activeRoutes || []], + ]; + for (const [label, list] of groups) { + const seen = new Set(); + for (const item of list) { + if (!item || !item.id) continue; + if (seen.has(item.id)) { + console.error('[Logistik] DOPPELTE ' + label + '-ID erkannt:', item.id, item); + return false; + } + seen.add(item.id); + } + } + return true; + } + const ECONOMY = Object.freeze({ standCostContainerPerHour: 10, // Container im Hafen standCostVehicleIdlePerHour: 5, @@ -176,6 +212,11 @@ hintUsages: 0, misclicks: 0, }, + progression: { + round: 1, // Aktuelle Runde, +1 nach jedem abgeschlossenen Auftrag + trucksBought: 0, // Wie viele Fahrzeuge wurden auto-/manuell dazugekauft + }, + pendingBonuses: [], // Ladebonus-Stack aus Minispielen: [{id, minutes, source, createdAt}] worldState: {}, version: '0.1.0', _pendingEvents: [], @@ -231,13 +272,37 @@ game.worldState.vehicleTypes = (seeds && seeds.vehicleTypes) || ws.vehicleTypes || []; game.worldState.cargoTypes = (seeds && seeds.cargoTypes) || ws.cargoTypes || []; game.worldState.railnet = (seeds && seeds.railnet) || ws.railnet || { nodes: [], edges: [] }; + game.worldState.roadnet = (seeds && seeds.roadnet) || ws.roadnet || { nodes: [], edges: [] }; // Phase 3: Hand-gepflegte Polylines (lg-routes-osm.json) const rOsm = (seeds && seeds.routesOsm) || ws.routesOsm || {}; game.worldState.routesOsm = Array.isArray(rOsm) ? rOsm : (rOsm.routes || []); + // Phase 8d: R-1 Startbudget-Runway-Pruefung (console.warn, nicht blockierend) + _validateRuleR1(game._config || {}); _setState(game, STATE.READY, 'content_loaded'); return game; } + /** Regel R-1 (Startbudget-Runway): ein Level muss 1.5 Tage „Luft" bieten. + * startBudget >= 1.5 * (rentalCostPerDay*vc + idleCostPerHour*vc*12) + * Verletzungen loggen, Spiel nicht abbrechen (Lehrkraft kann bewusst + * aendern, aber wir weisen in der Konsole darauf hin). */ + function _validateRuleR1(cfg) { + const vc = cfg.vehicleCount || 1; + const rental = cfg.rentalCostPerDay || 0; + const idle = cfg.idleCostPerHour || 0; + const daily = rental * vc + idle * vc * 12; + const needed = 1.5 * daily; + const have = cfg.startBudget || 0; + if (have < needed) { + console.warn( + '[Logistik R-1] Level verletzt Startbudget-Runway: ' + + 'startBudget=' + have + '€, benoetigt >= ' + Math.ceil(needed) + '€ ' + + '(Formel: 1.5 × (' + rental + '×' + vc + ' + ' + idle + '×' + vc + '×12 = ' + daily + '/Tag)). ' + + 'Spieler:in wird in Tag 1 im Minus landen. Fix: startBudget hoeher oder vehicleCount/rentalCostPerDay runter.' + ); + } + } + /** READY → PLANNING (Bearbeiter:in disponiert, Zeit steht still). */ function startPlanning(game) { if (game.state !== STATE.READY) { @@ -346,28 +411,31 @@ _maybeGenerateEvents(game, simMinutes); _expireEvents(game); - // Auswertung: Zeit-Limit (Fail) und Min-Ziel (Success bei Limit-Erreich) + // Phase 8e: OFFERED-Angebote auslaufen lassen (stille Entfernung) + _expireOffers(game); + + // Phase 8e: L1-Stufen-Generator haelt offer-buffer gefuellt + _maybeRefillOffers(game); + + // Bankrott-Regel (Thomas 2026-04-24): balance <= 0 -> sofort LEVEL_FAILED. + // Greift auf allen Levels. Gilt unabhaengig vom Zeitlimit. + if ((game.balance || 0) <= 0) { + _setState(game, STATE.LEVEL_FAILED, 'bankrupt'); + return game; + } + + // Auswertung: Zeit-Limit erreicht -> Abrechnung. + // Neue Regel: Erfolg = nicht-pleite (balance > 0). Kein Min-Ziel mehr, + // stattdessen Sterne-Bewertung in der UI basierend auf Endbilanz. const limit = game._config && game._config.timeLimitHours; if (typeof limit === 'number' && getSimHoursElapsed(game) >= limit) { - const cfg = game._config; - const startBudget = cfg.startBudget || 5000; - const profit = game.balance - startBudget; - const minTarget = cfg.minTargetEarnings || 0; _setState(game, - profit >= minTarget ? STATE.LEVEL_SUCCESS : STATE.LEVEL_FAILED, - profit >= minTarget ? 'time_limit_with_target_reached' : 'time_limit_reached' + (game.balance || 0) > 0 ? STATE.LEVEL_SUCCESS : STATE.LEVEL_FAILED, + (game.balance || 0) > 0 ? 'time_limit_solvent' : 'time_limit_bankrupt' ); - } else if ((game.activeContracts || []).length === 0 - && (game.completedContracts || []).length > 0) { - // Alle Aufträge abgeschlossen → Erfolgsprüfung sofort - const cfg = game._config || {}; - const startBudget = cfg.startBudget || 5000; - const profit = game.balance - startBudget; - const minTarget = cfg.minTargetEarnings || 0; - if (profit >= minTarget) { - _setState(game, STATE.LEVEL_SUCCESS, 'all_contracts_done_target_reached'); - } } + // Kein vorzeitiger SUCCESS mehr, wenn alle Auftraege fertig sind — + // Thomas will die volle Spielzeit fuer maximale Endbilanz nutzen. return game; } @@ -439,14 +507,10 @@ node(originId).id, node(targetId).id ); if (dij && dij.path) { - geometry = dij.path.map(nodeId => { - const n = rail.nodes.find(x => x.id === nodeId); - const loc = locs.find(l => l.id === n.locationId); - return [loc.lat, loc.lon]; - }); - distanceKm = dij.distanceKm; - provider = 'rail_dijkstra'; - // Segments pro Bahnkante + // Edge-Polylines ausrollen (analog zur Roadnet-Logik), damit auf + // der Karte echte Bahnkorridor-Kurven statt Luftlinie dargestellt + // werden. Edge-Polyline kann andersherum gespeichert sein -> reverse. + geometry = []; segments = []; for (let i = 0; i < dij.path.length - 1; i++) { const a = rail.nodes.find(n => n.id === dij.path[i]); @@ -456,6 +520,11 @@ (e.from === b.id && e.to === a.id)); const la = locs.find(l => l.id === a.locationId); const lb = locs.find(l => l.id === b.locationId); + let edgePoly = edge && edge.polyline ? edge.polyline.slice() : null; + if (edgePoly && edge.from === b.id && edge.to === a.id) edgePoly.reverse(); + if (!edgePoly) edgePoly = [[la.lat, la.lon], [lb.lat, lb.lon]]; + if (i === 0) geometry.push(...edgePoly); + else geometry.push(...edgePoly.slice(1)); segments.push({ sequenceNo: i, segmentType: 'RAIL', startLat: la.lat, startLon: la.lon, @@ -465,6 +534,55 @@ : ((edge ? edge.distanceKm : 0) / veh.speedKmh * 60), }); } + distanceKm = dij.distanceKm; + provider = 'rail_dijkstra_polyline'; + } + } + } + + // Phase 8e+: TRUCK_* nutzt Straßennetz-Dijkstra — fährt via sinnvolle + // Zwischenstationen (z.B. Salzburg→Berlin via München) statt Luftlinie. + // Option C: jede Kante kann eine hand-kuratierte Polyline haben, die die + // echte Autobahn-Fuehrung abbildet (keine Knotengerade mehr). + if (!geometry && mode !== 'TRAIN') { + const road = (game.worldState && game.worldState.roadnet) || { nodes: [], edges: [] }; + const roadNode = (locId) => road.nodes.find(n => n.locationId === locId); + if (road.nodes.length && roadNode(originId) && roadNode(targetId)) { + const dij = railShortestPath( + { nodes: road.nodes, edges: road.edges }, + roadNode(originId).id, roadNode(targetId).id + ); + if (dij && dij.path && dij.distanceKm) { + geometry = []; + segments = []; + for (let i = 0; i < dij.path.length - 1; i++) { + const a = road.nodes.find(n => n.id === dij.path[i]); + const b = road.nodes.find(n => n.id === dij.path[i + 1]); + const edge = road.edges.find(e => + (e.from === a.id && e.to === b.id) || + (e.from === b.id && e.to === a.id)); + const la = locs.find(l => l.id === a.locationId); + const lb = locs.find(l => l.id === b.locationId); + // Polyline in Richtung a→b holen. Kante kann andersherum gespeichert sein. + let edgePoly = edge && edge.polyline ? edge.polyline.slice() : null; + if (edgePoly && edge.from === b.id && edge.to === a.id) edgePoly.reverse(); + if (!edgePoly) edgePoly = [[la.lat, la.lon], [lb.lat, lb.lon]]; + // Fuer die erste Kante: gesamte Polyline. Fuer Folgende: ohne + // ersten Punkt (Endpunkt der vorigen Kante), um Duplikate zu + // vermeiden. + if (i === 0) geometry.push(...edgePoly); + else geometry.push(...edgePoly.slice(1)); + segments.push({ + sequenceNo: i, segmentType: 'ROAD', + startLat: la.lat, startLon: la.lon, + endLat: lb.lat, endLon: lb.lon, + distanceKm: edge ? edge.distanceKm : 0, + durationMinutes: (edge ? edge.distanceKm : 0) / veh.speedKmh * 60, + routeCode: edge && edge.routeCode, + }); + } + distanceKm = dij.distanceKm; + provider = 'road_dijkstra_polyline'; } } } @@ -495,7 +613,7 @@ } const durationMinutes = (distanceKm / veh.speedKmh) * 60; return { - id: 'route_' + originId + '_' + targetId + '_' + mode + '_' + Date.now(), + id: 'route_' + originId + '_' + targetId + '_' + mode + '_' + _nextUniqId(game), originLocationId: originId, targetLocationId: targetId, mode, @@ -596,10 +714,8 @@ v.tripPhase = 'loading'; v.activeRouteId = null; v.state = VEHICLE_STATE.LOADING; - // Phase 6c: Minigame-Modifier wirkt auf nächste LOADING (dann reset) - const mgFactor = game._loadingTimeModifier || 1; - v.phaseRemainingMinutes = loadMin * mgFactor; - if (mgFactor !== 1) game._loadingTimeModifier = 1; + v.phaseRemainingMinutes = loadMin; + _consumeNextLoadBonus(game, v); // Phase 8e++: Ladebonus-Queue-Konsum if (leg) leg.state = CONTRACT_STATE.LOADING; else c.state = CONTRACT_STATE.LOADING; } @@ -623,19 +739,199 @@ * @private */ function _updateVehicles(game, simMinutes) { + // Evt. Verfahren-Event pro Fahrzeug auswuerfeln, bevor die Bewegung passiert + _maybeTriggerVerfahren(game, simMinutes); (game.vehicles || []).forEach(v => { - // Pro Vehicle: in einer Schleife alle Phasen abarbeiten, falls die - // Phase mitten im Tick endet (sonst Drift bei großen Ticks). + // Fahrzeug steht still, wenn „verfahren" ungeloest ist + if (v.verfahrenPending) return; + // Pflicht-Pause (ab L2): Fahrzeug steht, bis die Pausenzeit abgelaufen ist + if (v.onMandatoryBreak) { + v.breakRemainingMin = Math.max(0, (v.breakRemainingMin || 0) - simMinutes); + if (v.breakRemainingMin <= 0) { + v.onMandatoryBreak = false; + v.breakRemainingMin = 0; + v.drivingMinutesSinceBreak = 0; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'break_end_' + v.id + '_' + Date.now(), + type: 'BREAK_ENDED', + vehicleId: v.id, + messageShort: '☕ ' + (v.displayName || 'LKW') + ': Pause vorbei, Fahrt geht weiter.', + simTime: game.simulationTime, + }); + } + return; + } let remaining = simMinutes; - let safety = 8; // max 8 Phasen pro Tick (4 Phasen × Sicherheits-Reserve) + let safety = 8; while (remaining > 0 && v.tripPhase && safety-- > 0) { const consumed = _stepVehiclePhase(game, v, remaining); - if (consumed <= 0) break; // Phase noch aktiv, kein Übertrag + if (consumed <= 0) break; remaining -= consumed; + // Pflicht-Pausen-Akkumulation pro Fahrminute (ab L2+) + if (consumed > 0 && _isDrivingPhase(v.tripPhase)) { + _accumulateDriving(game, v, consumed); + if (v.onMandatoryBreak) break; // sofort Pause beginnen, Rest verwerfen + } } }); } + function _isDrivingPhase(phase) { + return phase === 'pickup_drive' || phase === 'delivery_drive' || phase === 'tour_driving'; + } + + /** Pflicht-Pause-Logik: Ab L2 zaehlt jede Fahrminute. Bei 270 min (4,5 h) + * ohne Pause wird eine 45-min-Pflichtpause ausgeloest. Auf L1 (nur ein + * Auftrag parallel) ist das aus, weil die Lernkurve dort nicht damit + * belastet werden soll. */ + const MANDATORY_DRIVE_LIMIT_MIN = 270; // 4,5 h + const MANDATORY_BREAK_MIN = 45; // EU-Regel (grob) + function _accumulateDriving(game, v, minutes) { + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) <= 1) return; // L1 → keine Pflichtpause + v.drivingMinutesSinceBreak = (v.drivingMinutesSinceBreak || 0) + minutes; + if (v.drivingMinutesSinceBreak < MANDATORY_DRIVE_LIMIT_MIN) return; + v.onMandatoryBreak = true; + v.breakRemainingMin = MANDATORY_BREAK_MIN; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'break_start_' + v.id + '_' + Date.now(), + type: 'BREAK_STARTED', + vehicleId: v.id, + messageShort: '☕ ' + (v.displayName || 'LKW') + ': Lenkzeit-Pause — 45 min Pause.', + simTime: game.simulationTime, + }); + } + + /** Verfahren-Ereignis: mit kleiner Wahrscheinlichkeit pro Sim-Stunde + * gerät ein Fahrzeug im `*_drive`-Phase vom Weg ab. Fahrt pausiert, + * der Spieler muss auf der Karte den Zielort anklicken. Alternierend + * „Fahrerin" / „Fahrer" aus `game.progression.verfahrenCount`. + * Didaktisch: Kartenorientierung uebt. + * Nur auf L1 ab Stufe 2 und nur mit kleiner Rate. */ + function _maybeTriggerVerfahren(game, simMinutes) { + const cfg = game._config || {}; + const isL1 = (cfg.maxActiveContracts || 1) === 1; + const isL2New = (cfg.maxActiveContracts || 1) > 1 && cfg.railEnabled && !cfg.intermodalEnabled; + if (!isL1 && !isL2New) return; + const round = (game.progression && game.progression.round) || 1; + if (isL1) { + const stufe = _l1StufeCfg(game); + if (!stufe || stufe.n < 2) return; + } else { + // L2: ab Runde 2 (nach erster Lieferung). Gleiche Rate wie L1. + if (round < 2) return; + } + // Basisrate 6% pro Sim-Stunde und Fahrzeug im *_drive + const rate = 0.06 * (simMinutes / 60); + (game.vehicles || []).forEach(v => { + if (v.verfahrenPending) return; + if (v.tripPhase !== 'pickup_drive' && v.tripPhase !== 'delivery_drive' && v.tripPhase !== 'tour_driving') return; + // Thomas-Regel (2026-04-25): Verfahren maximal alle 4 Fahrten pro + // Fahrzeug. Wir zaehlen gelieferte Kontrakte dieses Fahrzeugs und + // erlauben einen neuen Verfahren-Wuerfel nur, wenn seit dem letzten + // Ereignis mindestens 3 Zustellungen durch sind. + const toursDone = (game.completedContracts || []) + .filter(c => c.assignedVehicleId === v.id).length; + const lastAt = (typeof v.lastVerfahrenAtTour === 'number') ? v.lastVerfahrenAtTour : -999; + if ((toursDone - lastAt) < 3) return; + if (!game._verfahrenRngCalls) game._verfahrenRngCalls = 0; + const rng = _makeRng((game.seed || 1) + 77777); + for (let i = 0; i < game._verfahrenRngCalls; i++) rng(); + const roll = rng(); + game._verfahrenRngCalls++; + if (roll >= rate) return; + // Ziel: aktueller Trip-Zielort bestimmen + let targetLocationId = null; + if (v.tripPhase === 'pickup_drive') { + const c = (game.activeContracts || []).find(x => x.id === v.assignedContractId); + if (c) targetLocationId = c.originLocationId; + } else if (v.tripPhase === 'delivery_drive') { + const c = (game.activeContracts || []).find(x => x.id === v.assignedContractId); + if (c) targetLocationId = c.targetLocationId; + } else if (v.tripPhase === 'tour_driving') { + const stop = v.tourStops && v.tourStops[v.currentStopIndex]; + if (stop) targetLocationId = stop.locationId; + } + if (!targetLocationId) return; + v.verfahrenPending = true; + v.verfahrenTargetLocationId = targetLocationId; + v.lastVerfahrenAtTour = toursDone; // Cooldown-Zaehler merken + // Fahrer-Label ist jetzt fest pro Fahrzeug (driverLabel beim Spawn). + // Fallback fuer alte Spielstaende ohne driverLabel. + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + const driverType = v.driverLabel + || ((game.progression.verfahrenCount || 0) % 2 === 0 ? 'Fahrerin Anna' : 'Fahrer Tom'); + game.progression.verfahrenCount = (game.progression.verfahrenCount || 0) + 1; + v.verfahrenDriverLabel = driverType; + // Notification fuer UI + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'verfahren_' + v.id + '_' + _nextUniqId(game), + type: 'DRIVER_LOST', + vehicleId: v.id, + targetLocationId: targetLocationId, + driverLabel: driverType, + messageShort: '🤷 ' + driverType + ' hat sich verfahren — Ziel auf der Karte zeigen.', + simTime: game.simulationTime, + }); + }); + } + + /** Der Spieler klickt auf einen Ort. 3 moegliche Auswertungen: + * - Falsche Stadt → 1,5 Sim-Stunden Zeit-Strafe akkumuliert, Event bleibt offen + * - Richtige Stadt → Event geloest, akkumulierte Strafe in segmentProgress abgezogen + * Jede Stadt mit Marker zaehlt als Tipp (nicht mehr nur Hauptstaedte). + * Return: { ok, wrong?, wastedHours?, totalLostHours?, reason? } */ + const VERFAHREN_PENALTY_H = 1.5; // Sim-Stunden pro falschem Tipp + function resolveDriverLost(game, vehicleId, clickedLocationId, locationType) { + const v = (game.vehicles || []).find(x => x.id === vehicleId); + if (!v || !v.verfahrenPending) return { ok: false, reason: 'kein Ereignis aktiv' }; + // Korrekter Tipp + if (clickedLocationId === v.verfahrenTargetLocationId) { + const totalPenalty = v.verfahrenPenaltyHours || 0; + // Akkumulierte Zeit-Strafe durch Reduktion des segmentProgress umsetzen, + // sodass die Restfahrt entsprechend laenger dauert. Pro Fahrt max 90 % + // des Fortschritts zuruecknehmen (realistisches Limit). + if (totalPenalty > 0) { + const route = (game.activeRoutes || []).find(r => r.id === v.activeRouteId); + if (route && route.totalDurationMinutes > 0) { + const wastedRatio = Math.min(0.9, (totalPenalty * 60) / route.totalDurationMinutes); + v.segmentProgress = Math.max(0, (v.segmentProgress || 0) - wastedRatio); + } + } + v.verfahrenPending = false; + v.verfahrenTargetLocationId = null; + v.verfahrenDriverLabel = null; + v.verfahrenPenaltyHours = 0; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'verfahren_resolved_' + v.id + '_' + Date.now(), + type: 'DRIVER_FOUND', + vehicleId: v.id, + totalLostHours: totalPenalty, + messageShort: totalPenalty > 0 + ? '✓ Ziel gezeigt — ' + totalPenalty.toFixed(1) + ' h durch falsche Tipps verloren.' + : '✓ Ziel gezeigt — Fahrt geht weiter.', + simTime: game.simulationTime, + }); + return { ok: true, totalLostHours: totalPenalty }; + } + // Falsche Hauptstadt: Penalty akkumulieren, Event bleibt offen + v.verfahrenPenaltyHours = (v.verfahrenPenaltyHours || 0) + VERFAHREN_PENALTY_H; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'verfahren_wrong_' + v.id + '_' + Date.now(), + type: 'DRIVER_WRONG_TIP', + vehicleId: v.id, + wastedHours: VERFAHREN_PENALTY_H, + messageShort: 'Dein Tipp war falsch — ' + VERFAHREN_PENALTY_H + ' h verloren.', + simTime: game.simulationTime, + }); + return { ok: false, wrong: true, wastedHours: VERFAHREN_PENALTY_H }; + } + /** * Verarbeitet EINEN Phasen-Schritt; gibt zurück, wie viele Sim-Minuten * verbraucht wurden. Wenn Phase fertig vor simMinutes-Ende: konsumiert @@ -683,12 +979,8 @@ v.segmentProgress = 0; v.state = stop.type === 'PICKUP' ? VEHICLE_STATE.LOADING : VEHICLE_STATE.UNLOADING; let handlingMinutes = stop.handlingMinutes || 5; - if (stop.type === 'PICKUP') { - const mgFactor = game._loadingTimeModifier || 1; - handlingMinutes *= mgFactor; - if (mgFactor !== 1) game._loadingTimeModifier = 1; - } v.phaseRemainingMinutes = handlingMinutes; + _consumeNextLoadBonus(game, v); // gilt fuer Pickup UND Dropoff stop.contractIds.forEach(cid => { const c = (game.activeContracts || []).find(x => x.id === cid); if (c) c.state = stop.type === 'PICKUP' ? CONTRACT_STATE.LOADING : CONTRACT_STATE.UNLOADING; @@ -728,12 +1020,8 @@ v.activeRouteId = null; v.state = nextStop.type === 'PICKUP' ? VEHICLE_STATE.LOADING : VEHICLE_STATE.UNLOADING; let m = nextStop.handlingMinutes || 5; - if (nextStop.type === 'PICKUP') { - const mgFactor = game._loadingTimeModifier || 1; - m *= mgFactor; - if (mgFactor !== 1) game._loadingTimeModifier = 1; - } v.phaseRemainingMinutes = m; + _consumeNextLoadBonus(game, v); nextStop.contractIds.forEach(cid => { const c = (game.activeContracts || []).find(x => x.id === cid); if (c) c.state = nextStop.type === 'PICKUP' ? CONTRACT_STATE.LOADING : CONTRACT_STATE.UNLOADING; @@ -990,8 +1278,9 @@ routeDistanceKm: Math.round((totalKm || 0) + (contract.pickupKm || 0)), routeMode: route && route.mode, }); + // Progression-Runde +1 (zaehlt fuer L1 dynamic max + Auto-Buy) + if (game.progression) game.progression.round = (game.progression.round || 1) + 1; // Phase-2-Vorgriff (für „L1 spielbar"): bei { - cost += rentalPerDay * simDays; + // Per-Vehicle Mietmultiplikator (Train teurer als Truck). + const veh = VEHICLE_DEFAULTS[v.mode] || {}; + const mult = (typeof veh.rentMult === 'number') ? veh.rentMult : 1.0; + costRental += rentalPerDay * simDays * mult; if (v.state === VEHICLE_STATE.IDLE) { - cost += idleCostPerH * simHours; + costIdle += idleCostPerH * simHours; } }); // Phase 4: Container-Standkosten an Häfen @@ -1023,13 +1317,17 @@ if (c.state !== CONTRACT_STATE.OPEN) return; const orig = locs.find(l => l.id === c.originLocationId); if (!orig || orig.type !== 'PORT') return; - cost += portStandPerH * (c.quantityContainers || 1) * simHours; + costPort += portStandPerH * (c.quantityContainers || 1) * simHours; }); } + const cost = costRental + costIdle + costPort; if (cost > 0) { game.balance -= cost; if (!game.analytics) game.analytics = {}; - game.analytics.runningCostsTotal = (game.analytics.runningCostsTotal || 0) + cost; + game.analytics.runningCostsTotal = (game.analytics.runningCostsTotal || 0) + cost; + game.analytics.runningCostsRental = (game.analytics.runningCostsRental || 0) + costRental; + game.analytics.runningCostsIdle = (game.analytics.runningCostsIdle || 0) + costIdle; + game.analytics.runningCostsPort = (game.analytics.runningCostsPort || 0) + costPort; } } @@ -1042,8 +1340,11 @@ */ function _maybeGenerateEvents(game, simMinutes) { const cfg = game._config || {}; - const probMul = (typeof cfg.eventProbabilityMultiplier === 'number') + let probMul = (typeof cfg.eventProbabilityMultiplier === 'number') ? cfg.eventProbabilityMultiplier : 1; + // L1-Event-Ramp: Stufe 1-5 = 0 (deterministisch), Stufe 6 = stufen-override + const stufe = _l1StufeCfg(game); + if (stufe) probMul = stufe.eventMult; if (probMul <= 0) return; if (!game._eventsRngCalls) game._eventsRngCalls = 0; if (!game.activeEvents) game.activeEvents = []; @@ -1341,9 +1642,8 @@ v.activeRouteId = null; v.segmentProgress = 0; v.state = firstStop.type === 'PICKUP' ? VEHICLE_STATE.LOADING : VEHICLE_STATE.UNLOADING; - const mgFactor = (firstStop.type === 'PICKUP' ? (game._loadingTimeModifier || 1) : 1); - v.phaseRemainingMinutes = firstStop.handlingMinutes * mgFactor; - if (firstStop.type === 'PICKUP' && mgFactor !== 1) game._loadingTimeModifier = 1; + v.phaseRemainingMinutes = firstStop.handlingMinutes; + _consumeNextLoadBonus(game, v); firstStop.contractIds.forEach(cid => { const c = (game.activeContracts || []).find(x => x.id === cid); if (c) c.state = firstStop.type === 'PICKUP' ? CONTRACT_STATE.LOADING : CONTRACT_STATE.UNLOADING; @@ -1473,6 +1773,7 @@ routeDistanceKm: contract.plannedRouteKm || null, routeMode: vehicle && vehicle.typeId, }); + if (game.progression) game.progression.round = (game.progression.round || 1) + 1; _maybeGenerateFollowupContract(game); } @@ -1505,27 +1806,599 @@ }); } + /** Effektives maxActiveContracts mit L1-Progression. + * L1: nimmt stufe.maxOpen. L2/L3: cfg-Wert direkt. */ + function _effectiveMaxContracts(game) { + const cfg = game._config || {}; + const cfgMax = cfg.maxActiveContracts || 1; + if (cfgMax !== 1) return cfgMax; + const stufe = _l1StufeCfg(game); + return (stufe && stufe.maxOpen) || 1; + } + + /** L1-Stufen-System (nur fuer cfg.max=1). Einfaches Modell: + * Pro Stufe waechst max gleichzeitiger OPEN-Auftraege + Pool-Distanz. + * Aufträge sind immer Standard (kein Eilauftrag, keine Multi-Container + * — Tutorial soll entspannt sein). Sie erscheinen direkt als OPEN. + * Kommen und gehen: nicht-zugewiesene Auftraege verfallen an ihrer + * eigenen dueTime (silent-remove). */ + const L1_STUFEN = Object.freeze([ + null, + { n: 1, name: 'Erster Auftrag', minRound: 1, maxOpen: 1, poolMaxMult: 1.0, eventMult: 0 }, + { n: 2, name: 'Mehrere zur Wahl', minRound: 2, maxOpen: 3, poolMaxMult: 1.2, eventMult: 0 }, + { n: 3, name: 'Weitere Strecken', minRound: 4, maxOpen: 4, poolMaxMult: 1.5, eventMult: 0 }, + { n: 4, name: 'Mehr Fahrzeuge', minRound: 7, maxOpen: 4, poolMaxMult: 1.7, eventMult: 0 }, + { n: 5, name: 'Soft-Ramp → L2', minRound: 11, maxOpen: 4, poolMaxMult: 1.8, eventMult: 0.3 }, + ]); + /** Gibt Stufen-Config fuer aktuelles Game zurueck (nur L1, sonst null). */ + function _l1StufeCfg(game) { + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) !== 1) return null; + const round = (game.progression && game.progression.round) || 1; + let pick = L1_STUFEN[1]; + for (let i = 1; i < L1_STUFEN.length; i++) { + if (L1_STUFEN[i] && round >= L1_STUFEN[i].minRound) pick = L1_STUFEN[i]; + } + return pick; + } + /** Kurzform nur Stufen-Nummer. */ + function _l1Stufe(game) { + const s = _l1StufeCfg(game); + return s ? s.n : 0; + } + + /** Nimmt ein OFFERED-Angebot an: wird zu OPEN, Expiry geloescht. */ + function acceptOffer(game, contractId) { + const c = (game.activeContracts || []).find(x => x.id === contractId); + if (!c) throw new Error('Contract nicht gefunden: ' + contractId); + if (c.state !== CONTRACT_STATE.OFFERED) { + throw new Error('Contract ' + contractId + ' ist nicht OFFERED (state=' + c.state + ')'); + } + // Pruefen: gibt es noch Platz fuer einen weiteren OPEN? + const openCount = (game.activeContracts || []).filter(x => x.state === CONTRACT_STATE.OPEN).length; + if (openCount >= _effectiveMaxContracts(game)) { + throw new Error('Maximale parallele Auftraege erreicht (' + openCount + ')'); + } + c.state = CONTRACT_STATE.OPEN; + c.offerExpiryTime = null; + return c; + } + + /** Lehnt ein OFFERED-Angebot ab: wird aus activeContracts entfernt. */ + function declineOffer(game, contractId) { + const before = (game.activeContracts || []).length; + game.activeContracts = (game.activeContracts || []).filter(c => { + return !(c.id === contractId && c.state === CONTRACT_STATE.OFFERED); + }); + return (game.activeContracts || []).length < before; + } + + /** L2-Auftragspool (Neuanfang 2026-04-25) — Klon des L1-Pools, aber + * ergaenzt um 2-Container-Auftraege auf passenden Strecken. So lohnt + * der TRUCK_MID (Cap 2) durch Buendelung. Container-Range wird in + * _generateL2ContractFromPool ausgewuerfelt. + * tier 1 = ab Runde 1 (Einstiegsrouten DACH+Brenner+Berlin) + * tier 2 = ab Runde 3 (ganz Europa, inkl. Häfen) */ + const L2_OFFER_POOL = [ + // ── Tier 1: DACH + Mailand + Berlin (wie L1) ── + { origin: 'salzburg', target: 'muenchen', mult: 0.7, cont: [1, 1], tier: 1 }, + { origin: 'muenchen', target: 'salzburg', mult: 0.7, cont: [1, 1], tier: 1 }, + { origin: 'wien', target: 'graz', mult: 0.8, cont: [1, 1], tier: 1 }, + { origin: 'graz', target: 'wien', mult: 0.8, cont: [1, 1], tier: 1 }, + { origin: 'wien', target: 'salzburg', mult: 1.0, cont: [1, 1], tier: 1 }, + { origin: 'salzburg', target: 'wien', mult: 1.0, cont: [1, 1], tier: 1 }, + { origin: 'muenchen', target: 'innsbruck', mult: 0.7, cont: [1, 1], tier: 1 }, + { origin: 'innsbruck',target: 'muenchen', mult: 0.7, cont: [1, 1], tier: 1 }, + { origin: 'wien', target: 'muenchen', mult: 1.3, cont: [1, 2], tier: 1 }, + { origin: 'muenchen', target: 'wien', mult: 1.3, cont: [1, 2], tier: 1 }, + { origin: 'salzburg', target: 'graz', mult: 1.0, cont: [1, 2], tier: 1 }, + { origin: 'graz', target: 'salzburg', mult: 1.0, cont: [1, 2], tier: 1 }, + { origin: 'innsbruck',target: 'salzburg', mult: 0.8, cont: [1, 2], tier: 1 }, + { origin: 'salzburg', target: 'innsbruck', mult: 0.8, cont: [1, 2], tier: 1 }, + { origin: 'muenchen', target: 'mailand', mult: 1.2, cont: [1, 2], tier: 1 }, + { origin: 'mailand', target: 'muenchen', mult: 1.2, cont: [1, 2], tier: 1 }, + { origin: 'innsbruck',target: 'mailand', mult: 1.1, cont: [1, 2], tier: 1 }, + { origin: 'mailand', target: 'innsbruck', mult: 1.1, cont: [1, 2], tier: 1 }, + { origin: 'salzburg', target: 'mailand', mult: 1.4, cont: [1, 2], tier: 1 }, + { origin: 'mailand', target: 'salzburg', mult: 1.4, cont: [1, 2], tier: 1 }, + { origin: 'muenchen', target: 'berlin', mult: 1.5, cont: [1, 2], tier: 1 }, + { origin: 'berlin', target: 'muenchen', mult: 1.5, cont: [1, 2], tier: 1 }, + { origin: 'wien', target: 'berlin', mult: 1.7, cont: [1, 2], tier: 1 }, + { origin: 'berlin', target: 'wien', mult: 1.7, cont: [1, 2], tier: 1 }, + + // ── Tier 2: ab Runde 3, ganz Europa und Häfen (NEU 2026-04-25) ── + // Frankreich + { origin: 'paris', target: 'rotterdam', mult: 1.5, cont: [1, 2], tier: 2 }, + { origin: 'rotterdam',target: 'paris', mult: 1.5, cont: [1, 2], tier: 2 }, + { origin: 'paris', target: 'mailand', mult: 2.0, cont: [1, 2], tier: 2 }, + { origin: 'paris', target: 'madrid', mult: 2.5, cont: [1, 2], tier: 2 }, + { origin: 'madrid', target: 'paris', mult: 2.5, cont: [1, 2], tier: 2 }, + // Deutschland-Nord + Skandinavien + { origin: 'hamburg', target: 'rotterdam', mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'rotterdam',target: 'hamburg', mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'berlin', target: 'kopenhagen',mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'kopenhagen',target:'berlin', mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'berlin', target: 'warschau', mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'warschau', target: 'berlin', mult: 1.4, cont: [1, 2], tier: 2 }, + // Häfen-Anbindung — Stadt zum Hafen (Luftlinie wenn Hafen nicht in Roadnet) + { origin: 'hamburg', target: 'hamburg_hafen', mult: 0.5, cont: [1, 2], tier: 2 }, + { origin: 'rotterdam',target: 'rotterdam_hafen', mult: 0.5, cont: [1, 2], tier: 2 }, + { origin: 'wien', target: 'triest_hafen', mult: 1.4, cont: [1, 2], tier: 2 }, + { origin: 'muenchen', target: 'genua_hafen', mult: 1.6, cont: [1, 2], tier: 2 }, + { origin: 'mailand', target: 'genua_hafen', mult: 0.8, cont: [1, 2], tier: 2 }, + { origin: 'hamburg', target: 'goeteborg_hafen', mult: 1.5, cont: [1, 2], tier: 2 }, + { origin: 'warschau', target: 'gdansk_hafen', mult: 1.0, cont: [1, 2], tier: 2 }, + // Rückrichtungen Hafen → Stadt + { origin: 'hamburg_hafen', target: 'berlin', mult: 1.0, cont: [1, 2], tier: 2 }, + { origin: 'rotterdam_hafen', target: 'paris', mult: 1.5, cont: [1, 2], tier: 2 }, + ]; + + /** Train-spezifische Auftraege (4-6 Container, lange Strecken). + * Train startet in Hamburg, daher viele hamburg-Routen + Rueckwege. */ + const L2_TRAIN_POOL = [ + // Hamburg-Hub (Train-Spawn) + { origin: 'hamburg', target: 'rotterdam', mult: 1.4, cont: [5, 6] }, + { origin: 'rotterdam',target: 'hamburg', mult: 1.4, cont: [5, 6] }, + { origin: 'hamburg', target: 'berlin', mult: 1.2, cont: [4, 6] }, + { origin: 'berlin', target: 'hamburg', mult: 1.2, cont: [4, 6] }, + { origin: 'hamburg', target: 'muenchen', mult: 1.7, cont: [5, 6] }, + { origin: 'muenchen', target: 'hamburg', mult: 1.7, cont: [5, 6] }, + // Lange DACH-Achse (Train-tauglich) + { origin: 'wien', target: 'muenchen', mult: 1.3, cont: [4, 6] }, + { origin: 'muenchen', target: 'mailand', mult: 1.3, cont: [4, 6] }, + { origin: 'wien', target: 'mailand', mult: 1.7, cont: [4, 6] }, + { origin: 'muenchen', target: 'berlin', mult: 1.5, cont: [4, 6] }, + { origin: 'wien', target: 'berlin', mult: 1.7, cont: [4, 6] }, + ]; + + /** Generiert einen L2-Auftrag aus dem Pool. Container-Anzahl per cont-Range. + * Tier-Filter: tier 1 von Anfang an, tier 2 ab Runde 3. + * Train-Wave (Thomas-Regel): wenn ein TRAIN_SMALL gemietet ist, kommt + * pro 3 generierte Aufträge genau 1 train-tauglicher (4-6 Container, + * lange Strecke). Letzte 24 h Sim-Zeit: keine Train-Aufträge mehr — + * der Spieler muss seine Train-Investition rechtzeitig amortisieren. */ + function _generateL2ContractFromPool(game, code, alreadyOffered) { + const usedOrigins = new Set(alreadyOffered.map(c => c.originLocationId)); + const vehicleLocs = new Set((game.vehicles || []).map(v => v.currentLocationId)); + const round = (game.progression && game.progression.round) || 1; + const maxTier = round >= 3 ? 2 : 1; + const cfg = game._config || {}; + const trainActive = (game.vehicles || []).some(v => v.mode === 'TRAIN_SMALL' || v.mode === 'TRAIN'); + const elapsedH = game.simulationStartTime + ? (new Date(game.simulationTime).getTime() - new Date(game.simulationStartTime).getTime()) / 3600000 + : 0; + const limitH = cfg.timeLimitHours || 144; + const isLastDay = (limitH - elapsedH) < 24; + + // Train-Wave: every 3rd train-tauglich, ausser im letzten Tag. + // Routen vom aktuellen Train-Standort werden bevorzugt (sonst muss + // der Train erst eine Leerfahrt zum Pickup machen). + if (trainActive && !isLastDay) { + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + const cnt = game.progression.trainWaveCounter = (game.progression.trainWaveCounter || 0) + 1; + if (cnt % 3 === 1) { // jeder 3., beginnend mit dem 1. nach Anmietung + const train = (game.vehicles || []).find(v => v.mode === 'TRAIN_SMALL' || v.mode === 'TRAIN'); + const trainLoc = train ? train.currentLocationId : 'hamburg'; + const direct = L2_TRAIN_POOL.filter(p => p.origin === trainLoc); + const trainPick = (direct.length && _progressionRng(game) < 0.7) + ? direct[Math.floor(_progressionRng(game) * direct.length)] + : L2_TRAIN_POOL[Math.floor(_progressionRng(game) * L2_TRAIN_POOL.length)]; + const [tcMin, tcMax] = trainPick.cont || [4, 6]; + const trainContainers = tcMin + Math.floor(_progressionRng(game) * (tcMax - tcMin + 1)); + const tc = _makeContract(game, code, trainPick.origin, trainPick.target, trainContainers, false); + tc.rewardBase = Math.round(tc.rewardBase * trainPick.mult); + tc.trainEligible = true; + return tc; + } + } + + // Mindest-Quote (Thomas 2026-04-26): immer >= 2 OPEN mit nur 1 Container + // (Paketcontainer fuer den TRUCK_SMALL). Wenn weniger als 2 offen sind, + // erzwinge der naechste Auftrag muss 1-Container sein. + const oneContainerCount = (alreadyOffered || []) + .filter(c => c.state === 'OPEN' && (c.quantityContainers || 1) === 1).length; + const forceSingle = oneContainerCount < 2; + + let pool = L2_OFFER_POOL.filter(p => (p.tier || 1) <= maxTier); + if (forceSingle) { + // Nur Templates mit cont-Range, der 1 enthaelt + pool = pool.filter(p => (p.cont || [1, 1])[0] === 1); + } + // Bevorzuge nicht-genutzte Origins fuer Vielfalt + let candidates = pool.filter(p => !usedOrigins.has(p.origin)); + if (!candidates.length) candidates = pool; + const direct = candidates.filter(p => vehicleLocs.has(p.origin)); + const away = candidates.filter(p => !vehicleLocs.has(p.origin)); + let pick; + if (direct.length && _progressionRng(game) < 0.7) { + pick = direct[Math.floor(_progressionRng(game) * direct.length)]; + } else if (away.length) { + pick = away[Math.floor(_progressionRng(game) * away.length)]; + } else { + pick = candidates[Math.floor(_progressionRng(game) * candidates.length)]; + } + const [cMin, cMax] = pick.cont || [1, 1]; + // Bei forceSingle Container-Wuerfel auf 1 zwingen (Mindest-Quote 1-Cont) + const containers = forceSingle ? 1 + : cMin + Math.floor(_progressionRng(game) * (cMax - cMin + 1)); + const c = _makeContract(game, code, pick.origin, pick.target, containers, false); + c.rewardBase = Math.round(c.rewardBase * pick.mult); + if (!vehicleLocs.has(pick.origin)) c.awayBonus = true; + return c; + } + + /** L1-Auftragspool — bewusst begrenzt auf DACH-Raum bis Mailand/Berlin. + * ALLE Staedte sind Roadnet-Knoten, d.h. die Fahrzeuge nutzen echte + * Autobahn-Polylines (keine Luftlinie-Fallbacks). Reward-Mult + * kompensiert zusaetzliche Distanz. Richtung spielt keine Rolle — jede + * Route existiert in beiden Richtungen. */ + const L1_OFFER_POOL = [ + // Stufe 1: sehr kurze Einstiegs-Touren (mult <= 1.0) + { origin: 'salzburg', target: 'muenchen', mult: 0.7 }, // 143 km + { origin: 'muenchen', target: 'salzburg', mult: 0.7 }, + { origin: 'muenchen', target: 'innsbruck', mult: 0.7 }, // 163 km + { origin: 'innsbruck',target: 'muenchen', mult: 0.7 }, + { origin: 'innsbruck',target: 'salzburg', mult: 0.8 }, // 186 km + { origin: 'salzburg', target: 'innsbruck', mult: 0.8 }, + { origin: 'wien', target: 'graz', mult: 0.8 }, // 194 km + { origin: 'graz', target: 'wien', mult: 0.8 }, + { origin: 'salzburg', target: 'graz', mult: 1.0 }, // 278 km + { origin: 'graz', target: 'salzburg', mult: 1.0 }, + { origin: 'wien', target: 'salzburg', mult: 1.0 }, // 296 km, Klassiker + { origin: 'salzburg', target: 'wien', mult: 1.0 }, + + // Stufe 2: mittlere Distanzen (mult <= 1.2) + { origin: 'innsbruck',target: 'mailand', mult: 1.1 }, // 389 km Brenner + { origin: 'mailand', target: 'innsbruck', mult: 1.1 }, + { origin: 'wien', target: 'muenchen', mult: 1.2 }, // ~500 km (via Salzburg) + { origin: 'muenchen', target: 'wien', mult: 1.2 }, + { origin: 'muenchen', target: 'mailand', mult: 1.2 }, // 493 km Brenner + { origin: 'mailand', target: 'muenchen', mult: 1.2 }, + + // Stufe 3: weitere Strecken (mult <= 1.5) + { origin: 'salzburg', target: 'mailand', mult: 1.4 }, // ~640 km Brenner + { origin: 'mailand', target: 'salzburg', mult: 1.4 }, + { origin: 'muenchen', target: 'berlin', mult: 1.5 }, // 585 km + { origin: 'berlin', target: 'muenchen', mult: 1.5 }, + + // Stufe 4+: weite DACH-Nord-Sued-Achse (mult <= 1.7) + { origin: 'wien', target: 'berlin', mult: 1.7 }, // ~880 km via Salzburg+Muenchen + { origin: 'berlin', target: 'wien', mult: 1.7 }, + { origin: 'wien', target: 'mailand', mult: 1.8 }, // ~970 km via Innsbruck + { origin: 'mailand', target: 'wien', mult: 1.8 }, + ]; + + /** L1-Auftragsprofile — jedes Angebot bekommt ein Profil gewuerfelt, + * das Reward-Mult + Frist-Mult + ggf. Pool-Filter festlegt. Gibt dem + * Spieler echte Entscheidungen (lohnend vs. zeitlich riskant). */ + const L1_PROFILES = Object.freeze({ + standard: { weight: 50, rewardMult: 1.0, deadlineMult: 1.0 }, + tight: { weight: 20, rewardMult: 1.3, deadlineMult: 0.6 }, // mehr Geld, knappe Frist + far: { weight: 15, rewardMult: 1.0, deadlineMult: 1.0, minPoolMult: 1.5 }, // weit + Abstecher-Risiko + easy: { weight: 15, rewardMult: 0.85, deadlineMult: 1.3, maxPoolMult: 1.0 }, // kurz, wenig, schnell fertig + }); + function _pickL1Profile(game, stufeCfg) { + // Stufe 1: immer standard (Tutorial) + if (stufeCfg.n <= 1) return Object.assign({ name: 'standard' }, L1_PROFILES.standard); + const entries = Object.entries(L1_PROFILES); + const total = entries.reduce((s, [,p]) => s + p.weight, 0); + let r = _progressionRng(game) * total, cum = 0; + for (const [name, p] of entries) { + cum += p.weight; + if (r < cum) return Object.assign({ name }, p); + } + return Object.assign({ name: 'standard' }, L1_PROFILES.standard); + } + + /** Generiert einen L1-Auftrag aus dem Pool. Immer OPEN, immer 1 Container, + * nie Express — aber mit **Profil-Variance** (standard/tight/far/easy) fuer + * echte Entscheidungsvielfalt. „Kommen und gehen": Auftrag verfaellt still + * wenn seine `dueTime` ohne Zuweisung verstreicht. */ + function _generateL1ContractFromPool(game, code, alreadyOffered) { + const usedOrigins = new Set(alreadyOffered.map(c => c.originLocationId)); + const vehicleLocs = new Set((game.vehicles || []).map(v => v.currentLocationId)); + const stufeCfg = _l1StufeCfg(game) || L1_STUFEN[1]; + const profile = _pickL1Profile(game, stufeCfg); + + let pool = L1_OFFER_POOL.filter(p => p.mult <= stufeCfg.poolMaxMult); + if (profile.minPoolMult) pool = pool.filter(p => p.mult >= profile.minPoolMult); + if (profile.maxPoolMult) pool = pool.filter(p => p.mult <= profile.maxPoolMult); + if (!pool.length) pool = L1_OFFER_POOL.filter(p => p.mult <= stufeCfg.poolMaxMult); + + let candidates = pool.filter(p => !usedOrigins.has(p.origin)); + if (!candidates.length) candidates = pool; + const direct = candidates.filter(p => vehicleLocs.has(p.origin)); + const away = candidates.filter(p => !vehicleLocs.has(p.origin)); + let pick; + if (alreadyOffered.length === 0 && direct.length) { + pick = direct[Math.floor(_progressionRng(game) * direct.length)]; + } else if (away.length) { + pick = away[Math.floor(_progressionRng(game) * away.length)]; + } else { + pick = candidates[Math.floor(_progressionRng(game) * candidates.length)]; + } + const c = _makeContract(game, code, pick.origin, pick.target, 1, false); + c.rewardBase = Math.round(c.rewardBase * pick.mult * profile.rewardMult); + c.profile = profile.name; + if (!vehicleLocs.has(pick.origin)) c.awayBonus = true; + // Frist per Profil strecken/stauchen + if (profile.deadlineMult !== 1.0) { + const nowMs = new Date(game.simulationTime).getTime(); + const origDueMs = new Date(c.dueTime).getTime(); + const adjustedMs = nowMs + (origDueMs - nowMs) * profile.deadlineMult; + c.dueTime = new Date(adjustedMs).toISOString(); + } + return c; + } + + /** Deterministische Progression-RNG (verbraucht eigenen Counter). */ + function _progressionRng(game) { + if (!game._progressionRngCalls) game._progressionRngCalls = 0; + const rng = _makeRng((game.seed || 1) + 99991); + for (let i = 0; i < game._progressionRngCalls; i++) rng(); + const v = rng(); + game._progressionRngCalls++; + return v; + } + + /** Gesamt-Slot-Groesse: einfach = effektives max (kein Offers-Buffer mehr). */ + function _totalContractSlots(game) { + return _effectiveMaxContracts(game); + } + function _maybeGenerateFollowupContract(game) { const cfg = game._config || {}; - const max = cfg.maxActiveContracts || 1; - if ((game.activeContracts || []).length >= max) return; - const generated = (game.completedContracts || []).length + (game.activeContracts || []).length; - const code = 'C-' + String(generated + 1).padStart(3, '0'); - let follow; - if (max === 1) { - // L1: pendele Wien ↔ Salzburg (vermeidet Leerfahrten — didaktisch - // illustriert Pendelprinzip, hält naive im Akzeptanzkorridor) - const lastTarget = ((game.completedContracts || []).slice(-1)[0] || {}).targetLocationId; - const nextOrigin = lastTarget || 'wien'; - const nextTarget = nextOrigin === 'wien' ? 'salzburg' : 'wien'; - follow = _makeContract(game, code, nextOrigin, nextTarget, 1, false); - } else { - // L2/L3: variiert aus CONTRACT_TEMPLATES - const tpl = _nextContractTemplate(game); - follow = _makeContract(game, code, tpl.origin, tpl.target, tpl.containers, tpl.express); - follow.cargoTypeId = tpl.cargoTypeId; + const cfgMax = cfg.maxActiveContracts || 1; + const max = _effectiveMaxContracts(game); + // L2-Pfad (Neuanfang 2026-04-25): wie L1, aber eigener Pool mit + // 1- und 2-Container-Auftraegen. Auftraege koennen weiter generiert + // werden bis maxActiveContracts erreicht. Identifikator: maxActiveContracts + // > 1 UND cfg.railEnabled (signalisiert L2 vs. L3). + if (cfgMax > 1 && cfg.railEnabled && !cfg.intermodalEnabled) { + // Refill bis OPEN-Anzahl == cfgMax (Thomas 2026-04-26). + // RESERVED-Auftraege (im LKW geladen, aber noch nicht losgefahren) + // zaehlen NICHT zum Slot-Limit — das hielte sonst neue Auftraege ab. + let safety = 12; + while (safety-- > 0) { + const openCount = (game.activeContracts || []) + .filter(c => c.state === 'OPEN').length; + if (openCount >= cfgMax) break; + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + if (typeof game.progression.contractCounter !== 'number') { + game.progression.contractCounter = + (game.completedContracts || []).length + (game.activeContracts || []).length; + } + game.progression.contractCounter++; + const code = 'C-' + String(game.progression.contractCounter).padStart(3, '0'); + const follow = _generateL2ContractFromPool(game, code, game.activeContracts || []); + game.activeContracts.push(follow); + _validateUniqueIds(game); + } + return; } - game.activeContracts.push(follow); + // Legacy-Pfad fuer altes komplexes L2/L3 (Templates-basiert) + if (cfgMax !== 1) { + if ((game.activeContracts || []).length >= max) return; + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + if (typeof game.progression.contractCounter !== 'number') { + game.progression.contractCounter = + (game.completedContracts || []).length + (game.activeContracts || []).length; + } + game.progression.contractCounter++; + const code = 'C-' + String(game.progression.contractCounter).padStart(3, '0'); + const tpl = _nextContractTemplate(game); + const follow = _makeContract(game, code, tpl.origin, tpl.target, tpl.containers, tpl.express); + follow.cargoTypeId = tpl.cargoTypeId; + game.activeContracts.push(follow); + _validateUniqueIds(game); + return; + } + + // L1: fuelle OPEN+OFFERED bis total-slots + _maybeRefillOffers(game); + // Auto-Buy nach jeder Generation pruefen + _maybeAutoBuyVehicle(game); + } + + /** Fuellt OFFERED-Angebote + OPEN-Slots bis totalContractSlots erreicht ist. + * Auf Stufe 1 (offersBuffer=0) fuellt nur OPEN-Slots. */ + function _maybeRefillOffers(game) { + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) !== 1) return; // Nur L1 + const total = _totalContractSlots(game); + const stufe = _l1StufeCfg(game) || {}; + // Thomas-Regel (2026-04-25): mit 25% Wahrscheinlichkeit einen bereits + // offenen Auftrag als Bonus-Zwilling mit +50% Reward anbieten. So hat + // der Spieler eine echte Wahl: Standard nehmen oder auf den Bonus warten + // und die lukrative Variante erwischen. Nur ab Stufe 2 (Tutorial- + // freundlich). + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + // Bug-Fix (2026-04-25): contractCounter darf NIEMALS zurueckgehen, + // sonst kollidieren Codes/IDs zwischen ausgelieferten und neuen Auftraegen. + // Initial-Sync auf bereits vorhandene Aufträge, falls Counter fehlt. + if (typeof game.progression.contractCounter !== 'number') { + game.progression.contractCounter = + (game.completedContracts || []).length + (game.activeContracts || []).length; + } + let safety = 8; + while ((game.activeContracts || []).length < total && safety-- > 0) { + game.progression.contractCounter++; + const code = 'C-' + String(game.progression.contractCounter).padStart(3, '0'); + const openList = (game.activeContracts || []).filter(c => + c.state === CONTRACT_STATE.OPEN && !c.bonusVariant && !c.bonusTwinOf + ); + const stufeN = (stufe && stufe.n) || 1; + const wantBonus = stufeN >= 2 && openList.length > 0 && _progressionRng(game) < 0.25; + if (wantBonus) { + const twin = openList[Math.floor(_progressionRng(game) * openList.length)]; + const bonusContract = _makeContract(game, code, twin.originLocationId, twin.targetLocationId, 1, false); + bonusContract.rewardBase = Math.round(twin.rewardBase * 1.5); + bonusContract.bonusVariant = true; + bonusContract.bonusTwinOf = twin.id; + bonusContract.profile = twin.profile; + game.activeContracts.push(bonusContract); + } else { + const follow = _generateL1ContractFromPool(game, code, game.activeContracts || []); + game.activeContracts.push(follow); + _validateUniqueIds(game); + } + } + } + + /** Silent-remove unassigned OPEN-Auftraege deren dueTime verstrichen ist. + * Nur auf L1 aktiv (dort kommen und gehen die Auftraege). L2/L3 laesst + * OPEN-Auftraege bestehen — dort muss der Spieler Prioritaeten setzen, + * verfallene Aufträge sind nicht Teil des Designs. */ + function _expireOffers(game) { + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) !== 1) return; // Nur L1 + const nowMs = new Date(game.simulationTime).getTime(); + const before = (game.activeContracts || []).length; + game.activeContracts = (game.activeContracts || []).filter(c => { + if (c.state !== CONTRACT_STATE.OPEN) return true; // nur unassigned OPEN + if (c.assignedVehicleId) return true; // ein Fahrzeug laeuft schon + if (!c.dueTime) return true; + return new Date(c.dueTime).getTime() > nowMs; + }); + const removed = before - (game.activeContracts || []).length; + if (removed > 0) { + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'contract_expired_' + nowMs, + type: 'CONTRACT_EXPIRED', + messageShort: removed + ' Auftrag' + (removed === 1 ? '' : 'en') + ' verfallen (Frist abgelaufen).', + simTime: game.simulationTime, + }); + } + } + + /** L1-Auto-Buy: kauft automatisch einen TRUCK_SMALL, wenn Balance ueber + * 15.000 € liegt UND maximal noch 2 Fahrzeuge da sind. Kosten 5.000 €. */ + function _maybeAutoBuyVehicle(game) { + // Auto-Rent nur noch auf L1 (Thomas 2026-04-25). L2 nutzt manuellen + // Kauf via buyVehicle-API (UI-Button im Vehicles-Panel). + const cfg = game._config || {}; + const isL1 = (cfg.maxActiveContracts || 1) === 1; + if (!isL1) return; + const cap = 2, threshold = 10000, mode = 'TRUCK_SMALL'; + if ((game.vehicles || []).length >= cap) return; + if ((game.balance || 0) < threshold) return; + const newVeh = _spawnVehicle(game, mode); + if (!newVeh) return; + game.progression.trucksBought = (game.progression.trucksBought || 0) + 1; + const rentPerDay = cfg.rentalCostPerDay || 0; + const totalRent = rentPerDay * (game.vehicles || []).length; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'autorent_' + newVeh.id, + type: 'VEHICLE_PURCHASED', + messageShort: '🚚 Neuer ' + newVeh.displayName + ' gemietet — Tagesmiete jetzt ' + totalRent + ' €.', + simTime: game.simulationTime, + }); + } + + /** Preisliste Fahrzeugkauf (manuell ab L2 via UI, auto auf L1 bei Balance-Trigger). */ + const VEHICLE_PRICES = Object.freeze({ + TRUCK_SMALL: 5000, + TRUCK_MID: 10000, + TRUCK_LARGE: 15000, + TRAIN_SMALL: 5000, // Symbolische Anzahlung (Train ist Miete, Tagesmiete ist der Hauptkosten-Hebel) + TRAIN: 40000, + }); + + /** Manueller Fahrzeugkauf. Returns {success, vehicle?, reason?}. + * Spezialfall TRAIN_SMALL: nur ab Tag 5 (Sim-Stunde 96), Spawn in Hamburg. */ + function buyVehicle(game, mode) { + const price = VEHICLE_PRICES[mode]; + if (!price) return { success: false, reason: 'Unbekannter Fahrzeugtyp: ' + mode }; + const cfg = game._config || {}; + const available = cfg.availableVehicleTypes || ['TRUCK_SMALL']; + if (!available.includes(mode)) { + return { success: false, reason: 'Typ ' + mode + ' nicht auf diesem Level freigeschaltet' }; + } + // Train: nur ab Tag 5 verfuegbar (Thomas-Regel L2) + if (mode === 'TRAIN_SMALL' || mode === 'TRAIN') { + const elapsedH = getSimHoursElapsed(game); + if (elapsedH < 96) { + return { success: false, reason: 'Güterzug erst ab Tag 5 mietbar (aktuell Tag ' + (Math.floor(elapsedH/24) + 1) + ')' }; + } + // Limit: nur ein Train pro Spiel + if ((game.vehicles || []).some(v => v.mode === 'TRAIN_SMALL' || v.mode === 'TRAIN')) { + return { success: false, reason: 'Nur ein Güterzug gleichzeitig moeglich' }; + } + } + if ((game.balance || 0) < price) { + return { success: false, reason: 'Zu wenig Geld (benoetigt ' + price + ' €)' }; + } + // Spawn-Hub: Train kommt in Hamburg an, sonst Standard (_spawnVehicle nutzt 'wien') + let veh; + if (mode === 'TRAIN_SMALL' || mode === 'TRAIN') { + veh = _spawnVehicle(game, mode, 'hamburg'); + } else { + veh = _spawnVehicle(game, mode); + } + if (!veh) return { success: false, reason: 'Konnte Fahrzeug nicht spawnen' }; + game.balance -= price; + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + game.progression.trucksBought = (game.progression.trucksBought || 0) + 1; + if (!game.pendingNotifications) game.pendingNotifications = []; + const isTrain = (mode === 'TRAIN_SMALL' || mode === 'TRAIN'); + game.pendingNotifications.push({ + id: (isTrain ? 'rent_train_' : 'buy_') + veh.id, + type: 'VEHICLE_PURCHASED', + messageShort: (isTrain ? '🚂 ' : '🛒 ') + veh.displayName + + (isTrain ? ' gemietet — Anzahlung ' : ' gekauft (−') + + price + ' €' + (isTrain ? ', Tagesmiete läuft.' : ')'), + simTime: game.simulationTime, + }); + return { success: true, vehicle: veh }; + } + + /** Spawnt ein neues Fahrzeug (gleiche Logik wie seedInitialVehicles, aber einzeln). + * optionaler hubLocId fuer Spawn-Ort (Default: wien). */ + function _spawnVehicle(game, mode, hubLocId) { + const t = VEHICLE_DEFAULTS[mode]; + if (!t) return null; + const locs = game.worldState.locations || []; + const home = locs.find(l => l.id === 'wien') || locs[0]; + if (!home) return null; + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + if (typeof game.progression.vehicleCounter !== 'number') { + game.progression.vehicleCounter = (game.vehicles || []).length; + } + game.progression.vehicleCounter++; + const idx = game.progression.vehicleCounter; + const driver = _pickDriver(idx); + const v = { + id: 'veh_' + idx, + mode, + driverName: driver.name, + driverLabel: driver.label, + displayName: driver.name, + vehicleTypeLabel: _vehicleLabel(mode), + color: VEHICLE_COLORS[(idx - 1) % VEHICLE_COLORS.length], + state: VEHICLE_STATE.IDLE, + currentLocationId: home.id, + lat: home.lat, + lon: home.lon, + capacityUnits: t.capacity, + speedKmh: t.speedKmh, + assignedContractId: null, + pickupRouteId: null, + deliveryRouteId: null, + activeRouteId: null, + tripPhase: null, + segmentProgress: 0, + phaseRemainingMinutes: 0, + loadMinutes: 0, + unloadMinutes: 0, + loadedContractIds: [], + tourStops: [], + currentStopIndex: 0, + }; + if (!game.vehicles) game.vehicles = []; + game.vehicles.push(v); + return v; } /** @@ -1556,13 +2429,13 @@ // Hub-Verteilung pro Level (nach maxActiveContracts als L-Indikator) const hubsPerLevel = { 1: ['wien'], - 2: ['wien', 'muenchen', 'hamburg'], + 2: ['wien', 'muenchen'], // L2: 2 Hubs für 2 Start-Fahrzeuge 3: ['wien', 'muenchen', 'hamburg', 'rotterdam', 'paris'], }; // Vehicle-Typ-Sequenz pro Level const seqPerLevel = { 1: ['TRUCK_SMALL'], - 2: ['TRUCK_SMALL', 'TRUCK_LARGE', 'TRAIN'], + 2: ['TRUCK_SMALL', 'TRUCK_MID'], // L2 (Neuanfang 2026-04-25): klein + mittel 3: ['TRUCK_SMALL', 'TRUCK_LARGE', 'TRUCK_SMALL', 'TRUCK_LARGE', 'TRAIN'], }; const lvlKey = (cfg.maxActiveContracts === 1) ? 1 : (cfg.maxActiveContracts <= 3 ? 2 : 3); @@ -1575,16 +2448,28 @@ const hubId = hubs[i % hubs.length]; const home = locs.find(l => l.id === hubId) || locs.find(l => l.id === 'wien') || locs[0]; const t = VEHICLE_DEFAULTS[mode]; + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + if (typeof game.progression.vehicleCounter !== 'number') game.progression.vehicleCounter = 0; + game.progression.vehicleCounter++; + const idx = game.progression.vehicleCounter; + const driver = _pickDriver(idx); + // Per-Level Capacity-Override: L2 hat einen kleineren TRAIN + // (8 Plaetze statt 20), damit er auch zum Einstieg rentabel laeuft. + let capacity = t.capacity; + if (mode === 'TRAIN' && lvlKey === 2) capacity = 8; out.push({ - id: 'veh_' + (i + 1), + id: 'veh_' + idx, mode, - displayName: _vehicleLabel(mode) + ' #' + (i + 1), - color: VEHICLE_COLORS[i % VEHICLE_COLORS.length], + driverName: driver.name, + driverLabel: driver.label, + displayName: driver.name, + vehicleTypeLabel: _vehicleLabel(mode) + (mode === 'TRAIN' && lvlKey === 2 ? ' (klein)' : ''), + color: VEHICLE_COLORS[(idx - 1) % VEHICLE_COLORS.length], state: VEHICLE_STATE.IDLE, currentLocationId: home.id, lat: home.lat, lon: home.lon, - capacityUnits: t.capacity, + capacityUnits: capacity, speedKmh: t.speedKmh, assignedContractId: null, // Legacy (für intermodal-Pfad) pickupRouteId: null, @@ -1606,11 +2491,30 @@ function _vehicleLabel(mode) { if (mode === 'TRUCK_SMALL') return 'Kleiner LKW'; + if (mode === 'TRUCK_MID') return 'Mittlerer LKW'; if (mode === 'TRUCK_LARGE') return 'Großer LKW'; + if (mode === 'TRAIN_SMALL') return 'Güterzug (klein)'; if (mode === 'TRAIN') return 'Güterzug'; return mode; } + /** Fahrer-Pool — wird in Reihenfolge an gespawnte Fahrzeuge zugeteilt. + * Anna und Tom zuerst, weil dafuer Voice-Lines (verfahren_anna/_tom) + * bereits generiert sind. Weitere Namen fuer L2/L3-Fahrzeuge. */ + const DRIVERS = Object.freeze([ + { name: 'Anna', label: 'Fahrerin Anna' }, + { name: 'Tom', label: 'Fahrer Tom' }, + { name: 'Lisa', label: 'Fahrerin Lisa' }, + { name: 'Max', label: 'Fahrer Max' }, + { name: 'Sara', label: 'Fahrerin Sara' }, + { name: 'Felix', label: 'Fahrer Felix' }, + { name: 'Mia', label: 'Fahrerin Mia' }, + { name: 'Jonas', label: 'Fahrer Jonas' }, + ]); + function _pickDriver(idx) { + return DRIVERS[(idx - 1) % DRIVERS.length]; + } + /** * Auftrags-Templates für deterministische Generierung (Phase 3). * Pro Template: Origin, Target, max-Container-Range, Eilauftrags-Flag. @@ -1618,18 +2522,36 @@ // Phase 8-Balance: mehr 1-Container-Aufträge, damit kleine LKWs nützlich sind. // Nur wenige „Großaufträge" (2-3 Container) bleiben für Zug / großen LKW. const CONTRACT_TEMPLATES = Object.freeze([ + // ── Wien-Hub-Auftraege (passend fuer den TRUCK_SMALL in Wien) ── { origin: 'wien', target: 'salzburg', containers: [1, 1], express: false, cargoTypeIds: ['food', 'machinery'] }, - { origin: 'wien', target: 'muenchen', containers: [1, 2], express: false, cargoTypeIds: ['machinery', 'auto_parts'] }, - { origin: 'muenchen', target: 'hamburg', containers: [2, 3], express: false, cargoTypeIds: ['machinery', 'chemicals'] }, // Großauftrag - { origin: 'berlin', target: 'hamburg', containers: [1, 1], express: false, cargoTypeIds: ['electronics', 'clothing'] }, - { origin: 'hamburg', target: 'rotterdam', containers: [2, 3], express: false, cargoTypeIds: ['raw_materials', 'chemicals'] }, // Großauftrag - { origin: 'rotterdam', target: 'paris', containers: [1, 1], express: true, cargoTypeIds: ['electronics', 'clothing'] }, - { origin: 'paris', target: 'muenchen', containers: [1, 2], express: false, cargoTypeIds: ['food', 'clothing'] }, - { origin: 'wien', target: 'berlin', containers: [1, 1], express: false, cargoTypeIds: ['auto_parts', 'machinery'] }, - { origin: 'muenchen', target: 'mailand', containers: [1, 1], express: true, cargoTypeIds: ['auto_parts', 'electronics'] }, - { origin: 'berlin', target: 'warschau', containers: [1, 2], express: false, cargoTypeIds: ['chemicals', 'machinery'] }, + { origin: 'wien', target: 'salzburg', containers: [1, 1], express: false, cargoTypeIds: ['food'] }, + { origin: 'wien', target: 'muenchen', containers: [1, 1], express: false, cargoTypeIds: ['machinery', 'auto_parts'] }, + { origin: 'wien', target: 'graz', containers: [1, 1], express: false, cargoTypeIds: ['food'] }, + { origin: 'wien', target: 'berlin', containers: [1, 1], express: false, cargoTypeIds: ['auto_parts'] }, { origin: 'salzburg', target: 'muenchen', containers: [1, 1], express: false, cargoTypeIds: ['food', 'paper_wood'] }, - { origin: 'kopenhagen', target: 'hamburg', containers: [1, 2], express: false, cargoTypeIds: ['paper_wood', 'food'] }, + { origin: 'salzburg', target: 'wien', containers: [1, 1], express: false, cargoTypeIds: ['food'] }, + + // ── Muenchen-Hub-Auftraege (TRUCK_LARGE-tauglich, 2-4 Container) ── + { origin: 'muenchen', target: 'hamburg', containers: [2, 3], express: false, cargoTypeIds: ['machinery', 'chemicals'] }, + { origin: 'muenchen', target: 'mailand', containers: [2, 3], express: false, cargoTypeIds: ['auto_parts'] }, // Brenner + { origin: 'muenchen', target: 'berlin', containers: [2, 3], express: false, cargoTypeIds: ['machinery', 'electronics'] }, + { origin: 'muenchen', target: 'wien', containers: [2, 3], express: false, cargoTypeIds: ['auto_parts'] }, + { origin: 'mailand', target: 'muenchen', containers: [2, 3], express: false, cargoTypeIds: ['food', 'clothing'] }, + + // ── Hamburg-Hub-Auftraege (TRAIN-tauglich, 8-15 Container) ── + { origin: 'hamburg', target: 'rotterdam', containers: [6, 8], express: false, cargoTypeIds: ['raw_materials'] }, // Hafen-Hafen, Train-Klasse (L2 Cap 8) + { origin: 'hamburg', target: 'berlin', containers: [4, 6], express: false, cargoTypeIds: ['chemicals'] }, // Mittelschwer + { origin: 'rotterdam', target: 'hamburg', containers: [6, 8], express: false, cargoTypeIds: ['electronics'] }, // Container Import (L2 Cap 8) + { origin: 'kopenhagen', target: 'hamburg', containers: [4, 6], express: false, cargoTypeIds: ['paper_wood'] }, + { origin: 'berlin', target: 'hamburg', containers: [3, 5], express: false, cargoTypeIds: ['electronics'] }, + + // ── Express-Aufträge (höhere Reward, knappe Frist) ── + { origin: 'rotterdam', target: 'paris', containers: [1, 2], express: true, cargoTypeIds: ['electronics'] }, + { origin: 'muenchen', target: 'mailand', containers: [1, 1], express: true, cargoTypeIds: ['auto_parts'] }, + + // ── Sonstige ── + { origin: 'paris', target: 'muenchen', containers: [2, 3], express: false, cargoTypeIds: ['food', 'clothing'] }, + { origin: 'berlin', target: 'warschau', containers: [2, 3], express: false, cargoTypeIds: ['chemicals'] }, ]); /** @@ -1672,7 +2594,18 @@ if (max === 1) { // L1 deterministisch und hardcoded — gut für Tutorial out.push(_makeContract(game, 'C-001', 'wien', 'salzburg', 1, false)); + } else if (cfg.railEnabled && !cfg.intermodalEnabled) { + // L2-Neuanfang: Pool-basiert, Container-Range wird ausgewuerfelt + if (!game.progression) game.progression = { round: 1, trucksBought: 0 }; + game.progression.contractCounter = 0; + for (let i = 0; i < max; i++) { + game.progression.contractCounter++; + const code = 'C-' + String(game.progression.contractCounter).padStart(3, '0'); + const c = _generateL2ContractFromPool(game, code, out); + out.push(c); + } } else { + // Legacy-Pfad fuer altes komplexes L2/L3 (Templates-basiert) for (let i = 0; i < max; i++) { const tpl = _nextContractTemplate(game); const code = 'C-' + String(i + 1).padStart(3, '0'); @@ -1717,7 +2650,7 @@ due.setTime(due.getTime() + fristH * 60 * 60 * 1000); const baseReward = Math.round(_computeReward(totalDistanceKm, isExpress, containers) * 1.5); // Intermodal-Bonus +50 % return { - id: 'contract_' + code, + id: 'contract_' + code + '_' + _nextUniqId(game), code, intermodal: true, legs: legs.map((l, i) => ({ @@ -1779,13 +2712,14 @@ const due = new Date(game.simulationTime); due.setTime(due.getTime() + fristH * 60 * 60 * 1000); return { - id: 'contract_' + code, + id: 'contract_' + code + '_' + _nextUniqId(game), code, originLocationId: originId, targetLocationId: targetId, cargoTypeId: 'general', quantityContainers: containers, state: CONTRACT_STATE.OPEN, + isExpress: !!isExpress, issueTime: game.simulationTime, dueTime: due.toISOString(), rewardBase: _computeReward(distanceKm, isExpress, containers), @@ -1804,9 +2738,52 @@ /** * Phase 6c: Minigame-Result wird auf nächste Verladung angewendet. - * `loadingTimeFactor` (aus result.metadata) modifiziert phaseRemainingMinutes - * der nächsten LOADING-Phase. + * Phase 8e++: Ladebonus-Queue fuer Minispiel-Rewards. + * Jeder erfolgreiche Einpark-Run erzeugt einen Bonus in Minuten. + * Beim naechsten Uebergang in LOADING / UNLOADING / tour_handling wird + * der aelteste Bonus aus der Queue konsumiert (FIFO) und reduziert die + * Phase-Dauer um bis zu X Minuten (min. 1 Min Restzeit). */ + const LOAD_BONUS_MAX_STACK = 5; + + function pushLoadBonus(game, minutes, source) { + if (!minutes || minutes <= 0) return false; + if (!game.pendingBonuses) game.pendingBonuses = []; + // Stapel-Limit: aelteste fliegen beim Ueberschreiten raus + while (game.pendingBonuses.length >= LOAD_BONUS_MAX_STACK) { + game.pendingBonuses.shift(); + } + game.pendingBonuses.push({ + id: 'bonus_' + (Date.now() + Math.floor(Math.random() * 1000)), + minutes: Math.round(minutes), + source: source || 'minigame', + createdAt: game.simulationTime, + }); + return true; + } + + /** Interner Konsum: beim Phase-Uebergang LOADING/UNLOADING/tour_handling. */ + function _consumeNextLoadBonus(game, vehicle) { + const q = game.pendingBonuses || []; + if (!q.length) return null; + const bonus = q.shift(); + const before = vehicle.phaseRemainingMinutes || 0; + const after = Math.max(1, before - bonus.minutes); + vehicle.phaseRemainingMinutes = after; + const actualSaved = before - after; + if (!game.pendingNotifications) game.pendingNotifications = []; + game.pendingNotifications.push({ + id: 'bonus_consumed_' + bonus.id, + type: 'LOAD_BONUS_CONSUMED', + vehicleId: vehicle.id, + bonusId: bonus.id, + minutesSaved: actualSaved, + messageShort: '⚡ Ladebonus −' + actualSaved + ' min bei ' + (vehicle.displayName || vehicle.id), + simTime: game.simulationTime, + }); + return { bonus, savedMinutes: actualSaved }; + } + function applyMinigameResult(game, result) { if (!result) return; const factor = (result.metadata && typeof result.metadata.loadingTimeFactor === 'number') @@ -1992,7 +2969,7 @@ STATE, VEHICLE_MODE, VEHICLE_STATE, CONTRACT_STATE, LOCATION_TYPE, EVENT_TYPE, HINT_MODE, // Konstanten - VEHICLE_DEFAULTS, ECONOMY, EVENT_RULES, + VEHICLE_DEFAULTS, VEHICLE_PRICES, ECONOMY, EVENT_RULES, // Factories createGame, // Lifecycle (Phase 1) @@ -2004,11 +2981,17 @@ serialize, deserialize, // Phase 2 — Aufträge, Fahrzeuge, Routen calculateRoute, assignContract, - seedInitialVehicles, seedInitialContracts, + seedInitialVehicles, seedInitialContracts, buyVehicle, // Phase 4b — Intermodal makeIntermodalContract: _makeIntermodalContract, // Phase 8 — Multi-Contract-Tour loadContract, unloadContract, cancelLoading, startTour, + // Phase 8e: L1-Stufen-Progression (Accept/Decline) + acceptOffer, declineOffer, + // Phase 8e+: Verfahren-Ereignis + resolveDriverLost, + // Phase 8e++: Ladebonus-Queue (pushLoadBonus fuer UI nach erfolgreichem Minispiel) + pushLoadBonus, // Stubs (Phase 5/6) useHint, applyMinigameResult, // Helper (Phase 0 testbar) diff --git a/App/sims/logistik/game.html b/App/sims/logistik/game.html index 605d814..0cd377a 100644 --- a/App/sims/logistik/game.html +++ b/App/sims/logistik/game.html @@ -4,6 +4,8 @@ Logistik Europa — Simulation + + @@ -14,18 +16,52 @@ html, body { height: 100%; margin: 0; padding: 0; font-family: 'Inter', system-ui, sans-serif; background: var(--ggs-bg); color: var(--ggs-text); touch-action: manipulation; } - .lg-layout { display: grid; grid-template-rows: auto 1fr auto auto; height: 100vh; } - .lg-main { display: grid; grid-template-columns: 240px 1fr 240px; gap: 8px; padding: 8px; min-height: 0; } + .lg-layout { display: grid; grid-template-rows: auto 1fr auto auto auto; height: 100vh; } + .lg-main { display: grid; grid-template-columns: 240px 220px 1fr; gap: 8px; padding: 8px; min-height: 0; } .lg-panel { background: var(--ggs-white); border-radius: var(--ggs-radius-md); padding: 12px; overflow-y: auto; box-shadow: var(--ggs-shadow-sm); } .lg-map { background: var(--ggs-white); border-radius: var(--ggs-radius-md); overflow: hidden; box-shadow: var(--ggs-shadow-sm); position: relative; } #map { width: 100%; height: 100%; } .lg-status { display: flex; gap: 16px; align-items: center; padding: 8px 16px; background: var(--ggs-white); border-top: 1px solid var(--ggs-border); font-size: 0.9rem; } + .lg-status-item { cursor: help; padding: 2px 4px; border-radius: 4px; + transition: background 0.15s; } + .lg-status-item:hover { background: var(--ggs-bg-dark, #f5f3ea); } + /* Blink-Highlight (Phase 8f++): Buttons pulsieren waehrend Voice-Hint */ + .lg-blink { animation: lg-btn-blink 0.7s ease-in-out infinite alternate; + box-shadow: 0 0 0 2px rgba(216,153,0,0); } + @keyframes lg-btn-blink { + 0% { background: var(--ggs-bg-dark, #f0ece3); + box-shadow: 0 0 0 2px rgba(216,153,0,0); transform: scale(1); } + 100% { background: #ffd766; + box-shadow: 0 0 0 4px rgba(216,153,0,.45); transform: scale(1.08); } + } + + .lg-status-realtime { font-size: 0.78rem; color: #6a6a6a; font-variant-numeric: tabular-nums; + padding: 2px 8px; background: #f5f3ea; border-radius: 10px; border: 1px solid #d6d1c4; } + .lg-status-explain { position: fixed; background: #fff; border: 1px solid #c8c4b3; + border-radius: 8px; padding: 12px 16px; box-shadow: 0 8px 24px rgba(0,0,0,.18); + max-width: 320px; font-size: 0.85rem; z-index: 4000; line-height: 1.4; } + .lg-status-explain h4 { margin: 0 0 6px; font-size: 0.95rem; color: #1f4b37; } + .lg-status-explain p { margin: 0 0 4px; color: #444; } + .lg-status-explain .lg-formula { font-family: monospace; background: #f5f3ea; + padding: 2px 6px; border-radius: 3px; display: inline-block; margin-top: 4px; } + .lg-trip-history { display: flex; gap: 6px; align-items: center; padding: 6px 12px; background: #f5f3ea; + border-top: 1px solid var(--ggs-border); overflow-x: auto; white-space: nowrap; + font-size: 0.82rem; min-height: 36px; } + .lg-trip-history .lg-th-label { color: #666; font-weight: 600; margin-right: 4px; flex-shrink: 0; } + .lg-trip-pill { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 12px; + font-variant-numeric: tabular-nums; cursor: pointer; border: 1px solid transparent; + font-weight: 600; font-size: 0.78rem; flex-shrink: 0; } + .lg-trip-pill.positive { background: #d6e8d6; color: #2d6d3d; border-color: #8cbf9b; } + .lg-trip-pill.negative { background: #f3e3e3; color: #b32d2d; border-color: #d8a0a0; } + .lg-trip-pill.rent { background: #fff3d6; color: #7a5a1a; border-color: #d6b94e; + font-style: italic; cursor: default; opacity: 0.92; } + .lg-trip-pill:hover { box-shadow: 0 2px 6px rgba(0,0,0,.15); } .lg-didaktik { max-height: 120px; overflow-y: auto; padding: 8px 16px; background: var(--ggs-bg); border-top: 1px solid var(--ggs-border); font-size: 0.82rem; } .lg-status b { font-variant-numeric: tabular-nums; } /* iPad Landscape (1180×820) — Touch-Ziele ≥ 36 px */ @media (min-width: 900px) and (max-width: 1199px) { - .lg-main { grid-template-columns: 215px 1fr 215px; } + .lg-main { grid-template-columns: 215px 200px 1fr; } } @media (max-width: 899px) { .lg-main { grid-template-columns: 1fr; grid-template-rows: auto auto 300px; } @@ -87,6 +123,26 @@ .lg-progress-bar { height: 4px; background: #e2ddd1; border-radius: 2px; margin-top: 4px; overflow: hidden; } .lg-progress-fill { height: 100%; background: #1f4b37; transition: width 0.3s; } .lg-progress-fill.pickup { background: #d6b94e; } + /* Frist-Balken in Kontrakt-Cards (Phase 8f++) — Restzeit visuell: + graue Strecke mit gelber/roter Gefahrenzone am Ende, grüner Fill von links */ + .lg-deadline { margin-top: 5px; } + .lg-deadline-bar { position: relative; height: 8px; + background: linear-gradient(to right, + #e8e4d8 0%, #e8e4d8 70%, + #fbe6c2 70%, #fbe6c2 88%, + #f3d0d0 88%, #f3d0d0 100%); + border-radius: 4px; overflow: hidden; } + .lg-deadline-fill { position: absolute; top: 0; right: 0; height: 100%; + background: #2d6d4d; transition: width 0.6s linear, background-color 0.3s; } + .lg-deadline-fill.warn { background: #c88a19; } + .lg-deadline-fill.danger { background: #b32d2d; } + .lg-deadline-label { font-size: 0.7rem; color: #555; margin-top: 2px; + font-variant-numeric: tabular-nums; display: flex; justify-content: space-between; } + .lg-deadline-label .lg-dl-warn { color: #b32d2d; font-weight: 700; } + .lg-trip-remaining { font-size: 0.7rem; color: #6a5e3e; margin-top: 2px; font-variant-numeric: tabular-nums; } + .lg-trip-penalty { font-size: 0.72rem; margin-top: 4px; padding: 2px 6px; border-radius: 3px; + background: #f3d0d0; color: #b32d2d; font-weight: 600; } + .lg-trip-penalty.warn { background: #fbe6c2; color: #7a3d00; } /* Analytics-Panel (Phase 7) — Toggle-Button liegt im Header (ggs-btn-ghost) */ .lg-analytics-panel { position: absolute; top: 8px; right: 8px; width: 240px; max-height: calc(100% - 16px); overflow-y: auto; background: #fff; border: 1px solid #e2ddd1; border-radius: 6px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,.15); z-index: 1100; font-size: 0.82rem; } .lg-analytics-panel h4 { margin: 0 0 8px; font-size: 0.9rem; color: var(--ggs-text); } @@ -95,6 +151,40 @@ .lg-analytics-panel dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; } /* End-Screen (Phase 7) */ + /* CTA-Button-Variante (Phase 8f++) — fuer prominente Splash/End-Aktionen. + Laenger, runder, mit klarem Focus-Ring. Ueberschreibt ggs-btn-primary + mit mehr Gewicht ohne das Design-System zu aendern. */ + .lg-cta-btn { padding: 12px 28px !important; font-size: 15px !important; + border-radius: 12px !important; letter-spacing: 0.02em; + box-shadow: 0 4px 14px rgba(47,109,77,.28); + transition: transform .15s ease, box-shadow .15s ease; } + .lg-cta-btn:hover { transform: translateY(-2px) !important; + box-shadow: 0 6px 18px rgba(47,109,77,.4) !important; } + .lg-cta-btn:active { transform: translateY(0) !important; } + .lg-cta-btn.secondary { color: var(--ggs-fjord); background: #fff !important; + border: 1.5px solid var(--ggs-fjord); box-shadow: 0 2px 6px rgba(47,109,77,.15); } + + /* Level-Splash / Intro-Screen (Phase 8f++) */ + .lg-splash-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.78); + display: none; align-items: center; justify-content: center; z-index: 3000; } + .lg-splash-overlay.open { display: flex; } + .lg-splash-modal { background: #fff; border-radius: 14px; max-width: 720px; + width: 94%; overflow: hidden; box-shadow: 0 16px 48px rgba(0,0,0,.55); + max-height: 95vh; display: flex; flex-direction: column; } + .lg-splash-img { width: 100%; display: block; max-height: 300px; object-fit: cover; + flex-shrink: 0; } + .lg-splash-body { padding: 20px 28px 24px; overflow-y: auto; } + .lg-splash-body h2 { margin: 0 0 4px; color: #1f4b37; font-size: 1.45rem; } + .lg-splash-subtitle { color: #666; font-size: 0.9rem; margin-bottom: 14px; } + .lg-splash-sections { display: flex; flex-direction: column; gap: 12px; margin-bottom: 18px; } + .lg-splash-section { display: flex; gap: 12px; align-items: flex-start; } + .lg-splash-section-icon { font-size: 1.6rem; line-height: 1.2; flex-shrink: 0; + width: 32px; text-align: center; } + .lg-splash-section-body h3 { margin: 0 0 3px; font-size: 0.98rem; color: #1f4b37; + font-weight: 700; } + .lg-splash-section-body p { margin: 0; font-size: 0.9rem; color: #444; line-height: 1.45; } + .lg-splash-actions { text-align: right; margin-top: 6px; } + .lg-endscreen-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display: none; align-items: center; justify-content: center; z-index: 2500; } .lg-endscreen-overlay.open { display: flex; } .lg-endscreen-modal { background: #fff; border-radius: 10px; padding: 24px 28px; max-width: 520px; width: 92%; box-shadow: 0 12px 36px rgba(0,0,0,.35); } @@ -106,6 +196,18 @@ .lg-endscreen-summary dt { color: #666; } .lg-endscreen-summary dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; } .lg-endscreen-actions { text-align: right; margin-top: 12px; display: flex; gap: 8px; justify-content: flex-end; } + /* Sterne + Leerfahrten-Display (Phase 8f++) */ + .lg-endscreen-stars { text-align: center; margin: 10px 0 6px; } + .lg-stars { font-size: 2.2rem; letter-spacing: 0.1em; line-height: 1; } + .lg-stars .lg-star-on { color: #e8c547; text-shadow: 0 1px 2px rgba(0,0,0,.15); } + .lg-stars .lg-star-off { color: #d6d1c4; } + .lg-stars-label { font-size: 0.95rem; color: #555; margin-top: 4px; font-weight: 600; } + .lg-endscreen-empty { margin: 10px 0; padding: 10px 14px; background: #f7f5ef; border-radius: 6px; } + .lg-empty-label { font-size: 0.88rem; color: #555; display: flex; justify-content: space-between; } + .lg-empty-label span { font-weight: 700; color: #1f4b37; font-variant-numeric: tabular-nums; } + .lg-empty-bar { height: 10px; background: #e2ddd1; border-radius: 5px; overflow: hidden; margin-top: 4px; } + .lg-empty-fill { height: 100%; background: linear-gradient(90deg, #2d6d4d 0%, #e8c547 60%, #b32d2d 100%); transition: width .4s; } + .lg-empty-sub { font-size: 0.78rem; color: #666; margin-top: 4px; } /* Card-Selection (klickt Vehicle ↔ Contract zusammen) */ .lg-card.linked { border-color: #1f4b37; box-shadow: 0 0 0 2px #2d6d4d44; } @@ -115,26 +217,234 @@ .lg-containers { white-space: nowrap; letter-spacing: -2px; } /* Laderaum (Phase 8 — Multi-Contract) */ - .lg-loadmode-banner { background: #fff4c9; border: 1px solid #d6b94e; border-radius: 4px; padding: 8px 10px; margin-top: 6px; font-size: 0.78rem; color: #5e5320; } - .lg-cargo-hold { margin-top: 8px; padding: 8px; background: #f7f5ef; border: 1px solid #c0b8a3; border-radius: 5px; } - .lg-cargo-hold-title { font-size: 0.74rem; color: #666; margin-bottom: 6px; letter-spacing: 0.3px; text-transform: uppercase; } + .lg-loadmode-banner { background: #fff4c9; border: 1px solid #d6b94e; border-radius: 4px; + padding: 6px 8px; margin-top: 6px; font-size: 0.72rem; color: #5e5320; + line-height: 1.3; } + .lg-loadmode-banner strong { font-size: 0.82rem; } + .lg-loadmode-sub { color: #8a7a4f; font-size: 0.68rem; } + .lg-cargo-hold { margin-top: 6px; padding: 6px 8px; background: #f7f5ef; + border: 1px solid #c0b8a3; border-radius: 5px; font-size: 0.7rem; } + .lg-cargo-hold-title { font-size: 0.66rem; color: #666; margin-bottom: 4px; + letter-spacing: 0.3px; text-transform: uppercase; } .lg-cargo-slots { display: flex; flex-wrap: wrap; gap: 2px; } .lg-slot { width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; border-radius: 3px; background: #e2ddd1; border: 1px dashed #b8ae90; } .lg-slot.filled { background: #d6e8d6; border: 1px solid #2d6d3d; } - .lg-tour-actions { display: flex; gap: 6px; margin-top: 8px; } - .lg-tour-actions button { flex: 1; min-height: 36px; font-size: 0.85rem; } + .lg-tour-actions { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; } + .lg-tour-actions button { width: 100%; min-height: 32px; padding: 6px 14px; + font-size: 0.85rem; border-radius: 16px; } .lg-stop-badge { display: inline-block; padding: 1px 6px; background: #e8f0eb; border-radius: 3px; font-size: 0.7rem; color: #2d6d3d; margin-left: 4px; } + /* Away-Bonus-Badge (Phase 8d): kennzeichnet Auftraege, die Leerfahrt zum Pickup brauchen */ + .lg-away-badge { display: inline-block; margin-left: 4px; padding: 1px 6px; background: #fff4c9; color: #7a5a1a; + border: 1px solid #d6b94e; border-radius: 3px; font-size: 0.68rem; font-weight: 600; vertical-align: middle; } + + /* Announce-Modal (Phase 8f): kurze Gratulation/Meldung fuer Meilensteine */ + .lg-announce-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); display: none; + align-items: center; justify-content: center; z-index: 2500; } + .lg-announce-overlay.open { display: flex; } + .lg-announce-modal { background: #fff; border-radius: 12px; padding: 22px 28px; text-align: center; + max-width: 440px; width: 90%; box-shadow: 0 12px 32px rgba(0,0,0,.3); + animation: lg-announce-in .35s cubic-bezier(.16,1,.3,1) both; } + .lg-announce-icon { font-size: 3rem; margin-bottom: 4px; } + .lg-announce-modal h3 { margin: 6px 0 4px; color: #1f4b37; font-size: 1.15rem; } + .lg-announce-modal p { margin: 4px 0; color: #333; font-size: 0.95rem; } + .lg-announce-sub { color: #666 !important; font-size: 0.82rem !important; font-style: italic; } + @keyframes lg-announce-in { + 0% { transform: scale(.6); opacity: 0; } + 100% { transform: scale(1); opacity: 1; } + } + + /* Auto-Boost-Badge (Phase 8e++): erscheint wenn Sim automatisch auf 8x ist */ + .lg-autoboost-badge { display: inline-flex; align-items: center; margin-left: 6px; padding: 2px 8px; + background: #ffecb3; color: #8a5a1a; border: 1px solid #e0a030; border-radius: 10px; + font-size: 0.72rem; font-weight: 600; animation: lg-autoboost-pulse 1.5s ease-in-out infinite; } + @keyframes lg-autoboost-pulse { + 0%, 100% { background: #ffecb3; } + 50% { background: #ffd766; } + } + + /* Bonus-Pills (Phase 8e++): Ladebonus-Stack oben rechts auf der Karte. + Position wo vorher das Layer-Control-Widget war. */ + .lg-bonus-pills { position: absolute; top: 12px; right: 12px; z-index: 480; + display: flex; flex-direction: column; gap: 4px; align-items: flex-end; } + .lg-bonus-pill { background: #ffecb3; color: #8a5a1a; border: 1px solid #e0a030; + border-radius: 12px; padding: 3px 10px; font-size: 0.78rem; font-weight: 600; + box-shadow: 0 2px 4px rgba(0,0,0,.15); cursor: pointer; + font-variant-numeric: tabular-nums; white-space: nowrap; + transition: transform .2s, box-shadow .2s; } + .lg-bonus-pill:hover { transform: scale(1.05); box-shadow: 0 3px 8px rgba(0,0,0,.2); } + .lg-bonus-popover { position: absolute; top: 40px; right: 0; background: #fff; + border: 1px solid #d6b94e; border-radius: 6px; padding: 8px 12px; + font-size: 0.82rem; color: #333; max-width: 240px; z-index: 490; + box-shadow: 0 4px 12px rgba(0,0,0,.2); } + /* Flug-Animation: Pill fliegt zum Fahrzeug-Marker und fadet aus */ + .lg-bonus-pill-fly { position: fixed; z-index: 9999; pointer-events: none; + transition: transform .8s cubic-bezier(.4,0,.2,1), opacity .8s, scale .8s; } + + /* Park-Celebration (Phase 8f++): grosses Glueckwunsch-Fenster zentriert, + fliegt nach 1,2 s zur Bonus-Pill oben rechts und verschwindet. + Wrapper macht das Zentrieren, innere Card kann unbeeinflusst ihren + eigenen Transform fuer Pop-in und Flight bekommen. */ + .lg-park-celebrate-wrap { position: fixed; inset: 0; display: flex; + align-items: center; justify-content: center; z-index: 10000; + pointer-events: none; } + .lg-park-celebrate { background: #fff; color: #1f4b37; padding: 22px 36px; + border: 2px solid #2d6d4d; border-radius: 14px; + box-shadow: 0 14px 40px rgba(0,0,0,.35); + text-align: center; min-width: 260px; transform: scale(1); + animation: lg-park-pop 0.35s cubic-bezier(.3,1.4,.5,1) both; } + .lg-park-celebrate-icon { font-size: 2.8rem; line-height: 1; margin-bottom: 4px; } + .lg-park-celebrate-title { font-size: 1.25rem; font-weight: 700; color: #2d6d3d; } + .lg-park-celebrate-sub { font-size: 0.92rem; color: #555; margin-top: 4px; } + @keyframes lg-park-pop { + 0% { transform: scale(0.4); opacity: 0; } + 60% { transform: scale(1.08); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } + } + + /* Pflicht-Pause-Banner in der Fahrzeug-Card (Phase 8f++) */ + .lg-break-banner { background: #fff3d6; color: #7a5a1a; border: 1px solid #d6b94e; + border-radius: 4px; padding: 4px 8px; font-size: 0.78rem; font-weight: 600; + margin-bottom: 4px; } + + /* Verfahren-Banner (Phase 8e+) */ + .lg-verfahren-banner { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); + z-index: 500; background: #fbe6c2; color: #7a3d00; border: 2px solid #c88a19; border-radius: 6px; + padding: 10px 16px; font-size: 0.95rem; font-weight: 500; + box-shadow: 0 4px 12px rgba(0,0,0,.2); max-width: 85%; text-align: center; } + + /* Uhr-Widget (Phase 8f++): Stunden-/Minutenzeiger + Tag/Nacht-Gradient. + Liegt unten-rechts auf der Karte (oben-links ist Leaflet-Zoom, + unten-links ist Leaflet-Scale). 72 × 72 px, nicht-interaktiv. */ + .lg-clock-widget { position: absolute; bottom: 16px; right: 12px; z-index: 450; + width: 72px; height: 72px; pointer-events: none; + filter: drop-shadow(0 2px 4px rgba(0,0,0,.25)); } + .lg-clock-widget .lg-clock-day-label { font-size: 9px; font-weight: 700; + fill: #fff; text-anchor: middle; paint-order: stroke; stroke: #333; + stroke-width: 2px; } + + /* Info-Cards (Phase 8f++): pro Zielort bei LOADING-Start, Slide-in-from-right. + Atlas-Pattern (Mail 2026-04-24 22:00): Toast++ mit Bild, nicht-blockierend. */ + .lg-infocard-container { position: fixed; top: 180px; right: 16px; z-index: 1200; + display: flex; flex-direction: column; gap: 12px; max-width: 300px; + pointer-events: none; } + .lg-infocard { background: #fff; border-radius: 12px; overflow: hidden; + box-shadow: 0 6px 24px rgba(0,0,0,.18); pointer-events: auto; + animation: lg-slide-in 0.4s cubic-bezier(.2,.8,.2,1) both, + lg-slide-out 0.4s 10s cubic-bezier(.5,0,.8,.3) both; } + .lg-infocard-img { width: 100%; height: 110px; display: flex; align-items: center; + justify-content: center; object-fit: cover; + background: linear-gradient(135deg, #e8e4d8 0%, #d6cfb8 100%); + background-size: cover; background-position: center; + color: #6a5a3a; font-size: 0.95rem; font-weight: 600; text-align: center; + letter-spacing: 0.02em; padding: 0 8px; box-sizing: border-box; } + .lg-infocard-body { padding: 10px 12px; } + .lg-infocard-body h3 { font-size: 0.88rem; font-weight: 700; margin: 0 0 3px; + color: #1f4b37; } + .lg-infocard-body p { font-size: 0.72rem; color: #555; margin: 0; line-height: 1.35; } + .lg-infocard-stats { display: flex; gap: 10px; font-size: 0.68rem; margin-top: 6px; + color: #888; font-variant-numeric: tabular-nums; } + .lg-infocard-stats b { color: #555; font-weight: 600; } + @keyframes lg-slide-in { from { transform: translateX(120%); opacity: 0 } + to { transform: translateX(0); opacity: 1 } } + @keyframes lg-slide-out { to { transform: translateX(120%); opacity: 0 } } + + /* Negative-Marge-Hinweis (Phase 8f++): wenn alle OPEN-Aufträge unprofitabel */ + .lg-all-negative-hint { margin-bottom: 8px; padding: 8px 10px; + background: #fff3d6; color: #7a5a1a; border: 1px solid #d6b94e; + border-radius: 6px; font-size: 0.82rem; line-height: 1.35; + display: flex; gap: 6px; align-items: flex-start; } + .lg-all-negative-hint .lg-anh-icon { font-size: 1.1rem; flex-shrink: 0; } + + /* Card-Dismiss-X (Phase 8f++): Auftrag ohne Annahme verwerfen */ + .lg-card { position: relative; } + .lg-card-dismiss { position: absolute; top: 4px; right: 4px; + width: 22px; height: 22px; border-radius: 11px; border: none; + background: rgba(255,255,255,.7); color: #888; + font-size: 14px; line-height: 1; cursor: pointer; padding: 0; + transition: background 0.15s, color 0.15s; } + .lg-card-dismiss:hover { background: #f3d0d0; color: #b32d2d; } + + /* Train-Eligible-Badge: Auftrag passt für Güterzug */ + .lg-train-badge { display: inline-block; margin-left: 4px; padding: 1px 8px; + border-radius: 3px; font-size: 0.7rem; font-weight: 700; + background: #cfe0f0; color: #1e4f7a; border: 1px solid #5f85b8; + vertical-align: middle; } + + /* Bonus-Variant-Badge (Phase 8f++): Zwilling mit +50 % Reward */ + .lg-bonus-variant-badge { display: inline-block; margin-left: 4px; padding: 1px 8px; + border-radius: 3px; font-size: 0.7rem; font-weight: 700; + background: #fff3d6; color: #7a5a1a; border: 1px solid #e0c060; + vertical-align: middle; animation: lg-bonus-pulse 2.2s ease-in-out infinite; } + @keyframes lg-bonus-pulse { + 0%, 100% { background: #fff3d6; box-shadow: 0 0 0 rgba(224,192,96,0); } + 50% { background: #ffe9a8; box-shadow: 0 0 8px rgba(224,192,96,.6); } + } + + /* Profile-Badges (Phase 8e simple): tight / far / easy */ + .lg-profile-badge { display: inline-block; margin-left: 4px; padding: 1px 6px; border-radius: 3px; + font-size: 0.68rem; font-weight: 600; vertical-align: middle; border: 1px solid; } + .lg-profile-tight { background: #fde1d1; color: #8a3a1a; border-color: #d8844e; } + .lg-profile-far { background: #e0eaf6; color: #1f4b7a; border-color: #5f85b8; } + .lg-profile-easy { background: #e8f0e8; color: #2d6b3d; border-color: #7aa87a; } + + /* Phase 8e: OFFERED-Auftrags-Karten mit Accept/Decline */ + .lg-card.state-offered { background: #fff9ea; border-left: 4px solid #c88a19; } + .lg-express-badge { display: inline-block; margin-left: 4px; padding: 1px 6px; background: #ffecb3; color: #8a5a1a; + border: 1px solid #e0a030; border-radius: 3px; font-size: 0.68rem; font-weight: 600; vertical-align: middle; } + .lg-multi-badge { display: inline-block; margin-left: 4px; padding: 1px 6px; background: #e0eaf6; color: #1f4b7a; + border: 1px solid #5f85b8; border-radius: 3px; font-size: 0.68rem; font-weight: 600; vertical-align: middle; } + .lg-offer-countdown { font-size: 0.74rem; color: #8a5a1a; margin-top: 4px; font-variant-numeric: tabular-nums; } + .lg-offer-actions { display: flex; gap: 6px; margin-top: 6px; } + .lg-offer-actions button { flex: 1; min-height: 32px; font-size: 0.82rem; border-radius: 4px; border: 0; cursor: pointer; + font-weight: 600; } + .lg-offer-accept { background: #1f6b3a; color: #fff; } + .lg-offer-accept:hover { background: #2d7d48; } + .lg-offer-decline { background: #f5f3ea; color: #7a2d2d; border: 1px solid #d6b0b0; } + .lg-offer-decline:hover { background: #f3e3e3; } + + /* Fahrzeugkauf-UI (Phase 8d) */ + .lg-buy-vehicle-btn { width: 100%; margin-bottom: 8px; min-height: 36px; } + .lg-buy-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display: none; align-items: center; + justify-content: center; z-index: 2100; } + .lg-buy-overlay.open { display: flex; } + .lg-buy-modal { background: #fff; border-radius: 8px; padding: 20px 22px; max-width: 560px; width: 94%; + box-shadow: 0 8px 24px rgba(0,0,0,.3); } + .lg-buy-modal h3 { margin: 0 0 10px; font-size: 1.05rem; color: var(--ggs-text); } + .lg-buy-modal .lg-buy-hint { font-size: 0.8rem; color: #666; margin: 4px 0 12px; } + .lg-buy-item { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; + padding: 10px; border: 1px solid #e2ddd1; border-radius: 5px; margin-bottom: 8px; } + .lg-buy-item.disabled { opacity: 0.5; background: #f5f3ea; } + .lg-buy-item-icon { font-size: 1.6rem; } + .lg-buy-item-body b { display: block; font-size: 0.92rem; color: #1f4b37; } + .lg-buy-item-body span { font-size: 0.78rem; color: #555; } + .lg-buy-item-price { font-weight: 700; color: #1f4b37; font-size: 1.0rem; white-space: nowrap; } + .lg-buy-item-price.unaffordable { color: #b32d2d; } + .lg-buy-item button { min-width: 80px; min-height: 34px; } + .lg-buy-actions { display: flex; justify-content: flex-end; margin-top: 8px; } + /* Kosten-Hinweise (Phase 8b — Didaktik: Kostenstruktur sichtbar machen) */ .lg-card-cost { color: #3d3a2f; margin-top: 2px; } .lg-card-cost strong { color: #1f4b37; } .lg-card-cost-detail { color: #888; font-size: 0.72rem; } .lg-contract-cost { color: #6b6135; font-size: 0.74rem; margin-top: 2px; } - .lg-tour-preview { margin-top: 8px; padding: 8px 10px; background: #eef6f0; border: 1px solid #8cbf9b; border-radius: 5px; font-size: 0.80rem; } - .lg-tour-preview-row { display: flex; justify-content: space-between; margin: 2px 0; } - .lg-tour-preview-row.sum { border-top: 1px solid #b6d4c0; margin-top: 6px; padding-top: 6px; font-weight: 600; } + .lg-tour-preview { margin-top: 6px; padding: 6px 8px; background: #eef6f0; + border: 1px solid #8cbf9b; border-radius: 5px; font-size: 0.72rem; } + .lg-tour-preview-row { display: flex; justify-content: space-between; + align-items: baseline; gap: 8px; margin: 1px 0; } + .lg-tour-preview-row.sum { border-top: 1px solid #b6d4c0; margin-top: 4px; + padding-top: 4px; font-weight: 600; } .lg-tour-preview-row.sum.positive { color: #1f6b3a; } .lg-tour-preview-row.sum.negative { color: #a83232; } + .lg-tp-num { font-variant-numeric: tabular-nums; white-space: nowrap; + text-align: right; min-width: 70px; } + .lg-tp-num small { color: #777; font-weight: 400; margin-left: 2px; } + .lg-info-btn { display: inline-flex; align-items: center; justify-content: center; + width: 14px; height: 14px; border-radius: 50%; border: 1px solid #888; + background: #fff; color: #555; font-size: 0.62rem; font-weight: 700; + cursor: pointer; padding: 0; line-height: 1; vertical-align: middle; + margin-left: 4px; font-style: italic; } + .lg-info-btn:hover { background: #555; color: #fff; } /* Intermodal Leg-Pills (Phase 6b) */ .lg-legs { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 3px; } @@ -224,11 +534,10 @@ if (!window.LOGISTIK_BASE) {
- - - - - + + + +
+ 📚 - - 🏠
@@ -254,8 +562,43 @@ if (!window.LOGISTIK_BASE) {
-
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + Tag 1 +
- -
- Sim-Zeit: - Konto: - Bilanz: - Aufträge: 0/0 + Sim-Zeit: + Konto: + Umsatz: + Gewinn: + + Aufträge: 0/0 + ⏱ — INIT
+ +
+
+
🎉
+

Titel

+

Text

+ +
+
+ + +
+ + + + + +
Willkommen in der Simulation „Logistik Europa". Du arbeitest als Bearbeiter:in eines europäischen Logistikunternehmens. Klick rechts auf ein Fahrzeug, dann links auf die passenden Aufträge zum Laden. Die Kostenzeile „€/100 km" hilft bei der Auswahl: grosses Fahrzeug lohnt nur bei vollen Ladungen.
- + + + + @@ -353,8 +747,9 @@ if (!window.LOGISTIK_BASE) {

🚆 Rangierbahnhof — Hauptgleis + Sammelgleis

- Ziel: Reihenfolge 🎡W · 🥨M · ⚓H · 🗼P - (Lok vorn) und mit der Lok rechts auf dem Hauptgleis ankommen.
+ Ziel: Waggons in Reihenfolge #1 · #2 · #3 · #4 + sortieren (Lok vorn) und mit der Lok rechts auf dem Hauptgleis ankommen. + Unter 4 Minuten = Bonus auf das nächste Be-/Entladen.
⬅ / ➡ Lok fahren (vor / zurück, jeweils relativ zur Lok-Front — am Scheinwerfer erkennst du, wohin sie schaut) · 1 2 3 obere Weichen, 4 5 6 untere Weichen @@ -376,6 +771,18 @@ if (!window.LOGISTIK_BASE) {

+ + + @@ -453,7 +860,11 @@ window.pickText = function(opts) { /* ---------- Engine-Initialisierung (Lifecycle) ---------- */ const params = new URLSearchParams(location.search); - const levelNum = parseInt(params.get('level') || '1', 10); + let levelNum = parseInt(params.get('level') || '1', 10); + // Integrations-Vertrag: teacher_started erzwingt forcedLevel + if (window.LOGISTIK_SESSION_MODE === 'teacher_started' && window.LOGISTIK_FORCED_LEVEL) { + levelNum = parseInt(window.LOGISTIK_FORCED_LEVEL, 10) || levelNum; + } const game = LogistikEngine.createGame(levelNum); /* ---------- Server-API-Helper (Phase 7b) ---------- @@ -527,23 +938,35 @@ window.pickText = function(opts) { }); } + /** Assessment-Call (Integrations-Vertrag §6.3/§6.4). + * phase: 'started' | 'running' | 'completed' + * payload: { kennzahlen?, results?, reflections?, durationMs?, completedPhases? } */ + async function postAssessment(phase, payload) { + payload = payload || {}; + return request('/api/assessment', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify(Object.assign({ + simId: 'logistik', + phase: phase, + duration: payload.durationMs || 0, + completedPhases: payload.completedPhases || [phase], + processLog: payload.kennzahlen ? [{ at: Date.now(), kennzahlen: payload.kennzahlen }] : [], + results: payload.results || {}, + reflections: payload.reflections || {}, + }, {})), + }); + } + return { - startAttempt, endAttempt, saveSlot, loadSlot, postAnalytics, + startAttempt, endAttempt, saveSlot, loadSlot, postAnalytics, postAssessment, isDisabled: () => disabled, }; })(); - // Autosave-Hook: local-first, Plattform-7b-Server-Save wird bei - // LEVEL_SUCCESS/LEVEL_FAILED vom Server-Closer ausgeloest. + // Autosave entfernt (Thomas 2026-04-25). Auf der ToDo-Liste: + // konzeptionelles Auto-Save mit Resume bei naechstem Start. LogistikEngine.onStateChange(game, function (evt) { - try { - localStorage.setItem( - 'logistik:autosave:' + game.sessionId, - JSON.stringify(LogistikEngine.serialize(game)) - ); - } catch (e) { - console.warn('[Logistik] Autosave fehlgeschlagen:', e.message); - } console.info('[Logistik] state:', evt.from, '→', evt.to, '(', evt.reason, ')'); }); @@ -551,6 +974,13 @@ window.pickText = function(opts) { LG_API.startAttempt().then(id => { if (id) console.info('[Logistik] Server-Attempt:', id); }); + // Assessment: Level-Start + const sessionStartMs = performance.now(); + LG_API.postAssessment('started', { + durationMs: 0, + completedPhases: [], + kennzahlen: { level: levelNum, mode: window.LOGISTIK_SESSION_MODE || 'free' }, + }); LogistikEngine.loadContent(game); // INIT → LOADING_CONTENT → READY LogistikEngine.seedInitialVehicles(game); // Phase 2: Fahrzeuge anlegen @@ -602,23 +1032,79 @@ window.pickText = function(opts) { TERMINAL: { radius: 6, color: '#7a4a1e', fillColor: '#c4823a', fillOpacity: 0.85, weight: 2 }, }; - // Locations rendern, gefiltert nach visibleLocationLevel - const visibleLevel = (game._config && game._config.visibleLocationLevel) || levelNum; + // Locations rendern — alle immer sichtbar (Thomas-Wunsch: keine Stufen-Filterung) const locations = game.worldState.locations || []; const locById = {}; locations.forEach(loc => { locById[loc.id] = loc; - if ((loc.visibleFromLevel || 1) > visibleLevel) return; const style = markerStyle[loc.type] || markerStyle.CITY; - const marker = L.circleMarker([loc.lat, loc.lon], style); + let marker; + if (loc.type === 'PORT') { + // Hafen als Schiff-Icon (Anker als Symbol fuer Seehafen) + marker = L.marker([loc.lat, loc.lon], { + icon: L.divIcon({ + className: 'lg-port-icon-wrap', + html: '
', + iconSize: [24, 24], + iconAnchor: [12, 12], + }), + }); + } else if (loc.type === 'TERMINAL') { + // Terminal/Binnenhafen als Container-Icon + marker = L.marker([loc.lat, loc.lon], { + icon: L.divIcon({ + className: 'lg-terminal-icon-wrap', + html: '
📦
', + iconSize: [22, 22], + iconAnchor: [11, 11], + }), + }); + } else { + marker = L.circleMarker([loc.lat, loc.lon], style); + } + const typeLabel = { + CAPITAL: 'Hauptstadt', + CITY: 'Stadt', + PORT: 'Seehafen', + TERMINAL: 'Logistik-Terminal', + INDUSTRY: 'Industrie', + CUSTOMER: 'Kunde', + HUB: 'Knoten', + }[loc.type] || loc.type; marker.bindPopup( '
' + '

' + escapeHtml(loc.name) + '

' + - '
' + escapeHtml(loc.countryId) + ' · ' + escapeHtml(loc.regionId || '—') + '
' + - '
' + escapeHtml(loc.type) + '
' + + '
' + escapeHtml(loc.countryId) + ' · ' + escapeHtml(typeLabel) + '
' + + (loc.didacticInfo ? '
' + escapeHtml(loc.didacticInfo) + '
' : '') + '
' ); + // Klick auf Stadt: + // 1) Wenn Verfahren-Event aktiv: als Ziel-Tipp werten + // 2) Sonst: didaktische Info-Card einblenden + marker.on('click', () => { + const lost = (game.vehicles || []).find(v => v.verfahrenPending); + if (!lost) { showCityCard(loc); return; } + const result = LogistikEngine.resolveDriverLost(game, lost.id, loc.id, loc.type); + if (result.ok) { + const lostH = result.totalLostHours || 0; + showToast(pickText({ + standard: lostH > 0 + ? '✓ Richtig! Durch falsche Tipps gingen ' + lostH.toFixed(1) + ' h verloren.' + : '✓ Richtig! ' + (lost.verfahrenDriverLabel || 'Fahrer:in') + ' fährt weiter.', + easy: lostH > 0 + ? 'Richtig. ' + lostH.toFixed(1) + ' Stunden verloren.' + : 'Richtig! Fahrt geht weiter.', + })); + } else if (result.wrong) { + const wasted = result.wastedHours || 1.5; + showToast(pickText({ + standard: '✗ Dein Tipp war falsch — ' + wasted + ' h verloren. Versuch nochmal.', + easy: 'Falsch. ' + wasted + ' Stunden verloren. Probier nochmal.', + })); + } + renderVerfahrenBanner(); + }); if (loc.type === 'CAPITAL') marker.addTo(layers.capitals); else if (loc.type === 'PORT') marker.addTo(layers.ports); else if (loc.type === 'TERMINAL') marker.addTo(layers.terminals); @@ -641,21 +1127,13 @@ window.pickText = function(opts) { line.addTo(layers.railnet); }); - // Default-Layer aktivieren (Hauptstädte + Städte immer; Häfen+Bahn nur wenn Level passt) + // Alle Layer immer sichtbar (kein Layer-Control mehr — Thomas-Wunsch) layers.capitals.addTo(map); layers.cities.addTo(map); - if (game._config && game._config.portsEnabled) layers.ports.addTo(map); + layers.ports.addTo(map); + layers.terminals.addTo(map); if (railEnabled) layers.railnet.addTo(map); - // Layer-Control rechts oben - L.control.layers(null, { - 'Hauptstädte': layers.capitals, - 'Städte': layers.cities, - 'Häfen': layers.ports, - 'Terminals': layers.terminals, - 'Bahnnetz': layers.railnet, - }, { collapsed: false }).addTo(map); - // Skalenleiste (Phase 7-UX) — metrisch, unten-links L.control.scale({ imperial: false, @@ -673,15 +1151,250 @@ window.pickText = function(opts) { return (r >= 0 ? '+' : '') + r.toLocaleString('de-DE') + ' €'; } + /* ---------- Uhr-Widget (Phase 8f++): Zeiger + Tag/Nacht-Gradient ---------- + Sonnen-Faktor in [0,1], 1=Mittag, 0=Mitternacht. Je nach Wert werden + zwei Farbstops im radialGradient interpoliert (innen=Sonne/Mond, + aussen=Himmel). Tag-Label unterm Zeiger. */ + function updateClockWidget(date, dayNum) { + const clock = document.getElementById('lgClock'); + if (!clock) return; + const h = date.getHours(); + const m = date.getMinutes(); + const minuteAngle = m * 6; + const hourAngle = (h % 12) * 30 + m * 0.5; + const hourHand = document.getElementById('lgClockHour'); + const minuteHand = document.getElementById('lgClockMinute'); + if (hourHand) hourHand.setAttribute('transform', 'rotate(' + hourAngle + ')'); + if (minuteHand) minuteHand.setAttribute('transform', 'rotate(' + minuteAngle + ')'); + + // Sonnen-Faktor: sin-Kurve, Maximum um 13 Uhr (kleine Verschiebung wirkt natuerlicher) + const tHours = h + m / 60; + const sun = Math.max(0, Math.sin((tHours - 7) / 12 * Math.PI)); + // Farben interpolieren: Mitternacht (dunkles Indigo) → Mittag (Hellblau/Gelb) + const mix = (a, b, t) => [ + Math.round(a[0] + (b[0] - a[0]) * t), + Math.round(a[1] + (b[1] - a[1]) * t), + Math.round(a[2] + (b[2] - a[2]) * t), + ]; + const hex = ([r, g, b]) => '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); + const centerNight = [60, 70, 120]; // Mond + const centerDay = [255, 233, 168]; // Sonne + const outerNight = [20, 38, 74]; // Nachthimmel + const outerDay = [122, 181, 224]; // Mittag-Blau + const center = hex(mix(centerNight, centerDay, sun)); + const outer = hex(mix(outerNight, outerDay, sun)); + const stop0 = document.getElementById('lgClockStop0'); + const stop1 = document.getElementById('lgClockStop1'); + if (stop0) stop0.setAttribute('stop-color', center); + if (stop1) stop1.setAttribute('stop-color', outer); + // Tag-Label-Farbe je nach Hintergrund-Helligkeit + const label = document.getElementById('lgClockDay'); + if (label) label.textContent = 'Tag ' + dayNum; + } + + /* ---------- Status-Click-Explainer (Phase 8f++) ---------- + Jedes Status-Feld zeigt bei Klick eine kurze Card mit Bedeutung + und aktueller Berechnung. Klick ausserhalb / nochmal schliesst sie. */ + let _statusExplainEl = null; + function _statusExplain(key, anchor) { + if (_statusExplainEl) { _statusExplainEl.remove(); _statusExplainEl = null; } + const a = (game.analytics || {}); + const cfg = game._config || {}; + const startBud = cfg.startBudget || 0; + const balance = Math.round(game.balance || 0); + const umsatz = Math.round(a.totalRewardEarned || 0); + const fahrk = Math.round(a.totalFareCostPaid || 0); + const miete = Math.round(a.runningCostsRental || 0); + const strafen = Math.round(a.totalPenaltyPaid || 0); + const gewinn = balance - startBud; + const nVeh = (game.vehicles || []).length; + const rentDay = (cfg.rentalCostPerDay || 0) * nVeh; + const limitH = cfg.timeLimitHours || 0; + const elapsedH = game.simulationStartTime + ? (new Date(game.simulationTime).getTime() - new Date(game.simulationStartTime).getTime()) / 3600000 + : 0; + const remainH = Math.max(0, limitH - elapsedH); + const completed = (game.completedContracts || []).length; + const totalC = completed + (game.activeContracts || []).length; + + const E = { + time: { + title: 'Sim-Zeit', + desc: 'Aktuelle Zeit in der Simulation. 1 Tag = 24 Sim-Stunden.', + line: 'Verstrichen: ' + elapsedH.toFixed(1) + ' h · Verbleibend: ' + remainH.toFixed(1) + ' h von ' + limitH + ' h', + }, + balance: { + title: 'Konto', + desc: 'Dein aktuelles Guthaben in Euro. Wenn das Konto auf 0 € oder darunter fällt, ist die Simulation verloren.', + line: 'Aktuell: ' + fmtEuro(balance), + }, + revenue: { + title: 'Umsatz', + desc: 'Summe aller eingenommenen Auftragserlöse seit Simulationsstart. Vor Abzug von Fahrkosten und Miete.', + line: 'Umsatz bisher: ' + fmtEuro(umsatz), + }, + profit: { + title: 'Gewinn', + desc: 'Was du seit Start verdient (oder verloren) hast. Konto minus Startbudget.', + formula: 'Konto ' + fmtEuro(balance) + ' − Startbudget ' + fmtEuro(startBud) + ' = ' + (gewinn >= 0 ? '+' : '−') + fmtEuro(Math.abs(gewinn)), + }, + rent: { + title: 'Tagesmiete', + desc: 'Fixkosten pro Sim-Tag für deine ' + nVeh + ' Fahrzeug' + (nVeh !== 1 ? 'e' : '') + ' à ' + fmtEuro(cfg.rentalCostPerDay || 0) + '. Läuft auch im Stand.', + formula: nVeh + ' × ' + fmtEuro(cfg.rentalCostPerDay || 0) + ' = ' + fmtEuro(rentDay) + ' / Tag', + line: 'Bisher gezahlte Miete: ' + fmtEuro(miete), + }, + contracts: { + title: 'Aufträge', + desc: 'Erledigte Aufträge / aktuelle Gesamtmenge (offen + erledigt).', + line: 'Erledigt: ' + completed + ' · Offen oder unterwegs: ' + (totalC - completed), + }, + }; + const info = E[key]; + if (!info) return; + const div = document.createElement('div'); + div.className = 'lg-status-explain'; + div.innerHTML = + '

' + escapeHtml(info.title) + '

' + + '

' + escapeHtml(info.desc) + '

' + + (info.formula ? '
' + escapeHtml(info.formula) + '
' : '') + + (info.line ? '

' + escapeHtml(info.line) + '

' : ''); + document.body.appendChild(div); + // Position oberhalb des Anker-Elements + const r = anchor.getBoundingClientRect(); + const dr = div.getBoundingClientRect(); + const left = Math.max(8, Math.min(window.innerWidth - dr.width - 8, r.left + r.width / 2 - dr.width / 2)); + const top = Math.max(8, r.top - dr.height - 8); + div.style.left = left + 'px'; + div.style.top = top + 'px'; + _statusExplainEl = div; + // Schliessen bei naechstem Klick irgendwo + setTimeout(() => { + const closer = (e) => { + if (!_statusExplainEl) return; + if (_statusExplainEl.contains(e.target)) return; + _statusExplainEl.remove(); + _statusExplainEl = null; + document.removeEventListener('click', closer, true); + }; + document.addEventListener('click', closer, true); + }, 0); + } + // Click-Handler an alle Status-Items binden + document.querySelectorAll('.lg-status-item').forEach(el => { + el.addEventListener('click', (e) => { + e.stopPropagation(); + const k = el.getAttribute('data-explain'); + _statusExplain(k, el); + }); + }); + + /* ---------- Frist-Balken Live-Update (Phase 8f++) ---------- + Aktualisiert die Width-Werte aller .lg-deadline-Elemente, ohne die + Card neu zu zeichnen. So tickt der Balken sichtbar runter. */ + function updateDeadlineBars() { + const nowMs = new Date(game.simulationTime).getTime(); + document.querySelectorAll('.lg-deadline').forEach(el => { + const cid = el.getAttribute('data-contract-id'); + const c = (game.activeContracts || []).find(x => x.id === cid); + if (!c || !c.dueTime || !c.issueTime) return; + const issueMs = new Date(c.issueTime).getTime(); + const dueMs = new Date(c.dueTime).getTime(); + const totalH = (dueMs - issueMs) / 3600000; + const remainH = (dueMs - nowMs) / 3600000; + if (totalH <= 0) return; + const pct = Math.max(0, Math.min(100, (remainH / totalH) * 100)); + const fill = el.querySelector('.lg-deadline-fill'); + if (fill) { + fill.style.width = pct.toFixed(1) + '%'; + fill.classList.toggle('warn', pct < 30 && pct >= 12); + fill.classList.toggle('danger', pct < 12); + } + const remainEl = el.querySelector('.lg-dl-remain'); + if (remainEl) { + const isL1Bar = el.getAttribute('data-l1') === '1'; + remainEl.textContent = remainH > 0 + ? (isL1Bar ? 'verschwindet in ' : 'noch ') + remainH.toFixed(1).replace('.', ',') + ' h' + : (isL1Bar ? 'läuft gleich aus' : 'Frist abgelaufen'); + remainEl.classList.toggle('lg-dl-warn', pct < 30); + } + }); + } + /* ---------- Status-Anzeige ---------- */ function updateStatusUI() { document.getElementById('statusState').textContent = game.state; + // Echte Session-Laufzeit in Minuten:Sekunden + const realSec = Math.max(0, (performance.now() - sessionStartMs) / 1000); + const m = Math.floor(realSec / 60); + const s = String(Math.floor(realSec % 60)).padStart(2, '0'); + const realEl = document.getElementById('statusRealtime'); + if (realEl) realEl.textContent = '⏱ ' + m + ':' + s + ' min'; document.getElementById('statusBalance').textContent = fmtEuro(game.balance); - document.getElementById('statusScore').textContent = game.score; + const a = (game.analytics || {}); + const startBud = (game._config && game._config.startBudget) || 0; + const umsatz = Math.round(a.totalRewardEarned || 0); + const gewinn = Math.round((game.balance || 0) - startBud); + const sign = gewinn >= 0 ? '+' : '−'; + document.getElementById('statusRevenue').textContent = fmtEuro(umsatz); + const profitEl = document.getElementById('statusProfit'); + profitEl.textContent = sign + fmtEuro(Math.abs(gewinn)); + profitEl.style.color = gewinn >= 0 ? '#1f6b3a' : '#a83232'; document.getElementById('statusContracts').textContent = game.completedContracts.length + '/' + (game.activeContracts.length + game.completedContracts.length); const t = new Date(game.simulationTime); - document.getElementById('statusTime').textContent = t.toLocaleString('de-DE'); + // Kompaktes Format ohne Sekunden: "Tag X, HH:MM" + const start = game.simulationStartTime + ? new Date(game.simulationStartTime).getTime() : t.getTime(); + const dayNum = Math.floor((t.getTime() - start) / 86400000) + 1; + const hh = String(t.getHours()).padStart(2, '0'); + const mm = String(t.getMinutes()).padStart(2, '0'); + document.getElementById('statusTime').textContent = 'Tag ' + dayNum + ', ' + hh + ':' + mm; + // Miete pro Tag (= rentalCostPerDay × Fahrzeuganzahl). Nur anzeigen, + // wenn das Level ueberhaupt Miete hat. + const rentPerDay = (game._config && game._config.rentalCostPerDay) || 0; + const rentWrap = document.getElementById('statusRentWrap'); + const rentEl = document.getElementById('statusRent'); + if (rentPerDay > 0 && rentEl && rentWrap) { + // Per-Vehicle Multiplikator (Train teurer): Summe aus allen Mietsaetzen + const VD = (LogistikEngine && LogistikEngine.VEHICLE_DEFAULTS) || {}; + const totalRent = (game.vehicles || []).reduce((sum, v) => { + const mult = (VD[v.mode] && typeof VD[v.mode].rentMult === 'number') ? VD[v.mode].rentMult : 1.0; + return sum + rentPerDay * mult; + }, 0); + rentEl.textContent = fmtEuro(Math.round(totalRent)); + rentWrap.hidden = false; + } else if (rentWrap) { + rentWrap.hidden = true; + } + // Uhr-Widget parallel aktualisieren + updateClockWidget(t, dayNum); + + // Level-Badge mit L1-Stufen-Progression (Phase 8e): + // "Level 1 · Stufe 3 Zeitdruck · Runde 5" + const lb = document.getElementById('levelBadge'); + if (lb) { + const cfg = game._config || {}; + const isL1 = (cfg.maxActiveContracts || 1) === 1; + if (isL1 && game.progression) { + const round = game.progression.round || 1; + const L1_STUFEN_UI = [ + null, + { n: 1, name: 'Erster Auftrag', minRound: 1 }, + { n: 2, name: 'Mehrere zur Wahl', minRound: 2 }, + { n: 3, name: 'Weitere Strecken', minRound: 4 }, + { n: 4, name: 'Mehr Fahrzeuge', minRound: 7 }, + { n: 5, name: 'Soft-Ramp → L2', minRound: 11 }, + ]; + let stufe = L1_STUFEN_UI[1]; + for (let i = 1; i < L1_STUFEN_UI.length; i++) { + if (L1_STUFEN_UI[i] && round >= L1_STUFEN_UI[i].minRound) stufe = L1_STUFEN_UI[i]; + } + lb.textContent = 'Level ' + levelNum + ' · Stufe ' + stufe.n + ' ' + stufe.name + ' · R' + round; + } else { + lb.textContent = 'Level ' + levelNum; + } + } } /* ---------- Karten-Layer für aktive Routen + Fahrzeug-Marker ---------- */ @@ -730,9 +1443,11 @@ window.pickText = function(opts) { } function vehicleEmoji(mode) { - if (mode === 'TRUCK_SMALL' || mode === 'TRUCK_LARGE') return '🚚'; - if (mode === 'TRAIN') return '🚆'; - return '📦'; + if (mode === 'TRUCK_SMALL') return '🚚'; + if (mode === 'TRUCK_MID') return '🚛'; + if (mode === 'TRUCK_LARGE') return '🚛'; + if (mode === 'TRAIN_SMALL' || mode === 'TRAIN') return '🚆'; + return '🚚'; } function ensureVehicleMarker(v) { @@ -783,7 +1498,22 @@ window.pickText = function(opts) { (game.vehicles || []).forEach(v => { const m = ensureVehicleMarker(v); if (typeof v.lat === 'number' && typeof v.lon === 'number') { - m.setLatLng([v.lat, v.lon]); + // IDLE-Fahrzeug leicht versetzen (Offset nach Fahrzeug-Index), damit + // der Stadtmarker darunter klickbar bleibt (Verfahren-Ziel-Tipp). + // Fahrende Fahrzeuge folgen der Route ohne Offset. + let displayLat = v.lat, displayLon = v.lon; + if (v.state === 'IDLE' && !v.tripPhase) { + const idx = parseInt((v.id || 'veh_1').replace(/\D/g, ''), 10) || 1; + // Ring von 4 Richtungen (NO, NW, SW, SO) + ~6 km Radius + const angles = [45, 135, 225, 315]; + const a = angles[(idx - 1) % 4] * Math.PI / 180; + const km = 6; + const dLat = Math.cos(a) * km / 111; + const dLon = Math.sin(a) * km / (111 * Math.cos(v.lat * Math.PI / 180)); + displayLat = v.lat + dLat; + displayLon = v.lon + dLon; + } + m.setLatLng([displayLat, displayLon]); } // Tooltip live aktualisieren const html = vehicleTooltipHtml(v); @@ -971,12 +1701,69 @@ window.pickText = function(opts) { function renderContracts() { const root = document.getElementById('contractsList'); - const list = (game.activeContracts || []).slice().concat((game.completedContracts || []).slice(-3)); + // Reihenfolge (Thomas 2026-04-26): primär nach Container-Anzahl + // aufsteigend (zuerst die kleinen), sekundaer nach Frist aufsteigend. + // Sichtbar: OPEN + RESERVED (geladen, noch nicht losgefahren). Sobald + // ein Tour startet (LOADING/IN_TRANSIT/...): aus der Liste raus, ist in + // der Fahrzeug-Card. Abgelaufene Frist wird ausgeblendet. + const nowMs = new Date(game.simulationTime).getTime(); + const visibleStates = new Set(['OPEN', 'RESERVED']); + const list = (game.activeContracts || []) + .filter(c => { + if (!(visibleStates.has(c.state) || c.intermodal)) return false; + if (c.dueTime && new Date(c.dueTime).getTime() < nowMs) return false; + return true; + }) + .slice() + .sort((a, b) => { + const ca = a.quantityContainers || 1; + const cb = b.quantityContainers || 1; + if (ca !== cb) return ca - cb; + const da = a.dueTime ? new Date(a.dueTime).getTime() : Infinity; + const db = b.dueTime ? new Date(b.dueTime).getTime() : Infinity; + return da - db; + }); if (!list.length) { root.innerHTML = '
Keine Aufträge offen.
'; return; } root.innerHTML = ''; + + // Negative-Marge-Hinweis: wenn ein Fahrzeug zur Verfuegung steht und ALLE + // OPEN-Auftraege fuer dieses Fahrzeug Verlustgeschaefte waeren, blende + // einen Tipp ein. Voice-Hint wird einmal pro "Welle" gesprochen. + const lvForCheck = (game.vehicles || []).find(v => v.id === loadingVehicleId + && v.state === 'IDLE' && !v.tripPhase); + const openOnly = (game.activeContracts || []).filter(c => c.state === 'OPEN' && !c.intermodal); + if (lvForCheck && openOnly.length > 0) { + const allNegative = openOnly.every(c => { + const cost = estimateRouteCost(c.originLocationId, c.targetLocationId, lvForCheck.mode); + return ((c.rewardBase || 0) - cost) < 0; + }); + if (allNegative) { + const banner = document.createElement('div'); + banner.className = 'lg-all-negative-hint'; + banner.innerHTML = '💡' + + escapeHtml(pickText({ + standard: 'Alle aktuellen Aufträge wären für ' + escapeHtml(lvForCheck.driverName || lvForCheck.displayName) + + ' Verlustgeschäfte. Vielleicht warten — neue Aufträge kommen regelmäßig nach.', + easy: 'Diese Aufträge bringen Verlust. Warte. Bessere kommen.', + })) + + ''; + root.appendChild(banner); + // Voice-Tipp einmal pro Welle (pro Spawn-Zyklus). Wir setzen einen + // Marker auf der Vehicle-Instanz, damit nicht auf jeden Re-Render + // erneut gesprochen wird. + if (!lvForCheck._allNegHintSpoken) { + lvForCheck._allNegHintSpoken = true; + speak('all_negative_hint'); + } + } else { + // Sobald wieder profitable Aufträge da sind: Marker zuruecksetzen, + // damit beim naechsten Engpass wieder ein Hinweis kommt. + if (lvForCheck._allNegHintSpoken) lvForCheck._allNegHintSpoken = false; + } + } if (selectedContractId && (game.activeContracts || []).find(c => c.id === selectedContractId && c.state === 'OPEN')) { const sel = game.activeContracts.find(c => c.id === selectedContractId); const hint = document.createElement('div'); @@ -994,18 +1781,46 @@ window.pickText = function(opts) { const v = (game.vehicles || []).find(x => x.id === selectedVehicleId); if (v && v.assignedContractId) pairedContractId = v.assignedContractId; } + // Capacity-Filter (nur L2/L3, nicht L1): wenn ein Fahrzeug ausgewaehlt + // ist, blende die Aufträge aus, die wegen Container-Anzahl nicht in + // den Restladeraum passen. Aufträge die kleiner sind als der Laderaum + // gehen natuerlich. L1 bleibt unangetastet. + const isNonL1 = (game._config || {}).maxActiveContracts > 1; + let lvForCap = null, freeCap = Infinity; + if (isNonL1 && loadingVehicleId) { + lvForCap = (game.vehicles || []).find(v => v.id === loadingVehicleId); + if (lvForCap) { + const used = (lvForCap.loadedContractIds || []).reduce((sum, cid) => { + const lc = (game.activeContracts || []).find(x => x.id === cid); + return sum + (lc ? (lc.quantityContainers || 0) : 0); + }, 0); + freeCap = (lvForCap.capacityUnits || 1) - used; + } + } list.forEach(c => { + // Filter: nicht-passende Auftraege ueberspringen (nur L2/L3 + Vehicle gewaehlt) + if (lvForCap && c.state === 'OPEN' && !c.intermodal + && (c.quantityContainers || 1) > freeCap) { + return; + } const card = document.createElement('div'); const cardSelected = (c.id === selectedContractId); const cardPaired = (c.id === pairedContractId); const isOpenForUser = c.intermodal ? (c.legs && c.legs[c.currentLegNum] && c.legs[c.currentLegNum].state === 'OPEN') : (c.state === 'OPEN'); + // Auftrag im aktuell selektierten Fahrzeug geladen? + const lvSel = (game.vehicles || []).find(x => x.id === loadingVehicleId); + const inSelected = !!(lvSel && (lvSel.loadedContractIds || []).includes(c.id)); + // Auftrag in einem anderen Fahrzeug geladen? + const inOtherVehicle = !inSelected && (game.vehicles || []) + .some(v => (v.loadedContractIds || []).includes(c.id)); const tone = contractStateTone(c.state); card.className = 'lg-card' + (cardSelected ? ' selected' : '') + (cardPaired ? ' linked-pair' : '') - + (!isOpenForUser ? ' busy' : '') + + (inSelected ? ' linked' : '') + + ((inOtherVehicle && !isOpenForUser) ? ' busy' : '') + (tone === 'open' ? ' state-open' : '') + (tone === 'busy' ? ' state-busy-soft' : ''); const due = new Date(c.dueTime); @@ -1024,9 +1839,7 @@ window.pickText = function(opts) { }).join(' ') + ''; } - // Kostenschaetzung fuer das aktuell im Lademodus aktive Fahrzeug - // (falls vorhanden): origin → target mit dessen Modus. Erlaubt vor - // dem Laden eine Wirtschaftlichkeits-Einordnung. + // Kostenschaetzung + Zeit-Reserve fuer das aktuell im Lademodus aktive Fahrzeug let costHintHtml = ''; if (loadingVehicleId && isOpenForUser && !c.intermodal) { const lv = (game.vehicles || []).find(x => x.id === loadingVehicleId); @@ -1035,35 +1848,187 @@ window.pickText = function(opts) { const margin = (c.rewardBase || 0) - estCost; const sign = margin >= 0 ? '+' : '−'; const col = margin >= 0 ? '#1f6b3a' : '#a83232'; + // Zeit-Reserve: Fahrtdauer vs. Frist + const vehDef = (LogistikEngine.VEHICLE_DEFAULTS || {})[lv.mode] || {}; + const distanceKm = _routeDistance(c.originLocationId, c.targetLocationId); + const travelH = vehDef.speedKmh ? distanceKm / vehDef.speedKmh : 0; + const nowMs = new Date(game.simulationTime).getTime(); + const dueMs = new Date(c.dueTime).getTime(); + const slackH = (dueMs - nowMs) / 3600000 - travelH; + let slackLbl, slackCol; + if (slackH >= 3) { slackLbl = slackH.toFixed(1) + 'h Reserve'; slackCol = '#2d6d3d'; } + else if (slackH >= 1) { slackLbl = slackH.toFixed(1) + 'h Reserve — knapp'; slackCol = '#8a5a1a'; } + else if (slackH >= 0) { slackLbl = slackH.toFixed(1) + 'h Reserve — kritisch'; slackCol = '#b32d2d'; } + else { slackLbl = 'Frist NICHT zu schaffen'; slackCol = '#b32d2d'; } + const isL1ForCost = (game._config || {}).maxActiveContracts === 1; costHintHtml = '
' + '≈ Fahrkosten mit ' + vehicleEmoji(lv.mode) + ' ' + escapeHtml(lv.displayName) + ': ' + '' + fmtEuro(estCost) + '' + ' · Marge ' + sign + fmtEuro(Math.abs(margin)) + '' + + (isL1ForCost ? '' : '
' + slackLbl + '') + '
'; } } + // Profile-Badge (aus L1-Generator): tight/far/easy + const profileBadges = { + tight: '⚡ knappe Frist', + far: '🌍 weite Fahrt', + easy: '🌿 kurz', + }; + const profileBadge = c.profile && profileBadges[c.profile] ? ' ' + profileBadges[c.profile] : ''; + // L1 hat keine Fristen (Thomas 2026-04-25): Frist- und Slack-Hinweise + // komplett ausblenden. Profile "tight" (knappe Frist) waere auf L1 + // ebenfalls irrefuehrend → Badge unterdruecken. + const isL1 = (game._config || {}).maxActiveContracts === 1; + const profileBadgeFinal = isL1 && c.profile === 'tight' ? '' : profileBadge; + const bonusBadge = c.bonusVariant + ? ' 💎 Bonus +50 %' + : ''; + const trainBadge = c.trainEligible + ? ' 🚂 Zug-tauglich' + : ''; + const deadlineLine = isL1 + ? '' // keine Frist-Zeile auf L1 + : ' · Frist: ' + due.toLocaleString('de-DE'); + // Zeit-Balken — zeigt verbleibende Zeit als Anteil der Gesamtfrist. + // Gruener Fill von links, gelbe und rote Gefahrenzone am rechten Ende. + // L1: nur fuer OPEN-Auftraege, Label "verschwindet in X h" (Cards + // kommen und gehen), keine Strafanzeige. + // L2/L3: in allen aktiven Zustaenden, Label "noch X h" als Lieferfrist. + let deadlineBarHtml = ''; + const deadlineDoneStates = new Set(['DELIVERED', 'FAILED', 'CANCELLED', 'EXPIRED']); + const showDeadline = c.dueTime && !deadlineDoneStates.has(c.state) + && (isL1 ? c.state === 'OPEN' : true); + if (showDeadline) { + const issueRaw = c.issueTime || game.simulationStartTime || game.simulationTime; + const issueMs = new Date(issueRaw).getTime(); + const dueMs = new Date(c.dueTime).getTime(); + const nowMs = new Date(game.simulationTime).getTime(); + const totalH = (dueMs - issueMs) / 3600000; + const remainH = (dueMs - nowMs) / 3600000; + if (totalH > 0) { + const pct = Math.max(0, Math.min(100, (remainH / totalH) * 100)); + const fillCls = pct < 12 ? 'danger' : pct < 30 ? 'warn' : ''; + const remainTxt = remainH > 0 + ? (isL1 ? 'verschwindet in ' : 'noch ') + remainH.toFixed(1).replace('.', ',') + ' h' + : (isL1 ? 'läuft gleich aus' : 'Frist abgelaufen'); + const totalTxt = isL1 ? 'Angebot' : 'Frist: ' + totalH.toFixed(0) + ' h'; + deadlineBarHtml = '
' + + '
' + + '
' + + '
' + + '
' + + '' + totalTxt + '' + + '' + remainTxt + '' + + '
' + + '
'; + } + } + // X-Button oben rechts: Auftrag verwerfen (nur fuer OPEN-Auftraege) + const dismissBtn = (c.state === 'OPEN' && !c.intermodal) + ? '' + : ''; card.innerHTML = - '
' + escapeHtml(c.code || c.id) + (c.intermodal ? ' 🔗' : '') + '
' + + dismissBtn + + '
' + escapeHtml(c.code || c.id) + + (c.intermodal ? ' 🔗' : '') + + profileBadgeFinal + + bonusBadge + + trainBadge + + (c.awayBonus ? ' ↗ woanders' : '') + + '
' + '
' + escapeHtml(_locName(c.originLocationId)) + ' → ' + escapeHtml(_locName(c.targetLocationId)) + ' · ' + Math.round(distance) + ' km · ' + containerSymbols(c.quantityContainers) + '
' + legsHtml + - '
Erlös: ' + fmtEuro(c.rewardBase) + ' · Frist: ' + due.toLocaleString('de-DE') + '
' + + '
Erlös: ' + fmtEuro(c.rewardBase) + deadlineLine + '
' + + deadlineBarHtml + costHintHtml + '' + escapeHtml(contractStateLabel(c.state)) + ''; - card.addEventListener('click', () => { + card.addEventListener('click', (ev) => { + // X-Button: Auftrag verwerfen (silent remove aus activeContracts) + if (ev.target && ev.target.classList && ev.target.classList.contains('lg-card-dismiss')) { + ev.stopPropagation(); + const dismissId = ev.target.getAttribute('data-dismiss-id'); + const idx = (game.activeContracts || []).findIndex(x => x.id === dismissId && x.state === 'OPEN'); + if (idx >= 0) { + game.activeContracts.splice(idx, 1); + renderPanels(); + } + return; + } + // RESERVED in selektiertem Vehicle → Toggle: ausladen + if (!isOpenForUser && inSelected && !c.intermodal) { + try { LogistikEngine.unloadContract(game, loadingVehicleId, c.id); } catch (e) {} + renderPanels(); + return; + } + // RESERVED in anderem Vehicle → kurzer Hinweis + if (!isOpenForUser && inOtherVehicle && !c.intermodal) { + const otherV = (game.vehicles || []) + .find(v => (v.loadedContractIds || []).includes(c.id)); + showToast(pickText({ + standard: 'Bereits in ' + (otherV ? otherV.displayName : 'anderem Fahrzeug') + ' geladen.', + easy: 'Schon im anderen LKW.', + })); + return; + } if (!isOpenForUser) return; + // Robuste Auswahl (Thomas 2026-04-25): vor dem Laden immer pruefen, + // ob das aktuell gewaehlte Fahrzeug wirklich laden kann. Sonst + // das naechste IDLE-Fahrzeug nehmen. Verhindert stale + // loadingVehicleId nach Tour-Starts. + if (!c.intermodal) { + let lvCheck = (game.vehicles || []).find(v => v.id === loadingVehicleId); + if (!lvCheck || lvCheck.state !== 'IDLE' || lvCheck.tripPhase) { + const nextIdle = (game.vehicles || []).find(v => v.state === 'IDLE' && !v.tripPhase); + loadingVehicleId = nextIdle ? nextIdle.id : null; + } + if (!loadingVehicleId) { + showToast(pickText({ + standard: 'Kein freies Fahrzeug verfügbar.', + easy: 'Kein LKW frei.', + })); + return; + } + } // Phase 8: Wenn ein Vehicle im Lademodus ist und Contract NICHT intermodal, // direkt in den LKW laden (statt nur Contract zu selecten). if (loadingVehicleId && !c.intermodal) { - try { - const r = LogistikEngine.loadContract(game, loadingVehicleId, c.id); - showToast(pickText({ - standard: '📦 ' + (c.code || c.id) + ' geladen — ' + r.freeSlotsAfter + ' Plätze frei.', - easy: 'Auftrag ist im LKW.', - })); - } catch (err) { - showToast(pickText({ standard: 'Laden geht nicht: ' + err.message, easy: 'Geht nicht: ' + err.message })); + // Prüfen ob dieser Auftrag bereits geladen ist → Toggle: ausladen + const lv = (game.vehicles || []).find(x => x.id === loadingVehicleId); + if (lv && (lv.loadedContractIds || []).includes(c.id)) { + try { LogistikEngine.unloadContract(game, loadingVehicleId, c.id); } catch (e) {} + renderPanels(); + return; } + // Auto-Swap: wenn kein Platz, ältesten geladenen Auftrag entladen und neu versuchen + let r, swapped = null; + try { + r = LogistikEngine.loadContract(game, loadingVehicleId, c.id); + } catch (err) { + const loaded = (lv && lv.loadedContractIds) || []; + if (loaded.length) { + swapped = loaded[0]; + try { + LogistikEngine.unloadContract(game, loadingVehicleId, swapped); + r = LogistikEngine.loadContract(game, loadingVehicleId, c.id); + } catch (err2) { + showToast(pickText({ standard: 'Laden geht nicht: ' + err2.message, easy: 'Geht nicht.' })); + renderPanels(); + return; + } + } else { + showToast(pickText({ standard: 'Laden geht nicht: ' + err.message, easy: 'Geht nicht: ' + err.message })); + renderPanels(); + return; + } + } + const msg = swapped + ? '🔄 ' + (c.code || c.id) + ' geladen (' + swapped.replace('contract_','') + ' ausgetauscht)' + : '📦 ' + (c.code || c.id) + ' geladen — ' + r.freeSlotsAfter + ' Plätze frei.'; + showToast(pickText({ standard: msg, easy: 'Auftrag ist im LKW.' })); + // Voice: Auftrag nach Zielstadt + speak('order_to_' + c.targetLocationId); renderPanels(); return; } @@ -1079,11 +2044,41 @@ window.pickText = function(opts) { function renderVehicles() { const root = document.getElementById('vehiclesList'); const list = game.vehicles || []; - if (!list.length) { - root.innerHTML = '
Keine Fahrzeuge verfügbar.
'; - return; + // Auto-Select: oberstes IDLE-Fahrzeug automatisch in Lademodus, wenn noch keins aktiv + if (!loadingVehicleId) { + const firstIdle = list.find(v => v.state === 'IDLE' && !v.tripPhase); + if (firstIdle) loadingVehicleId = firstIdle.id; + } else { + // Wenn das aktuell gewählte Fahrzeug nicht mehr IDLE ist (rollt), auf nächstes IDLE umschalten + const current = list.find(v => v.id === loadingVehicleId); + if (!current || current.state !== 'IDLE' || current.tripPhase) { + const firstIdle = list.find(v => v.state === 'IDLE' && !v.tripPhase); + loadingVehicleId = firstIdle ? firstIdle.id : null; + } } root.innerHTML = ''; + + // Phase 8d: Fahrzeugkauf-Button ab L2 (maxActiveContracts > 1). + // L1 nutzt Auto-Buy bei Balance ≥ 15k, kein manueller Kauf. + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) > 1) { + const btn = document.createElement('button'); + btn.className = 'ggs-btn-secondary lg-buy-vehicle-btn'; + btn.innerHTML = '🛒 Neues Fahrzeug kaufen'; + btn.addEventListener('click', openBuyVehicleDialog); + root.appendChild(btn); + } + + if (!list.length) { + const hint = document.createElement('div'); + hint.className = 'lg-hint'; + hint.textContent = pickText({ + standard: 'Keine Fahrzeuge verfügbar — kauf eins oder warte auf die nächste Runde.', + easy: 'Keine Fahrzeuge da. Kauf eines.', + }); + root.appendChild(hint); + return; + } // Falls intermodal-Auftrag selected: erforderlicher Mode für aktuellen Leg let neededMode = null; if (selectedContractId) { @@ -1106,37 +2101,88 @@ window.pickText = function(opts) { ? '
benötigt: ' + (neededMode === 'TRAIN' ? '🚆 Zug' : '🚚 LKW') + '
' : ''; - // (Phase 7-UX) Trip-Info bei MOVING/LOADING/UNLOADING - let tripHtml = ''; + // (Phase 8f++) Pflicht-Pause-Banner (ab L2 aktiv) + let breakHtml = ''; + if (v.onMandatoryBreak) { + const rem = Math.ceil(v.breakRemainingMin || 0); + breakHtml = '
☕ Lenkzeit-Pause · noch ' + + rem + ' min
'; + } + // (Phase 7-UX) Trip-Info bei MOVING/LOADING/UNLOADING + Multi-Tour + let tripHtml = breakHtml; if (v.tripPhase) { const c = (game.activeContracts || []).find(x => x.id === v.assignedContractId); + const isDriving = v.tripPhase.endsWith('_drive') || v.tripPhase === 'tour_driving'; const phaseLabel = v.tripPhase === 'pickup_drive' ? '🟡 Leerfahrt zum Abholen' : v.tripPhase === 'loading' ? '📦 Lädt' : v.tripPhase === 'delivery_drive' ? '🟢 Unterwegs' : v.tripPhase === 'unloading' ? '📥 Lädt ab' + : v.tripPhase === 'tour_driving' ? '🟢 Tour-Fahrt' + : v.tripPhase === 'tour_handling' ? '📦 Tour-Stop' : v.tripPhase; const phaseClass = v.tripPhase === 'pickup_drive' ? 'pickup' - : v.tripPhase === 'loading' || v.tripPhase === 'unloading' ? 'loading' + : v.tripPhase === 'loading' || v.tripPhase === 'unloading' || v.tripPhase === 'tour_handling' ? 'loading' : ''; const progress = Math.round((v.segmentProgress || 0) * 100); - let etaLine = ''; const route = (game.activeRoutes || []).find(r => r.id === v.activeRouteId); - if (route && v.tripPhase.endsWith('_drive')) { - const remainingMin = Math.round(route.totalDurationMinutes * (1 - (v.segmentProgress || 0))); + let remainingMin = 0; + let etaLine = ''; + if (route && isDriving) { + remainingMin = Math.round(route.totalDurationMinutes * (1 - (v.segmentProgress || 0))); const etaDate = new Date(new Date(game.simulationTime).getTime() + remainingMin * 60000); etaLine = ' · ETA ' + etaDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); - } else if ((v.tripPhase === 'loading' || v.tripPhase === 'unloading') && v.phaseRemainingMinutes > 0) { + } else if ((v.tripPhase === 'loading' || v.tripPhase === 'unloading' || v.tripPhase === 'tour_handling') + && v.phaseRemainingMinutes > 0) { etaLine = ' · noch ' + Math.ceil(v.phaseRemainingMinutes) + ' min'; } - const contractInfo = c - ? escapeHtml(c.code) + ' · ' + escapeHtml(_locName(c.originLocationId)) + ' → ' + escapeHtml(_locName(c.targetLocationId)) - : '—'; - tripHtml = '
' + + // Origin/Target-Info: bei Tour-Driving aus tourStops, sonst aus assignedContract + let contractInfo = '—'; + if (v.tripPhase === 'tour_driving' || v.tripPhase === 'tour_handling') { + const stops = v.tourStops || []; + const idx = v.currentStopIndex || 0; + const stop = stops[idx]; + const fromLoc = idx === 0 ? null : stops[idx - 1]; + if (stop) { + const fromName = fromLoc ? _locName(fromLoc.locationId) : _locName(v.currentLocationId); + contractInfo = 'Stop ' + (idx + 1) + '/' + stops.length + + ' · ' + escapeHtml(fromName) + ' → ' + escapeHtml(_locName(stop.locationId)); + } + } else if (c) { + contractInfo = escapeHtml(c.code) + ' · ' + + escapeHtml(_locName(c.originLocationId)) + ' → ' + escapeHtml(_locName(c.targetLocationId)); + } + + // Verbleibende Zeit als kleine Stunden-Zahl (nur bei Fahrt-Phasen) + const remainHrsLine = (isDriving && remainingMin > 0) + ? '
noch ' + (remainingMin / 60).toFixed(1).replace('.', ',') + ' h
' + : ''; + + // Strafen-Hinweis: Auftrag laeuft, dueTime ueberschritten oder kritisch + let strafeHtml = ''; + const cForPenalty = c || ((game.activeContracts || []) + .find(x => v.tourStops && v.tourStops.some(s => (s.contractIds || []).includes(x.id)))); + const cfgPen = (game._config || {}).latePenaltyOverride; + if (cForPenalty && cForPenalty.dueTime && cfgPen > 0) { + const nowMs = new Date(game.simulationTime).getTime(); + const dueMs = new Date(cForPenalty.dueTime).getTime(); + const lateH = (nowMs - dueMs) / 3600000; + if (lateH > 0) { + const est = Math.round((cForPenalty.rewardBase || 0) * cfgPen * lateH); + strafeHtml = '
⚠ Strafe wächst — bisher −' + fmtEuro(est) + '
'; + } else if (lateH > -2 && isDriving && remainingMin / 60 > -lateH) { + strafeHtml = '
⚠ Frist droht — Restzeit reicht nicht
'; + } + } + + tripHtml += '
' + '
' + phaseLabel + etaLine + '
' + '
' + contractInfo + '
' - + (v.tripPhase.endsWith('_drive') + + (isDriving ? '
' : '') + + remainHrsLine + + strafeHtml + '
'; } @@ -1145,7 +2191,7 @@ window.pickText = function(opts) { // Phase 8: Lademodus-Banner + Cargo-Hold + Tour-Actions let loadModeHtml = ''; if (isLoading) { - loadModeHtml += '
📥 Lademodus aktiv — klick auf Aufträge links, um sie zu laden.
'; + loadModeHtml += '
📥 Lademodus
Klicke auf Aufträge
'; loadModeHtml += renderCargoHold(v); // Tour-Kostenvorschau: didaktischer Hinweis fuer Anfaenger, auf Profi-Level (HintMode=NONE) // ausgeblendet, damit Kopfrechnen explizit verlangt wird. @@ -1161,15 +2207,15 @@ window.pickText = function(opts) { const sign = preMargin >= 0 ? '+' : '−'; const sumClass = preMargin >= 0 ? 'positive' : 'negative'; loadModeHtml += '
' - + '
Strecke (geschätzt)' + Math.round(est.km) + ' km
' - + '
Fahrkosten (geschätzt)−' + fmtEuro(est.cost) + '
' - + '
Erlöse (ohne Boni/Strafen)+' + fmtEuro(rewardSum) + '
' - + '
Vor-Marge' + sign + fmtEuro(Math.abs(preMargin)) + '
' + + '
Strecke ca.' + Math.round(est.km) + ' km
' + + '
Fahrkosten ca.−' + fmtEuro(est.cost).replace(' €', '') + '
' + + '
Erlöse +' + fmtEuro(rewardSum).replace(' €', '') + '
' + + '
Gewinn ca.' + sign + fmtEuro(Math.abs(preMargin)).replace(' €', '') + '
' + '
'; } const loaded = (v.loadedContractIds || []).length; loadModeHtml += '
' - + '' + + '' + '' + '
'; } @@ -1196,6 +2242,16 @@ window.pickText = function(opts) { '' + escapeHtml(vehicleStateLabel(v.state)) + ''; card.addEventListener('click', (e) => { + // Info-Button (i): zeigt Tooltip-artige Erklaerung an + if (e.target && e.target.classList && e.target.classList.contains('lg-info-btn')) { + e.stopPropagation(); + const key = e.target.getAttribute('data-info'); + const explanations = { + erloese: 'Erlöse = Summe der Auftragsbeträge ohne Bonus-Pillen und ohne Verspätungsstrafen. Tatsächlicher Eingang weicht ggf. ab.', + }; + showToast(explanations[key] || ''); + return; + } // Klick auf Tour-Action-Buttons separat behandeln if (e.target && e.target.id && e.target.id.indexOf('btnStartTour-') === 0) { e.stopPropagation(); @@ -1206,6 +2262,7 @@ window.pickText = function(opts) { LogistikEngine.startSimulation(game); } showToast(pickText({ standard: '🚀 Tour gestartet — ' + (v.tourStops || []).length + ' Stops.', easy: 'Das Fahrzeug fährt jetzt los.' })); + maybeMinigameHint(); } catch (err) { showToast(pickText({ standard: 'Tour fehlgeschlagen: ' + err.message, easy: 'Das geht nicht: ' + err.message })); } @@ -1247,7 +2304,266 @@ window.pickText = function(opts) { }); } - function renderPanels() { renderContracts(); renderVehicles(); } + function renderPanels() { renderContracts(); renderVehicles(); renderTripHistory(); renderVerfahrenBanner(); renderBonusPills(); } + + /* ---------- Bonus-Pills (Phase 8e++) ---------- */ + function renderBonusPills() { + const wrap = document.getElementById('bonusPills'); + if (!wrap) return; + const bonuses = game.pendingBonuses || []; + wrap.innerHTML = ''; + if (!bonuses.length) return; + bonuses.forEach(b => { + const pill = document.createElement('div'); + pill.className = 'lg-bonus-pill'; + pill.dataset.bonusId = b.id; + pill.textContent = '⚡ Ladebonus +' + b.minutes + ' min'; + pill.title = 'Klick: Info. Wird automatisch beim naechsten Laden/Entladen verwendet.'; + pill.addEventListener('click', (e) => { + e.stopPropagation(); + showBonusPopover(b, pill); + }); + wrap.appendChild(pill); + }); + } + function showBonusPopover(bonus, anchor) { + // Entferne alte Popover + const existing = document.querySelector('.lg-bonus-popover'); + if (existing) existing.remove(); + const pop = document.createElement('div'); + pop.className = 'lg-bonus-popover'; + pop.innerHTML = pickText({ + standard: 'Ladebonus +' + bonus.minutes + ' min
' + + 'Aus dem Einpark-Minispiel (' + bonus.source + ').
' + + 'Wird automatisch beim nächsten Laden oder Entladen genutzt — die Phase-Zeit verringert sich entsprechend.', + easy: '+' + bonus.minutes + ' Minuten Bonus.
' + + 'Wird automatisch beim Laden/Entladen benutzt.', + }); + document.getElementById('bonusPills').appendChild(pop); + setTimeout(() => { + document.addEventListener('click', function h(ev) { + if (!pop.contains(ev.target)) { pop.remove(); document.removeEventListener('click', h); } + }); + }, 10); + setTimeout(() => { if (pop.parentNode) pop.remove(); }, 5000); + } + /** Flug-Animation einer Bonus-Pill zum Fahrzeug-Marker. */ + function flyBonusToVehicle(minutesSaved, vehicleId) { + const v = (game.vehicles || []).find(x => x.id === vehicleId); + if (!v || typeof v.lat !== 'number') return; + const fromEl = document.querySelector('.lg-bonus-pill'); + const mapEl = document.getElementById('map'); + if (!mapEl) return; + const fromRect = fromEl ? fromEl.getBoundingClientRect() + : { left: mapEl.getBoundingClientRect().right - 140, top: mapEl.getBoundingClientRect().top + 20, width: 120, height: 26 }; + const point = map.latLngToContainerPoint([v.lat, v.lon]); + const mapRect = mapEl.getBoundingClientRect(); + const toX = mapRect.left + point.x; + const toY = mapRect.top + point.y; + const clone = document.createElement('div'); + clone.className = 'lg-bonus-pill lg-bonus-pill-fly'; + clone.textContent = '⚡ −' + minutesSaved + ' min'; + clone.style.left = fromRect.left + 'px'; + clone.style.top = fromRect.top + 'px'; + document.body.appendChild(clone); + requestAnimationFrame(() => { + clone.style.transform = 'translate(' + (toX - fromRect.left) + 'px, ' + (toY - fromRect.top) + 'px) scale(0.5)'; + clone.style.opacity = '0.2'; + }); + setTimeout(() => clone.remove(), 900); + } + + /* ---------- Verfahren-Ereignis-Banner (Phase 8e+) ---------- */ + function renderVerfahrenBanner() { + const banner = document.getElementById('verfahrenBanner'); + if (!banner) return; + const pending = (game.vehicles || []).find(v => v.verfahrenPending); + if (!pending) { banner.hidden = true; return; } + const target = (game.worldState.locations || []).find(l => l.id === pending.verfahrenTargetLocationId); + const targetName = target ? target.name : pending.verfahrenTargetLocationId; + banner.hidden = false; + banner.innerHTML = pickText({ + standard: '🤷 ' + escapeHtml(pending.verfahrenDriverLabel || 'Fahrer:in') + + ' hat sich verfahren! Bring sie nach ' + escapeHtml(targetName) + + ' — klick den Ort auf der Karte.', + easy: '🤷 ' + escapeHtml(pending.verfahrenDriverLabel || 'Fahrer:in') + + ' ist falsch gefahren. Ziel: ' + escapeHtml(targetName) + + '. Klick die Stadt auf der Karte.', + }); + } + + /* ---------- Trip-Historie als Pills (Phase 8e) ---------- */ + // Miete-Marker pro Sim-Tag (clientseitig akkumuliert, sobald die Sim + // einen Tagesübergang ueberschreitet). Visualisiert die Fixkosten. + const _rentMarkers = []; + let _lastRentTickDay = 0; + function checkDailyRentMarkers() { + if (!game.simulationStartTime) return; + const cfg = game._config || {}; + const perVeh = cfg.rentalCostPerDay || 0; + if (perVeh <= 0) return; + const elapsedH = (new Date(game.simulationTime).getTime() - + new Date(game.simulationStartTime).getTime()) / 3600000; + const currentDayBoundary = Math.floor(elapsedH / 24); + while (_lastRentTickDay < currentDayBoundary) { + _lastRentTickDay++; + const nVeh = (game.vehicles || []).length || 1; + const total = perVeh * nVeh; + _rentMarkers.push({ + day: _lastRentTickDay, + amount: total, + vehicles: nVeh, + // Zeitstempel = Tagesgrenze (Start + n*24h) + simTime: new Date(new Date(game.simulationStartTime).getTime() + _lastRentTickDay * 86400000).toISOString(), + }); + } + } + + function renderTripHistory() { + const strip = document.getElementById('tripHistoryStrip'); + const pills = document.getElementById('tripHistoryPills'); + const done = game.completedContracts || []; + if (!done.length && !_rentMarkers.length) { strip.hidden = true; return; } + strip.hidden = false; + pills.innerHTML = ''; + + // Trip-Items + Miete-Items zeitlich mischen + const items = []; + done.forEach((c, i) => items.push({ + kind: 'trip', simTime: c.deliveryTime || c.dueTime || game.simulationTime, + data: c, index: i + 1, + })); + _rentMarkers.forEach(m => items.push({ kind: 'rent', simTime: m.simTime, data: m })); + items.sort((a, b) => new Date(a.simTime).getTime() - new Date(b.simTime).getTime()); + + items.forEach(it => { + if (it.kind === 'trip') { + const c = it.data; + const net = Math.round(c.finalBalance || 0); + const cls = net >= 0 ? 'positive' : 'negative'; + const sign = net >= 0 ? '+' : '−'; + const pill = document.createElement('span'); + pill.className = 'lg-trip-pill ' + cls; + pill.textContent = 'Fahrt ' + it.index + ' · ' + sign + fmtEuro(Math.abs(net)); + pill.title = (c.code || c.id) + ': ' + _locName(c.originLocationId) + ' → ' + _locName(c.targetLocationId); + pill.addEventListener('click', () => openTripDetail(c, it.index)); + pills.appendChild(pill); + } else { + const m = it.data; + const pill = document.createElement('span'); + pill.className = 'lg-trip-pill rent'; + pill.textContent = 'Miete Tag ' + m.day + ' · −' + fmtEuro(m.amount); + pill.title = 'Tagesmiete für ' + m.vehicles + ' Fahrzeug' + (m.vehicles > 1 ? 'e' : '') + + ' à ' + fmtEuro((game._config || {}).rentalCostPerDay || 0); + pills.appendChild(pill); + } + }); + } + function openTripDetail(c, num) { + const body = document.getElementById('tripDetailBody'); + const net = Math.round(c.finalBalance || 0); + const lateH = c.lateHours || 0; + const durH = c.durationHours || 0; + const rows = [ + ['Fahrt', '#' + num + ' · ' + escapeHtml(c.code || c.id)], + ['Strecke', escapeHtml(_locName(c.originLocationId)) + ' → ' + escapeHtml(_locName(c.targetLocationId))], + ['Container', c.quantityContainers || 1], + ['Status', escapeHtml(c.state || '?')], + ['Erlös (Basis)', fmtEuro(c.rewardBase || 0)], + ['Netto', (net >= 0 ? '+' : '−') + fmtEuro(Math.abs(net))], + ['Dauer', durH.toFixed(1) + ' h'], + ['Verspätung', lateH > 0 ? lateH.toFixed(1) + ' h' : '—'], + ]; + body.innerHTML = rows.map(([k, v]) => '
' + escapeHtml(k) + '
' + v + '
').join(''); + document.getElementById('tripDetailTitle').textContent = 'Fahrt ' + num + ' · ' + (c.code || c.id); + document.getElementById('tripDetailOverlay').classList.add('open'); + } + document.getElementById('tripDetailClose').addEventListener('click', () => { + document.getElementById('tripDetailOverlay').classList.remove('open'); + }); + document.getElementById('tripDetailOverlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) e.currentTarget.classList.remove('open'); + }); + + /* ---------- Fahrzeugkauf-Dialog (Phase 8d, ab L2) ---------- */ + function openBuyVehicleDialog() { + const overlay = document.getElementById('buyVehicleOverlay'); + const listEl = document.getElementById('buyVehicleList'); + const hintEl = document.getElementById('buyVehicleHint'); + const cfg = game._config || {}; + const available = cfg.availableVehicleTypes || ['TRUCK_SMALL']; + const prices = LogistikEngine.VEHICLE_PRICES || {}; + const defaults = LogistikEngine.VEHICLE_DEFAULTS || {}; + + hintEl.textContent = pickText({ + standard: 'Kontostand: ' + fmtEuro(game.balance) + '. Wähle ein Fahrzeug — die Kosten werden sofort abgezogen.', + easy: 'Du hast ' + fmtEuro(game.balance) + ' auf dem Konto. Wähle ein Fahrzeug.', + }); + + // Train ist erst ab Tag 5 (Sim-Stunde 96) mietbar + const elapsedH = game.simulationStartTime + ? (new Date(game.simulationTime).getTime() - new Date(game.simulationStartTime).getTime()) / 3600000 + : 0; + const trainUnlocked = elapsedH >= 96; + const rows = [ + { mode: 'TRUCK_SMALL', icon: '🚚', name: 'Kleiner LKW', hint: '1 Container · 70 km/h · günstig pro km' }, + { mode: 'TRUCK_MID', icon: '🚛', name: 'Mittlerer LKW', hint: '2 Container · 65 km/h · gute Bündelung' }, + { mode: 'TRUCK_LARGE', icon: '🚛', name: 'Großer LKW', hint: '3 Container · 60 km/h · lohnt bei voller Ladung · Tagesmiete +40 %' }, + { mode: 'TRAIN_SMALL', icon: '🚂', name: 'Güterzug (klein)', + hint: '6 Container · 80 km/h · Spawn in Hamburg · Tagesmiete 3× — nur sinnvoll bei großen Aufträgen!' + + (trainUnlocked ? '' : ' · 🔒 ab Tag 5 verfügbar') }, + { mode: 'TRAIN', icon: '🚆', name: 'Güterzug', hint: '20 Container · 90 km/h · für L3' }, + ].filter(r => available.includes(r.mode)); + + listEl.innerHTML = rows.map(r => { + const price = prices[r.mode] || 0; + const isTrainLocked = (r.mode === 'TRAIN_SMALL' || r.mode === 'TRAIN') && !trainUnlocked; + const canAfford = (game.balance || 0) >= price; + const enabled = canAfford && !isTrainLocked; + const d = defaults[r.mode] || {}; + const priceTxt = fmtEuro(price); + const isTrain = (r.mode === 'TRAIN_SMALL' || r.mode === 'TRAIN'); + const buyLabel = isTrain ? 'Mieten' : 'Kaufen'; + return '
' + + '
' + r.icon + '
' + + '
' + + '' + escapeHtml(r.name) + '' + + '' + escapeHtml(r.hint) + ' · ' + d.costPerKm + '€/km + ' + d.costPerHour + '€/h' + + '
' + + '
' + priceTxt + '
' + + '' + + '
'; + }).join(''); + + // Click-Handler nur auf enabled Buttons + listEl.querySelectorAll('button[data-mode]').forEach(b => { + if (b.disabled) return; + b.addEventListener('click', () => { + const mode = b.getAttribute('data-mode'); + const r = LogistikEngine.buyVehicle(game, mode); + if (r.success) { + showToast(pickText({ + standard: '🛒 ' + r.vehicle.displayName + ' gekauft.', + easy: 'Neues Fahrzeug gekauft.', + })); + overlay.classList.remove('open'); + renderPanels(); + updateStatusUI(); + } else { + showToast('⚠ ' + r.reason); + } + }); + }); + + overlay.classList.add('open'); + } + document.getElementById('buyVehicleClose').addEventListener('click', () => { + document.getElementById('buyVehicleOverlay').classList.remove('open'); + }); + // Klick auf Overlay-Hintergrund schliesst Dialog + document.getElementById('buyVehicleOverlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget) e.currentTarget.classList.remove('open'); + }); function _locName(id) { const l = (game.worldState.locations || []).find(x => x.id === id); @@ -1266,6 +2582,8 @@ window.pickText = function(opts) { /* ---------- State-Labels (Phase 7-UX): Deutsch + Leichte Sprache ---------- */ const CONTRACT_STATE_LABELS = { + OFFERED: { standard: 'Angebot', easy: 'Angebot' }, + EXPIRED: { standard: 'verfallen', easy: 'abgelaufen' }, OPEN: { standard: 'offen', easy: 'frei' }, RESERVED: { standard: 'reserviert', easy: 'reserviert' }, ASSIGNED: { standard: 'zugewiesen', easy: 'zugeteilt' }, @@ -1345,11 +2663,23 @@ window.pickText = function(opts) { } function _routeDistance(originId, targetId) { + // Bevorzugt Roadnet-Dijkstra (entspricht der echten Fahrt). Fallback + // Luftlinie × 1.3, wenn Knoten nicht im Roadnet sind. + const road = (game.worldState && game.worldState.roadnet) || { nodes: [], edges: [] }; + const nodeOf = (locId) => (road.nodes || []).find(n => n.locationId === locId); + if (LogistikEngine && LogistikEngine.railShortestPath && road.nodes && road.nodes.length) { + const a = nodeOf(originId), b = nodeOf(targetId); + if (a && b) { + try { + const dij = LogistikEngine.railShortestPath(road, a.id, b.id); + if (dij && isFinite(dij.distanceKm) && dij.distanceKm > 0) return dij.distanceKm; + } catch (e) { /* fallback unten */ } + } + } const locs = game.worldState.locations || []; const o = locs.find(l => l.id === originId); const t = locs.find(l => l.id === targetId); if (!o || !t) return 0; - // Haversine × 1.3 (gleiche Logik wie engine.calculateRoute Phase 2) const R = 6371, dLat = (t.lat - o.lat) * Math.PI/180, dLon = (t.lon - o.lon) * Math.PI/180; const s = Math.sin(dLat/2)**2 + Math.cos(o.lat*Math.PI/180)*Math.cos(t.lat*Math.PI/180)*Math.sin(dLon/2)**2; return 2 * R * Math.atan2(Math.sqrt(s), Math.sqrt(1-s)) * 1.3; @@ -1363,6 +2693,27 @@ window.pickText = function(opts) { } /* ---------- Notifications aus dem Engine in Toasts überführen ---------- */ + /** Announce-Modal — 3.5s sichtbar, klickbar zum sofortigen Schließen. + * Für Meilensteine (Auto-Buy, Stufen-Wechsel, Level-Win). Auto-Boost + * pausiert sich nicht; Modal blockiert Eingabe optisch, Sim läuft weiter. */ + function announce(icon, title, text, sub) { + const o = document.getElementById('announceOverlay'); + if (!o) return; + document.getElementById('announceIcon').textContent = icon || '🎉'; + document.getElementById('announceTitle').textContent = title || ''; + document.getElementById('announceText').textContent = text || ''; + const subEl = document.getElementById('announceSub'); + if (sub) { subEl.hidden = false; subEl.textContent = sub; } else { subEl.hidden = true; } + o.classList.add('open'); + clearTimeout(announce._t); + announce._t = setTimeout(() => { o.classList.remove('open'); }, 3500); + } + document.getElementById('announceOverlay').addEventListener('click', (e) => { + if (e.target === e.currentTarget || e.target.closest('.lg-announce-modal')) { + e.currentTarget.classList.remove('open'); + } + }); + function drainNotifications() { if (!game.pendingNotifications || !game.pendingNotifications.length) return; const next = game.pendingNotifications.shift(); @@ -1372,12 +2723,191 @@ window.pickText = function(opts) { standard: next.messageShort + ' (' + fmtEuroSigned(next.net) + ')', easy: 'Auftrag fertig. Du hast ' + Math.abs(Math.round(next.net)) + ' Euro ' + (next.net >= 0 ? 'verdient.' : 'verloren.'), })); + // Voice: je nach Zielstadt die passende Line. Wenn das Level keine + // Strafen hat (latePenaltyOverride=0), wird "delivered_late" nicht + // gesprochen — sonst würde die Stimme auf L1 von einer Strafe reden, + // die gar nicht abgezogen wird. + const delivered = (game.completedContracts || []).slice(-1)[0]; + if (delivered) { + const targetId = delivered.targetLocationId; + const cfgPenForVoice = (game._config || {}).latePenaltyOverride || 0; + const isLate = cfgPenForVoice > 0 + && (delivered.state === 'LATE' || (delivered.finalBalance < 0 && next.net < 0)); + if (isLate) speak('delivered_late'); + else speak('delivered_at_' + targetId); + } } else if (next.type === 'EVENT_STARTED') { showToast(next.messageShort); } else if (next.type === 'SHIP_ARRIVED') { showToast(next.messageShort); } else if (next.type === 'LEG_TRANSFERRED') { showToast(next.messageShort); + } else if (next.type === 'VEHICLE_PURCHASED') { + announce('🎉', pickText({ standard: 'Neuer LKW!', easy: 'Neuer LKW!' }), + next.messageShort, + pickText({ standard: 'Mehr Aufträge gleichzeitig möglich.', easy: 'Du kannst mehr auf einmal machen.' })); + renderPanels(); + // Voice je nach Notification-ID: + // autorent_* = L1 Auto-Rent → second_truck_rented + // rent_train_* = L2 Train-Anmietung → train_rented + // buy_* = manueller Truck-Kauf L2 → kein Audio + if (next.id && next.id.indexOf('autorent_') === 0) speak('second_truck_rented'); + else if (next.id && next.id.indexOf('rent_train_') === 0) speak('train_rented'); + } else if (next.type === 'LOAD_BONUS_CONSUMED') { + flyBonusToVehicle(next.minutesSaved || 0, next.vehicleId); + showToast(next.messageShort); + } else if (next.type === 'CONTRACT_EXPIRED') { + showToast(next.messageShort); + } else if (next.type === 'REPAIR_COST') { + showToast(next.messageShort); + speak('minigame_touch'); + } else if (next.type === 'BREAK_STARTED') { + showToast(next.messageShort); + renderPanels(); + speak('mandatory_break_start'); + } else if (next.type === 'BREAK_ENDED') { + showToast(next.messageShort); + renderPanels(); + } else if (next.type === 'DRIVER_LOST') { + const vname = next.driverLabel || ''; + speak(vname.toLowerCase().indexOf('anna') >= 0 ? 'verfahren_anna' : 'verfahren_tom'); + } else if (next.type === 'DRIVER_FOUND') { + speak('verfahren_ok'); + } else if (next.type === 'DRIVER_WRONG_TIP') { + speak('verfahren_wrong'); + } + } + + /* ---------- Info-Cards (Phase 8f++) ---------- + Beim Uebergang eines Auftrags in CONTRACT_STATE.LOADING (= Fahrzeug + laedt am Pickup) blenden wir fuer 5 Sek eine Card mit Bild der + Zielstadt + Didaktik-Text ein. Atlas-Pattern (Mail 22:00). */ + const _shownInfocardForContract = new Set(); + function watchContractLoading() { + const contracts = game.activeContracts || []; + contracts.forEach(c => { + if (c.state !== 'LOADING') return; + if (_shownInfocardForContract.has(c.id)) return; + _shownInfocardForContract.add(c.id); + showInfoCard(c); + }); + // Aufraeumen: IDs, die nicht mehr aktiv sind, aus dem Set raus + // (damit Wiederverwendung von IDs — unwahrscheinlich, aber sicher — OK ist) + const activeIds = new Set(contracts.map(c => c.id)); + for (const id of Array.from(_shownInfocardForContract)) { + if (!activeIds.has(id)) _shownInfocardForContract.delete(id); + } + } + + function showInfoCard(contract) { + const locs = (game.worldState && game.worldState.locations) || []; + const dest = locs.find(l => l.id === contract.targetLocationId); + if (!dest) return; + const origin = locs.find(l => l.id === contract.originLocationId); + _renderInfoCard(dest, origin, contract); + } + + /** Info-Card beim Marker-Klick: reines Stadt-Portrait ohne Auftragskontext. */ + function showCityCard(loc) { + if (!loc) return; + _renderInfoCard(loc, null, null); + } + + function _renderInfoCard(dest, origin, contract) { + const container = document.getElementById('lgInfocards'); + if (!container) return; + // Max 2 Cards gleichzeitig — aelteste weicht (FIFO). + const existing = container.querySelectorAll('.lg-infocard'); + for (let i = 0; i <= existing.length - 2; i++) existing[i].remove(); + const card = document.createElement('div'); + card.className = 'lg-infocard'; + + // Bild-Bereich: immer als DIV starten (zeigt Stadtnamen). Wenn das Bild + // erfolgreich laedt, wird's als background-image drauflegt und der Name + // verschwindet. Wenn das Bild 404 ist, bleibt der Name + Farbverlauf. + const imgWrap = document.createElement('div'); + imgWrap.className = 'lg-infocard-img'; + imgWrap.textContent = dest.name; + + const imgBase = (window.LOGISTIK_BASE || '') + 'assets/cities/'; + const imgUrl = imgBase + encodeURIComponent(dest.id) + '.png'; + const probe = new Image(); + probe.onload = function() { + imgWrap.style.backgroundImage = 'url("' + imgUrl + '")'; + imgWrap.style.backgroundSize = 'cover'; + imgWrap.style.backgroundPosition = 'center'; + imgWrap.textContent = ''; + }; + probe.src = imgUrl; + + const body = document.createElement('div'); + body.className = 'lg-infocard-body'; + const title = (contract && origin) + ? escapeHtml(origin.name) + ' → ' + escapeHtml(dest.name) + : escapeHtml(dest.name); + let statsHtml = ''; + if (contract) { + const reward = Math.round(contract.rewardBase || 0); + let dueH = 0; + if (contract.dueTime && game.simulationTime) { + dueH = Math.max(0, + (new Date(contract.dueTime).getTime() - new Date(game.simulationTime).getTime()) / 36e5); + } + statsHtml = + '
' + + '💰 ' + reward + ' €' + + (dueH > 0 ? '' + dueH.toFixed(1) + ' h' : '') + + '
'; + } else { + const typeLabel = { + CAPITAL: 'Hauptstadt', CITY: 'Stadt', PORT: 'Hafen', + TERMINAL: 'Terminal', HUB: 'Knoten', + }[dest.type] || dest.type; + statsHtml = '
' + + '' + escapeHtml(typeLabel) + '' + + (dest.countryId ? '' + escapeHtml(dest.countryId) + '' : '') + + '
'; + } + body.innerHTML = + '

' + title + '

' + + (dest.didacticInfo ? '

' + escapeHtml(dest.didacticInfo) + '

' : '') + + statsHtml; + + card.appendChild(imgWrap); + card.appendChild(body); + card.addEventListener('click', () => card.remove()); + container.appendChild(card); + setTimeout(() => { if (card.parentNode) card.remove(); }, 10500); + } + + /** Stufen-Wechsel-Announce: wenn sich L1-Stufe aendert, Gratulation. */ + let lastAnnouncedStufe = null; + function checkStufeChange() { + const cfg = game._config || {}; + if ((cfg.maxActiveContracts || 1) !== 1) return; // nur L1 + if (!game.progression) return; + const round = game.progression.round || 1; + const stufen = [ + null, + { n: 1, name: 'Erster Auftrag', minRound: 1 }, + { n: 2, name: 'Mehrere zur Wahl', minRound: 2 }, + { n: 3, name: 'Weitere Strecken', minRound: 4 }, + { n: 4, name: 'Mehr Fahrzeuge', minRound: 7 }, + { n: 5, name: 'Soft-Ramp → L2', minRound: 11 }, + ]; + let stufe = stufen[1]; + for (let i = 1; i < stufen.length; i++) { + if (stufen[i] && round >= stufen[i].minRound) stufe = stufen[i]; + } + if (lastAnnouncedStufe === null) { lastAnnouncedStufe = stufe.n; return; } + if (stufe.n > lastAnnouncedStufe) { + announce('🎯', pickText({ standard: 'Neue Stufe erreicht!', easy: 'Du bist weiter!' }), + 'Stufe ' + stufe.n + ': ' + stufe.name, + pickText({ + standard: 'Das Auftragsangebot wird größer und die Strecken länger.', + easy: 'Mehr Aufträge und weitere Orte.', + })); + lastAnnouncedStufe = stufe.n; } } @@ -1396,28 +2926,63 @@ window.pickText = function(opts) { }).join(''); } - /* ---------- Time-Controls (Speed inkl. Pause als 0×) ---------- - Konvention aus Template: Pause ist data-speed="0" (timeScale=0). - Der 1×-Button startet die Sim, falls noch in PLANNING. */ + /* ---------- Time-Controls: 3 Modi (Pause / Auto / Turbo) ---------- + - Pause: alles steht (timeScale 0) + - Auto: Sim-Tempo wird automatisch bestimmt — 1× bei Entscheidung / + Verfahren, 8× bei reiner Fahrt. Label zeigt aktuellen Wert. + - Turbo: 16× (Entwickler-Knopf, kommt spaeter raus) */ const speedBtns = document.querySelectorAll('#speedCtrl .ggs-speed-btn'); function setActiveSpeedBtn(btn) { speedBtns.forEach(b => b.classList.toggle('active', b === btn)); } + let playerMode = 'auto'; // 'pause' | 'auto' | 'fixed8' | 'turbo' + speedBtns.forEach(btn => { btn.addEventListener('click', () => { - const scale = parseInt(btn.getAttribute('data-speed'), 10); - // Beim ersten Tempo > 0 die Sim starten, falls noch nicht RUNNING - if (scale > 0 && (game.state === LogistikEngine.STATE.PLANNING || - game.state === LogistikEngine.STATE.READY)) { + const mode = btn.getAttribute('data-mode'); + // Auto/Turbo startet Sim aus PLANNING/READY + if (mode !== 'pause' && (game.state === LogistikEngine.STATE.PLANNING || + game.state === LogistikEngine.STATE.READY)) { if (game.state === LogistikEngine.STATE.READY) LogistikEngine.startPlanning(game); LogistikEngine.startSimulation(game); } - LogistikEngine.setTimeScale(game, scale); + playerMode = mode; setActiveSpeedBtn(btn); updateStatusUI(); }); }); + /** Effektive TimeScale nach Modus: + * pause → 0 + * turbo → 16 (Dev-Flag) + * auto → Verfahren: 1×, reine Fahrt (alle Fahrzeuge in tripPhase): 8×, + * sonst 1× (Entscheidungsfenster) */ + function effectiveTimeScale() { + if (playerMode === 'pause') return 0; + if (playerMode === 'turbo') return 16; + if (playerMode === 'fixed8') return 8; + const vehicles = game.vehicles || []; + if (vehicles.some(v => v.verfahrenPending)) return 1; + if (vehicles.length && vehicles.every(v => v.tripPhase)) return 8; + return 1; + } + function updateAutoBoost() { + const autoLabel = document.getElementById('autoModeLabel'); + if (game.state !== LogistikEngine.STATE.RUNNING) { + if (autoLabel) autoLabel.textContent = 'Auto'; + return; + } + const target = effectiveTimeScale(); + if (target !== game.timeScale) { + LogistikEngine.setTimeScale(game, target); + } + // Label des Auto-Buttons zeigt den aktuell wirksamen Faktor im Auto-Modus. + // In Pause/Turbo zeigt es nur „Auto" (der andere Button hat die Info). + if (autoLabel) { + autoLabel.textContent = (playerMode === 'auto') ? ('Auto ' + target + '×') : 'Auto'; + } + } + /* ---------- Lehrplan-Link (Modul-Detailseite, Anker lehrplan-bezug) ---------- */ const lehrplanLink = document.getElementById('lehrplanLink'); if (lehrplanLink) { @@ -1429,45 +2994,234 @@ window.pickText = function(opts) { const levelBadge = document.getElementById('levelBadge'); if (levelBadge) levelBadge.textContent = 'Level ' + levelNum; - /* ---------- Save/Load (Phase 7b: Server-API + localStorage-Fallback) ---------- */ - document.getElementById('btnSave').addEventListener('click', async () => { - try { - const raw = JSON.stringify(LogistikEngine.serialize(game)); - localStorage.setItem('logistik:save:manual', raw); - const res = await LG_API.saveSlot(1, raw); - if (res && res.saved) { - showToast(pickText({ standard: '✓ Gespeichert (Server + lokal).', easy: 'Gespeichert.' })); - } else { - showToast(pickText({ standard: '✓ Lokal gespeichert (Server nicht erreichbar).', easy: 'Gespeichert.' })); - } - console.info('[Logistik] Durchgang gespeichert.'); - } catch (e) { alert('Speichern fehlgeschlagen: ' + e.message); } - }); - document.getElementById('btnLoad').addEventListener('click', async () => { - const serverResp = await LG_API.loadSlot(1); - const serverData = serverResp && serverResp.data; - const raw = serverData || localStorage.getItem('logistik:save:manual'); - if (!raw) return alert('Kein gespeicherter Durchgang gefunden.'); - if (!confirm(pickText({ - standard: 'Gespeicherten Durchgang laden? Aktueller Fortschritt geht verloren.', - easy: 'Alten Stand wieder holen? Der jetzige Fortschritt ist dann weg.', - }))) return; - try { - const restored = LogistikEngine.deserialize(typeof raw === 'string' ? raw : JSON.stringify(raw)); - // Listener-Array erhalten (serialize() strippt sie) -- Closures im UI - // bleiben sonst ohne State-Hook. - const keepListeners = game._stateChangeListeners || []; - Object.keys(game).forEach(k => { if (!(k in restored) && k !== '_stateChangeListeners') delete game[k]; }); - Object.assign(game, restored); - game._stateChangeListeners = keepListeners; - showToast(pickText({ standard: '✓ Durchgang geladen.', easy: 'Alter Stand ist wieder da.' })); - renderPanels(); - updateStatusUI(); - updateVehicleMarkers(); - } catch (e) { - alert('Laden fehlgeschlagen: ' + e.message); + /* ---------- Level-Splash (Phase 8f++) ---------- + Einmaliger Intro-Screen pro Level: Bild + Was / Wo / Ziel. Die + Simulation startet nach Click auf „Los geht's". */ + const SPLASH_CONTENT = { + 2: { + title: 'Level 2 — Wachstum', + subtitle: 'Du hast einen zweiten Lieferwagen bekommen. Mehr Aufträge, mehr Auswahl, sechs Tage Zeit.', + img: 'level-2.png', + sections: [ + { + icon: '🚚', + title: 'Was?', + text: 'Du startest mit zwei Fahrzeugen: einem kleinen Lieferwagen (1 Container) in Wien und einem mittleren (2 Container) in München. Manche Aufträge brauchen 2 Container — die kann nur der mittlere Lieferwagen alleine nehmen, oder du belädst ihn mit zwei kleinen Aufträgen vom selben Pickup-Ort.', + }, + { + icon: '🗺️', + title: 'Wo?', + text: 'Wien, München, Innsbruck, Salzburg, Graz, Mailand, Berlin. Längere Strecken sind möglich — und können sich richtig lohnen.', + }, + { + icon: '🏁', + title: 'Ziel', + text: '6 Tage durchhalten. Pleite gehen heißt verloren. Endbilanz bestimmt deine Sterne (1 bis 5). Tagesmiete: 400 € pro Fahrzeug. Ab 15 000 € Konto kommt automatisch ein dritter Lieferwagen dazu.', + }, + { + icon: '🚂', + title: 'Tipp — Der Güterzug', + text: 'Ab dem 5. Tag kannst du einen kleinen Güterzug dazumieten (6 Container Kapazität). Vorsicht: er ist nur dann profitabel, wenn er auch volle Aufträge bekommt. Sonst frisst die Tagesmiete den Gewinn.', + }, + ], + }, + 1: { + title: 'Level 1 — Einstieg in die Spedition', + subtitle: 'Deine erste Woche als Disponent:in eines kleinen Logistik­unternehmens.', + img: 'level-1.png', + sections: [ + { + icon: '📦', + title: 'Was?', + text: 'Du arbeitest mit einem gemieteten Lieferwagen, der in Wien bereitsteht. Klicke einen offenen Auftrag an — dein Fahrzeug wird automatisch beladen und fährt los. Jeder erledigte Auftrag bringt Geld aufs Konto. Ab 10 000 € Konto kommt automatisch ein zweiter Lieferwagen dazu — die Tagesmiete verdoppelt sich dann.', + }, + { + icon: '🗺️', + title: 'Wo?', + text: 'Mitteleuropa: Wien, Salzburg, München, Innsbruck, Graz. In den späteren Runden kommen Mailand und Berlin dazu. Deine Fahrzeuge nutzen echte Autobahn-Strecken auf der Karte.', + }, + { + icon: '🏁', + title: 'Ziel', + text: '4 Tage durchhalten. Wer pleite geht (Konto auf 0 € oder weniger), scheidet aus. Sonst entscheidet die Endbilanz: je höher, desto mehr Sterne (1 bis 5). Achtung: die Tages­miete von 500 € pro Fahrzeug läuft auch dann, wenn der Lieferwagen steht.', + }, + { + icon: '🎮', + title: 'Tipp — Minispiel', + text: 'Über den 🎮-Button kannst du das Einpark-Minispiel starten. Wer schnell und sauber andockt, bekommt eine Bonus-Pill oben rechts: sie verkürzt die nächste Ladezeit um bis zu 45 min. Touchierst du dabei einen anderen LKW oder die Rampe, kostet das 1 000 € Reparatur und der Bonus entfällt.', + }, + ], + }, + // Stufen 2 und 3: Bilder + Texte folgen + }; + + function showLevelSplash(lvl) { + const c = SPLASH_CONTENT[lvl]; + const overlay = document.getElementById('levelSplashOverlay'); + if (!c || !overlay) return; + const img = document.getElementById('lgSplashImg'); + if (img) { + img.src = (window.LOGISTIK_BASE || '') + 'assets/splash/' + c.img; + img.alt = c.title; } + document.getElementById('lgSplashTitle').textContent = c.title; + document.getElementById('lgSplashSubtitle').textContent = c.subtitle || ''; + const sec = document.getElementById('lgSplashSections'); + sec.innerHTML = ''; + (c.sections || []).forEach(s => { + const el = document.createElement('div'); + el.className = 'lg-splash-section'; + el.innerHTML = + '
' + s.icon + '
' + + '
' + + '

' + escapeHtml(s.title) + '

' + + '

' + escapeHtml(s.text) + '

' + + '
'; + sec.appendChild(el); + }); + overlay.classList.add('open'); + } + document.getElementById('lgSplashStart').addEventListener('click', () => { + document.getElementById('levelSplashOverlay').classList.remove('open'); }); + // Splash einmal beim Level-Start anzeigen (nur wenn Content vorhanden) + showLevelSplash(levelNum); + + /* ---------- Voice-Pipeline (Phase 8f++) ---------- + Sequentielle Audio-Wiedergabe via OpenAI-TTS-generierten MP3s. + Eine Queue — nie zwei gleichzeitig. Easy-Language nutzt andere Dateien. + Toggle 🔊/🔇 im Header, Default: an. */ + const VOICE_BASE = (window.LOGISTIK_BASE || '') + 'assets/audio/voice/de/'; + const voiceQueue = []; + let voicePlaying = false; + let voiceAudio = null; + let voiceEnabled = (typeof localStorage !== 'undefined') + ? (localStorage.getItem('lg_voice_off') !== '1') + : true; + + // Music-Auto-Start nach Welcome: gemerkter Key wird gespielt, sobald + // die Voice-Queue (zum ersten Mal) leerlaeuft. + let _pendingMusicStart = null; + + function speak(lineId, opts) { + if (!voiceEnabled || !lineId) return; + const easy = !!window.STUDENT_EASY; + const file = VOICE_BASE + lineId + (easy ? '_easy' : '') + '.mp3'; + const wasIdle = (voiceQueue.length === 0 && !voicePlaying); + voiceQueue.push({ file, onStart: opts && opts.onStart, onEnd: opts && opts.onEnd }); + if (wasIdle && typeof window.lgAmbientDuck === 'function') { + window.lgAmbientDuck(); + } + _voiceNext(); + } + function _voiceNext() { + if (voicePlaying) return; + if (!voiceQueue.length) { + // Queue leer: Musik wieder hochfaden ODER (falls ein Auto-Start + // gemerkt wurde) erst Musik starten und sofort auf voller Lautstaerke. + if (_pendingMusicStart && typeof window.lgAmbientStart === 'function') { + window.lgAmbientStart(_pendingMusicStart); + _pendingMusicStart = null; + } + if (typeof window.lgAmbientUnduck === 'function') window.lgAmbientUnduck(); + return; + } + const item = voiceQueue.shift(); + voicePlaying = true; + voiceAudio = new Audio(item.file); + voiceAudio.volume = 0.9; + if (item.onStart) { try { item.onStart(); } catch (_) {} } + const done = () => { + voicePlaying = false; voiceAudio = null; + if (item.onEnd) { try { item.onEnd(); } catch (_) {} } + _voiceNext(); + }; + voiceAudio.addEventListener('ended', done); + voiceAudio.addEventListener('error', done); + voiceAudio.play().catch(done); + } + function voiceStop() { + voiceQueue.length = 0; + if (voiceAudio) { try { voiceAudio.pause(); } catch (_) {} voiceAudio = null; } + voicePlaying = false; + if (typeof window.lgAmbientUnduck === 'function') window.lgAmbientUnduck(); + } + + /* Voice-Toggle im Header */ + const voiceBtn = document.getElementById('btnVoiceToggle'); + function renderVoiceBtn() { + if (voiceBtn) voiceBtn.textContent = voiceEnabled ? '🔊' : '🔇'; + } + if (voiceBtn) { + voiceBtn.addEventListener('click', () => { + voiceEnabled = !voiceEnabled; + if (!voiceEnabled) voiceStop(); + try { localStorage.setItem('lg_voice_off', voiceEnabled ? '0' : '1'); } catch (_) {} + renderVoiceBtn(); + }); + renderVoiceBtn(); + } + + // Welcome-Line + Auto-Start Country-Playlist nach dem Splash-Klick. + // Nach Ende der Welcome-Voice (Queue leer) startet die gespeicherte + // Playlist via lgAmbientStart automatisch. + const splashBtnVoice = document.getElementById('lgSplashStart'); + if (splashBtnVoice) { + splashBtnVoice.addEventListener('click', () => { + _pendingMusicStart = 'country'; + speak('welcome'); + }); + } + + /* Minispiel-Hinweis: bei der ersten Tour erstmals, danach alle 6 Touren + erneut. Während der Sprachausgabe blinkt der 🎮-Button im Header. */ + let _toursStartedSoFar = 0; + function maybeMinigameHint() { + _toursStartedSoFar++; + const fire = (_toursStartedSoFar === 1) || (_toursStartedSoFar % 6 === 0); + if (!fire) return; + const btn = document.getElementById('btnMinigame'); + speak('minigame_hint', { + onStart: () => { if (btn) btn.classList.add('lg-blink'); }, + onEnd: () => { if (btn) btn.classList.remove('lg-blink'); }, + }); + } + + /* Tag-Meilenstein-Watcher: pro Sim-Tag-Ende einmal feuern. Plus + "Halbzeit" (50% des Limits) und "letzter Tag" (Limit-24h). Generisch + fuer L1 (4 Tage) und L2 (6 Tage). */ + const _spokenMilestones = { + day1: false, day2: false, day3: false, day4: false, day5: false, + halftime: false, lastDay: false, lastHour: false, + }; + function checkTimeMilestones() { + if (!game.simulationStartTime || !game.simulationTime) return; + const elapsedH = (new Date(game.simulationTime).getTime() - + new Date(game.simulationStartTime).getTime()) / 3600000; + const limitH = (game._config || {}).timeLimitHours || 96; + const totalDays = Math.round(limitH / 24); + if (!_spokenMilestones.day1 && elapsedH >= 24) { _spokenMilestones.day1 = true; speak('day_1_end'); } + if (!_spokenMilestones.day2 && elapsedH >= 48) { _spokenMilestones.day2 = true; speak('day_2_end'); } + if (!_spokenMilestones.day3 && elapsedH >= 72) { _spokenMilestones.day3 = true; speak('day_3_end'); } + if (totalDays >= 5 && !_spokenMilestones.day4 && elapsedH >= 96) { _spokenMilestones.day4 = true; speak('day_4_end'); } + if (totalDays >= 6 && !_spokenMilestones.day5 && elapsedH >= 120) { _spokenMilestones.day5 = true; speak('day_5_end'); } + // Halbzeit: nur wenn vom Tagesende-Trigger nicht bereits abgedeckt + const halfH = limitH / 2; + if (totalDays !== 4 && !_spokenMilestones.halftime + && elapsedH >= halfH && elapsedH < halfH + 6) { + _spokenMilestones.halftime = true; speak('halftime'); + } + // Letzter Tag (24h vor Schluss). Bei 4-Tage-Level passt day_3_end, sonst extra. + if (!_spokenMilestones.lastDay && elapsedH >= limitH - 24 && totalDays !== 4) { + _spokenMilestones.lastDay = true; speak('last_day'); + } + if (!_spokenMilestones.lastHour && elapsedH >= limitH - 1) { + _spokenMilestones.lastHour = true; speak('last_hour'); + } + } + + /* Save/Load entfernt (Thomas 2026-04-25). Auto-Save mit Resume steht + auf der ToDo-Liste — siehe project_logistik_state.md */ /* ---------- Analytics-Drain: game.pendingAnalytics -> Server ---------- Pro Tick maximal 1 Eintrag senden. Fehlgeschlagene Eintraege werden @@ -1492,10 +3246,14 @@ window.pickText = function(opts) { let lastAnalyticsRender = 0; // (Phase 7-UX) Throttling auf 1s let lastStatusRender = 0; // Status-Leiste auf 250ms let lastAnalyticsDrain = 0; // POST an Server auf 2s + let lastDeadlineRefresh = 0; // Frist-Balken alle 500ms tickend + let lastAssessmentBeat = 0; // Integrations-Vertrag §6.3: running-Heartbeat 30s function loop(now) { const dt = now - lastTime; lastTime = now; LogistikEngine.tick(game, dt); + // Auto-Boost bei reiner Fahrtzeit (8× wenn alle Fahrzeuge busy, kein Verfahren) + updateAutoBoost(); // Status-Leiste nur alle 250ms (flackert sonst bei hoher Simulationsrate) if (now - lastStatusRender > 250) { updateStatusUI(); @@ -1504,14 +3262,44 @@ window.pickText = function(opts) { syncRouteLines(); updateVehicleMarkers(); drainNotifications(); + checkStufeChange(); + watchContractLoading(); + checkTimeMilestones(); + if (now - lastDeadlineRefresh > 500) { + updateDeadlineBars(); + lastDeadlineRefresh = now; + } + const prevRentDay = _lastRentTickDay; + checkDailyRentMarkers(); + if (_lastRentTickDay !== prevRentDay) renderTripHistory(); if (now - lastAnalyticsDrain > 2000) { drainAnalyticsQueue(); lastAnalyticsDrain = now; } + // Assessment-Heartbeat alle 30s (Integrations-Vertrag §6.3) + if (now - lastAssessmentBeat > 30000 && !game.state.startsWith('LEVEL_')) { + const a = (typeof computeAnalytics === 'function') ? computeAnalytics() : {}; + LG_API.postAssessment('running', { + durationMs: Math.round(now - sessionStartMs), + completedPhases: ['started', 'running'], + kennzahlen: { + level: levelNum, + balance: Math.round(game.balance || 0), + completed: a.completed || 0, + active: a.active || 0, + lateRatio: a.lateRatio || '–', + emptyRatio: a.emptyRatio || '–', + round: (game.progression && game.progression.round) || 1, + }, + }); + lastAssessmentBeat = now; + } // Panels nur neu rendern, wenn sich Vehicle/Contract-States geändert haben - const sig = (game.vehicles || []).map(v => v.id + ':' + v.state).join('|') + '#' + + // (inkl. Verfahren-Pending). + const sig = (game.vehicles || []).map(v => v.id + ':' + v.state + ':' + (v.verfahrenPending ? 'lost' : 'ok')).join('|') + '#' + (game.activeContracts || []).map(c => c.id + ':' + c.state).join('|') + '#' + - (game.completedContracts || []).length; + (game.completedContracts || []).length + '#' + + (game.pendingBonuses || []).length; if (sig !== lastVehicleStateSig) { lastVehicleStateSig = sig; renderPanels(); @@ -1554,9 +3342,9 @@ window.pickText = function(opts) { Rückwärts-Lenkinversion (wie in echt). Ziel: LKW-Heck in die Andock-Bucht am rechten Rand. ================================================================= */ - const cfgMG = game._config && game._config.minigamesEnabled; const btnMinigame = document.getElementById('btnMinigame'); - if (cfgMG) btnMinigame.hidden = false; + // LKW-Einpark-Minigame ist auf allen Levels verfügbar (Rampe gehört zum Job) + btnMinigame.hidden = false; const overlay = document.getElementById('minigameOverlay'); const canvas = document.getElementById('minigameCanvas'); @@ -1578,9 +3366,10 @@ window.pickText = function(opts) { steerReturn: Math.PI * 2.0, // Bucht: mittig-rechts, groß genug für den LKW längs rein rampX: 360, rampY: 120, rampW: 120, rampH: 60, - // Grüne Zielzone — so groß wie der LKW + 3 px Toleranz in jede Richtung - // = 66 × 28, zentriert in der Bucht (Mittelpunkt 420/150) - greenZone: { x: 387, y: 136, w: 66, h: 28 }, + // Grüne Zielzone (Thomas 2026-04-25): GANZER Truck muss drin liegen. + // Truck 60×22 → Zone 76×34 (≈ +8 px je Seite Toleranz). Truck-Mitte + // muss in [412..428] × [144..156] landen (16 × 12 px Sweet-Spot). + greenZone: { x: 382, y: 133, w: 76, h: 34 }, // Hindernisse außerhalb des direkten Wegs (y=150 ist frei zwischen 100..350) obstacles: [ { x: 60, y: 40, w: 62, h: 22, color: '#7a7a7a' }, @@ -1713,6 +3502,7 @@ window.pickText = function(opts) { steer: 0, crashed: false, docked: false, + _touched: false, // gesetzt bei erster Kollision (1000 €-Strafe, kein Park-Bonus) result: null, // Timer (Phase 8-UX): Stopwatch für Ladezeit-Konzept timerStarted: false, @@ -1774,33 +3564,32 @@ window.pickText = function(opts) { // Truck-AABB approximativ (Rotation ignoriert — reicht didaktisch) const hx = MG.truckW / 2, hy = MG.truckH / 2; let tL = s.x - hx, tR = s.x + hx, tT = s.y - hy, tB = s.y + hy; - // Kollisionen mit geparkten LKWs → Crash = Game Over („Kollege übernimmt") + // Kollisionen mit geparkten LKWs / Active Mover / Bucht-Wand → Touchiert + // (−1000 € Reparatur, Minispiel läuft weiter, kein Game Over). 1-s-Cooldown + // verhindert wiederholtes Abbuchen pro Frame bei anhaltendem Kontakt. for (const o of MG.obstacles) { const oL = o.x, oR = o.x + o.w, oT = o.y, oB = o.y + o.h; if (tR > oL && tL < oR && tB > oT && tT < oB) { - mgGameOver('crash_vehicle'); - return; + mgTouch('parked', s, o); + break; } } - // Kollision mit Active Mover → Crash if (MG.activeMover) { const m = MG.activeMover; const mhx = m.w / 2, mhy = m.h / 2; const mL = m.x - mhx, mR = m.x + mhx, mT = m.y - mhy, mB = m.y + mhy; if (tR > mL && tL < mR && tB > mT && tT < mB) { - mgGameOver('crash_vehicle'); - return; + mgTouch('mover', s, { x: m.x - mhx, y: m.y - mhy, w: m.w, h: m.h }); } } - // Bucht-Wand-Kollision: Crash nur bei hoher Geschwindigkeit (>55) UND - // tiefem Eindringen (>6 px in die Wand). Sonst nur Bremse — damit - // sanftes Einfahren und leichtes Schrammen nie als Crash enden. + // Bucht-Wand-Kollision: bei hoher Geschwindigkeit UND tiefem Eindringen + // gilt es als Touchieren (−1000 €). Sanftes Einfahren / leichtes Schrammen + // bleibt ohne Strafe. const deepTop = s.y < r.y - 4 && tB > r.y + 6 && tR > r.x + 4 && tL < r.x + r.w - 4; const deepBottom = s.y > r.y + r.h + 4 && tT < r.y + r.h - 6 && tR > r.x + 4 && tL < r.x + r.w - 4; const deepRight = s.x > r.x + r.w + 4 && tL < r.x + r.w - 6 && tB > r.y + 4 && tT < r.y + r.h - 4; if ((deepTop || deepBottom || deepRight) && Math.abs(s.v) > 55) { - mgGameOver('crash_bay'); - return; + mgTouch('bay', s, null); } // Sanfte Bremse + Reposition bei normalem Kontakt if (tB > r.y && tT < r.y && tR > r.x && tL < r.x + r.w) { @@ -1862,12 +3651,20 @@ window.pickText = function(opts) { // - langsam + 0.3s stabil if (s.timerStarted) { const g = MG.greenZone; - const inGreen = s.x >= g.x && s.x <= g.x + g.w && s.y >= g.y && s.y <= g.y + g.h; + // Strenger Check (Thomas 2026-04-25): der GANZE Truck muss in der + // gruenen Zone liegen, nicht nur die Mitte. Bei korrekter Andock- + // Orientierung (~180°) ist die Truck-AABB axisaligned, daher reicht + // ein Eckenpruefer. + const hx = MG.truckW / 2, hy = MG.truckH / 2; + const tL = s.x - hx, tR = s.x + hx; + const tT = s.y - hy, tB = s.y + hy; + const inGreen = tL >= g.x && tR <= g.x + g.w + && tT >= g.y && tB <= g.y + g.h; const deg = ((s.angle * 180 / Math.PI) % 360 + 540) % 360 - 180; // rückseitig = Winkel nahe ±180°. angleDev misst Abweichung vom 180°-Soll. const angleDev = Math.abs(180 - Math.abs(deg)); - const stable = Math.abs(s.v) < 20; - const rearCorrect = angleDev < 18; + const stable = Math.abs(s.v) < 10; + const rearCorrect = angleDev < 9; s._dockStatus = { inGreen, rearCorrect, stable, angleDev }; if (inGreen && rearCorrect && stable) { s.stableInZoneSec = (s.stableInZoneSec || 0) + dt; @@ -2132,36 +3929,72 @@ window.pickText = function(opts) { mgResult.textContent = pickText({ standard: msgStd, easy: msgEasy }); mgResult.hidden = false; mgApplyResult({ quality, factor, minigameTimeSec: t, success: true }); + setTimeout(() => { if (typeof mgCloseOverlay === 'function') mgCloseOverlay(); }, 3000); } - /** Game Over: Crash oder Zeitablauf → Maximale Ladezeit (Kollege übernimmt). */ + /** Game Over: nur noch bei Zeitablauf (Kollisionen: siehe mgTouch). */ function mgGameOver(reason) { const s = mgState; if (!s || s.docked) return; s.docked = true; - // Audio-Effekte: Crash-Sound bei Kollisionen (nicht bei Timeout) - if (reason === 'crash_vehicle' || reason === 'crash_bay') { - mgAudioCrash(); - } - // Engine stumm if (mgEngineGain && mgAudioCtx) { mgEngineGain.gain.setTargetAtTime(0, mgAudioCtx.currentTime, 0.1); } const factor = 1.5; - let msgStd, msgEasy; - if (reason === 'timeout') { - msgStd = '⏰ Zeit abgelaufen! Ein Kollege übernimmt — maximale Ladezeit (150 %).'; - msgEasy = 'Zeit vorbei. Jemand anderes macht das. Laden dauert lange.'; - } else if (reason === 'crash_bay') { - msgStd = '💥 Gegen die Rampen-Wand! Ein Kollege übernimmt — maximale Ladezeit (150 %).'; - msgEasy = 'Au! Gegen die Wand gefahren. Laden dauert lange.'; - } else { - msgStd = '💥 Anderes Fahrzeug gerammt! Ein Kollege übernimmt — maximale Ladezeit (150 %).'; - msgEasy = 'Au! Anderen LKW getroffen. Laden dauert lange.'; - } + const msgStd = '⏰ Zeit abgelaufen! Ein Kollege übernimmt — maximale Ladezeit (150 %).'; + const msgEasy = 'Zeit vorbei. Jemand anderes macht das. Laden dauert lange.'; mgResult.className = 'lg-minigame-result fail'; mgResult.textContent = pickText({ standard: msgStd, easy: msgEasy }); mgResult.hidden = false; mgApplyResult({ quality: 'miss', factor, reason, success: false }); + setTimeout(() => { if (typeof mgCloseOverlay === 'function') mgCloseOverlay(); }, 3000); + } + + /** Touchiert: Kollision mit geparktem LKW, Active-Mover oder Bucht-Wand. + * → Einmal pro Minispiel-Sitzung 1000 € Reparatur. Weitere Touches + * sind nur Rebound + Sound. Wer touchiert hat, bekommt keinen Park-Bonus + * (siehe mgApplyResult-Check auf s._touched). */ + function mgTouch(kind, s, obstacle) { + if (!s || s.docked) return; + const alreadyTouched = !!s._touched; + // Truck abstoßen, damit er nicht im Obstacle stecken bleibt. + if (obstacle) mgReboundFrom(s, obstacle); + s.v *= -0.4; + mgAudioCrash(); + if (alreadyTouched) return; // Cap: max 1× pro Einparken + s._touched = true; + + // Strafe einmalig abbuchen + Notification ueber Engine + game.balance = (game.balance || 0) - 1000; + if (!game.pendingNotifications) game.pendingNotifications = []; + const label = kind === 'bay' ? 'Rampen-Wand' + : kind === 'mover' ? 'fahrendem Fahrzeug' + : 'geparktem LKW'; + game.pendingNotifications.push({ + id: 'touch_' + performance.now(), + type: 'REPAIR_COST', + messageShort: '💥 Touchiert (' + label + ') — 1000 € Reparatur, kein Park-Bonus', + simTime: game.simulationTime, + }); + mgResult.className = 'lg-minigame-result fail'; + mgResult.textContent = pickText({ + standard: '💥 Touchiert — 1000 € Reparatur. Kein Park-Bonus. Weiter einparken!', + easy: 'Au! Berührt. Kostet 1000 Euro. Kein Bonus. Weiter.', + }); + mgResult.hidden = false; + setTimeout(() => { if (mgState && !mgState.docked) mgResult.hidden = true; }, 2500); + if (typeof updateStatusUI === 'function') updateStatusUI(); + } + + /** Stoesst den Truck von einem Obstacle-AABB weg (kleinster Achsen-Overlap). */ + function mgReboundFrom(s, o) { + const hx = MG.truckW / 2, hy = MG.truckH / 2; + const oCx = o.x + o.w / 2, oCy = o.y + o.h / 2; + const dx = s.x - oCx, dy = s.y - oCy; + const overlapX = (hx + o.w / 2) - Math.abs(dx); + const overlapY = (hy + o.h / 2) - Math.abs(dy); + if (overlapX < 0 || overlapY < 0) return; + if (overlapX < overlapY) s.x += Math.sign(dx || 1) * (overlapX + 1); + else s.y += Math.sign(dy || 1) * (overlapY + 1); } function mgApplyResult(info) { @@ -2179,6 +4012,57 @@ window.pickText = function(opts) { if (mgState) mgState.result = result; try { LogistikEngine.applyMinigameResult(game, result); } catch (e) { console.warn('[Logistik] applyMinigameResult:', e.message); } + + // Phase 8e++: Ladebonus-Pill als Stapel-Item. Zeit-basiert, je kuerzer desto mehr Bonus. + if (info.success) { + const t = info.minigameTimeSec || 999; + // Park-Bonus entfaellt, wenn die Fahrt jemanden touchiert hat + // (Thomas-Regel: Reparatur ist gezahlt, aber kein extra Bonus). + const touched = !!(mgState && mgState._touched); + let bonusMin = 0; + if (!touched && t < 15) bonusMin = 45; + else if (!touched && t < 30) bonusMin = 20; + else if (!touched && t < 60) bonusMin = 5; + if (bonusMin > 0) { + try { + LogistikEngine.pushLoadBonus(game, bonusMin, 'Einparken ' + t.toFixed(1) + 's'); + renderBonusPills(); + celebrateParkingSuccess(bonusMin, t); + speak('minigame_perfect'); + } catch (e) { /* ignore */ } + } + } else if (info.reason === 'timeout') { + speak('minigame_timeout'); + } + } + + /** Park-Celebration (Phase 8f++): grosse Info-Karte zeigt Bonus, haelt + * kurz, fliegt dann nach rechts oben zur Bonus-Pill und schrumpft dort. + * Reines UI-Feedback — keine State-Aenderung. */ + function celebrateParkingSuccess(bonusMin, timeSec) { + const pillContainer = document.getElementById('bonusPills'); + if (!pillContainer) return; + const wrap = document.createElement('div'); + wrap.className = 'lg-park-celebrate-wrap'; + const card = document.createElement('div'); + card.className = 'lg-park-celebrate'; + card.innerHTML = + '
🏆
' + + '
Perfekt eingeparkt!
' + + '
+' + bonusMin + ' min Ladezeit-Bonus · ' + timeSec.toFixed(1) + ' s
'; + wrap.appendChild(card); + document.body.appendChild(wrap); + // Fluglogik: nach 1200 ms Richtung Pill-Container translatieren + schrumpfen + setTimeout(() => { + const pillRect = pillContainer.getBoundingClientRect(); + const cardRect = card.getBoundingClientRect(); + const dx = (pillRect.left + 30) - (cardRect.left + cardRect.width / 2); + const dy = (pillRect.top + 14) - (cardRect.top + cardRect.height / 2); + card.style.transition = 'transform .55s cubic-bezier(.4,0,.2,1), opacity .55s'; + card.style.transform = 'translate(' + dx + 'px,' + dy + 'px) scale(0.18)'; + card.style.opacity = '0'; + }, 1200); + setTimeout(() => { if (wrap.parentNode) wrap.remove(); }, 1850); } /** (Legacy-Funktion — wird nicht mehr vom UI aufgerufen, aber bleibt @@ -2313,36 +4197,37 @@ window.pickText = function(opts) { mgAudioResume(); // Trucker-Musik starten (falls Hauptmusik noch nicht läuft) mgMusicEnsurePlaying(); - // Ab L2: ein fahrendes Fahrzeug mit Lenklogik, sichtbar im Spielfeld + // Aktiver Mover: immer aktiv (in allen Levels — gehört didaktisch zum + // Rangieren auf einem echten Hof). Geschwindigkeit wächst mit Level. const cfg = game._config || {}; const lvl = (cfg.maxActiveContracts || 0) >= 6 ? 3 : (cfg.maxActiveContracts || 0) >= 3 ? 2 : 1; - MG.activeMover = null; - if (lvl >= 2) { - MG.activeMover = { - x: 180, y: 85, angle: 0, v: lvl >= 3 ? 38 : 28, // L3 etwas schneller - w: 58, h: 22, color: '#9a3a3a', - path: [ - { x: 330, y: 85 }, - { x: 330, y: 215 }, - { x: 180, y: 215 }, - { x: 180, y: 85 }, - ], - pathIdx: 0, - }; - } + MG.activeMover = { + x: 180, y: 85, angle: 0, + v: lvl >= 3 ? 38 : lvl >= 2 ? 28 : 20, // L1: langsam, L2: mittel, L3: schnell + w: 58, h: 22, color: '#9a3a3a', + path: [ + { x: 330, y: 85 }, + { x: 330, y: 215 }, + { x: 180, y: 215 }, + { x: 180, y: 85 }, + ], + pathIdx: 0, + }; mgInit(); mgDrawReferenceMiniatures(); cancelAnimationFrame(mgRaf); mgRaf = requestAnimationFrame(mgLoop); }); - document.getElementById('mgClose').addEventListener('click', () => { + function mgCloseOverlay() { + if (!overlay.classList.contains('open')) return; overlay.classList.remove('open'); cancelAnimationFrame(mgRaf); mgRaf = null; mgResetKeys(); mgAudioStop(); mgState = null; - }); + } + document.getElementById('mgClose').addEventListener('click', mgCloseOverlay); document.getElementById('mgReset').addEventListener('click', () => { mgResetKeys(); mgReset(); @@ -2356,7 +4241,8 @@ window.pickText = function(opts) { kommen erst drauf, wenn die Fahrgrundlage sich gut anfuehlt. ================================================================= */ const btnShunting = document.getElementById('btnShunting'); - btnShunting.hidden = false; + // Rangier-Minigame nur wenn TRAIN im Fuhrpark (auf L1 kein Zug → Button bleibt hidden) + btnShunting.hidden = !(game.vehicles || []).some(v => v.mode === 'TRAIN'); /* === Gleisnetz Phase 4: Hauptgleis oben (4 Teile, W1-W3) + 3 Sidings (gleis_1/2/3) + Sammelgleis unten (4 Teile, W4-W6). @@ -2390,12 +4276,12 @@ window.pickText = function(opts) { weichenPos: { W1: [280, 80], W2: [520, 80], W3: [760, 80], W4: [600, 320], W5: [840, 320], W6: [1080, 320] }, WAGONS: [ - { id: 'W', color: '#9a3d4d', emoji: '🎡', label: 'Wien' }, - { id: 'M', color: '#b88a45', emoji: '🥨', label: 'München' }, - { id: 'H', color: '#4f7a9a', emoji: '⚓', label: 'Hamburg' }, - { id: 'P', color: '#6a7a3d', emoji: '🗼', label: 'Paris' }, + { id: '1', color: '#9a3d4d', label: 'Wagen 1' }, + { id: '2', color: '#b88a45', label: 'Wagen 2' }, + { id: '3', color: '#4f7a9a', label: 'Wagen 3' }, + { id: '4', color: '#6a7a3d', label: 'Wagen 4' }, ], - TARGET: ['W', 'M', 'H', 'P'], + TARGET: ['1', '2', '3', '4'], WAGON_BY_ID: {}, consists: [], // [{ id, items, head:{segId,s}, isActive }] @@ -2411,6 +4297,10 @@ window.pickText = function(opts) { startTime: 0, solved: false, + warnSwitch: null, // Name der Weiche, die gerade rot pulsiert (oder null) + warnTime: 0, // performance.now() des letzten Warn-Triggers + _audioCtx: null, // Web-Audio-Kontext, lazy + lastT: 0, raf: null, }; SG.WAGONS.forEach(w => SG.WAGON_BY_ID[w.id] = w); @@ -2479,41 +4369,75 @@ window.pickText = function(opts) { /** Bewege Zustand {segId, s, dir} um signedDs Pixel. * signedDs > 0: vorwaerts (in Heading) * signedDs < 0: rueckwaerts - * Beruecksichtigt Segmentwechsel an Endpunkten. */ + * An Segmentgrenzen wird die neue Orientierung per Dot-Produkt berechnet: + * Lok-Heading + Bewegungsrichtung werden geometrisch auf das neue + * Segment projiziert, sodass bei gerade Uebergaengen dir erhalten bleibt + * (kein Spiegeln, kein Zusammenfalten des Zugs). */ function sgTraverse(state, signedDs) { let { segId, s, dir } = state; - let remaining = signedDs; + let rSigned = signedDs; + let blockedAt = null; // {segId, endpoint} falls an einer Sackgasse/gesperrten Weiche gestoppt for (let g = 0; g < 16; g++) { const seg = SG.seg[segId]; - const ds = dir * remaining; // Aenderung des s-Wertes - let newS = s + ds; + const sCh = dir * rSigned; + const newS = s + sCh; if (newS > seg.len + 1e-9) { - // Endpunkt e1 ueberschritten - const overshoot = newS - seg.len; - const next = sgConnected(segId, 'e1'); - if (!next) { s = seg.len; break; } - segId = next.seg; - const nseg = SG.seg[segId]; - if (next.endpoint === 'e0') { s = 0; dir = +1; } - else { s = nseg.len; dir = -1; } - // Verbleibende Strecke in der neuen Heading-Richtung weiterfahren - remaining = (signedDs >= 0 ? 1 : -1) * Math.abs(overshoot); + const over = newS - seg.len; + const nc = sgConnected(segId, 'e1'); + if (!nc) { s = seg.len; blockedAt = { segId, endpoint: 'e1' }; break; } + const t = sgTransitionOut(seg, SG.seg[nc.seg], nc.endpoint, dir, sCh); + segId = nc.seg; s = t.s; dir = t.dir; + rSigned = t.sChSign * dir * Math.abs(over); } else if (newS < -1e-9) { - // Endpunkt e0 unterschritten - const undershoot = -newS; - const prev = sgConnected(segId, 'e0'); - if (!prev) { s = 0; break; } - segId = prev.seg; - const nseg = SG.seg[segId]; - if (prev.endpoint === 'e1') { s = nseg.len; dir = -1; } - else { s = 0; dir = +1; } - remaining = (signedDs >= 0 ? 1 : -1) * Math.abs(undershoot); + const under = -newS; + const pc = sgConnected(segId, 'e0'); + if (!pc) { s = 0; blockedAt = { segId, endpoint: 'e0' }; break; } + const t = sgTransitionOut(seg, SG.seg[pc.seg], pc.endpoint, dir, sCh); + segId = pc.seg; s = t.s; dir = t.dir; + rSigned = t.sChSign * dir * Math.abs(under); } else { s = newS; break; } } - return { segId, s, dir }; + return { segId, s, dir, blockedAt }; + } + + /** Mapping Endpunkt → Weichen-Name (oder null). */ + const SG_ENDPOINT_SWITCH = {}; + Object.keys(SG_SWITCHES).forEach(name => { + const sw = SG_SWITCHES[name]; + [].concat(sw.gerade, sw.abbiegen).forEach(k => { SG_ENDPOINT_SWITCH[k] = name; }); + }); + function sgEndpointSwitch(segId, endpoint) { + return SG_ENDPOINT_SWITCH[segId + ':' + endpoint] || null; + } + + /** Berechnet neuen (s, dir, sChSign) beim Segmentwechsel. + * oldSeg, newSeg: Segment-Objekte + * newEntry: 'e0' | 'e1' (an welcher Seite tritt Zug ein) + * oldDir, oldSCh: Lok-Heading und s-Aenderung vor dem Uebergang + * Rueckgabe: + * s = 0 bzw. newSeg.len (Eintrittspunkt) + * dir = Sign(Dot(alte Lok-Heading, neue e0->e1-Richtung)) + * sChSign = Sign(Dot(phys. Motion-Richtung, neue e0->e1)) + * → gibt das Vorzeichen von sChange auf dem neuen Segment */ + function sgTransitionOut(oldSeg, newSeg, newEntry, oldDir, oldSCh) { + const odx = oldSeg.e1[0] - oldSeg.e0[0], ody = oldSeg.e1[1] - oldSeg.e0[1]; + const oln = Math.hypot(odx, ody); + const ofx = oldDir * odx / oln, ofy = oldDir * ody / oln; // alte Lok-Heading (world) + const pmSign = oldSCh >= 0 ? +1 : -1; + const pmx = pmSign * odx / oln, pmy = pmSign * ody / oln; // phys. Motion (world) + const ndx = newSeg.e1[0] - newSeg.e0[0], ndy = newSeg.e1[1] - newSeg.e0[1]; + const nln = Math.hypot(ndx, ndy); + const nfx = ndx / nln, nfy = ndy / nln; // neue e0->e1-Richtung + const fd = ofx * nfx + ofy * nfy; + const pd = pmx * nfx + pmy * nfy; + return { + s: newEntry === 'e0' ? 0 : nln, + dir: fd >= 0 ? +1 : -1, + sChSign: pd >= 0 ? +1 : -1, + }; } /** Zustand `distance` Pixel hinter Lok (gegen Heading-Richtung). */ function sgWalkBack(state, distance) { return sgTraverse(state, -distance); } @@ -2623,12 +4547,21 @@ window.pickText = function(opts) { if (Math.min(dF, dB) < threshold + Math.abs(ds)) { ds = 0; flashStatus('🚫 Lok-Front blockiert — Waggons koppeln nur am hinteren Ende.'); + sgPlayWarnTone(); break; } } } - if (ds !== 0) active.head = sgTraverse(active.head, ds); + if (ds !== 0) { + const r = sgTraverse(active.head, ds); + active.head = { segId: r.segId, s: r.s, dir: r.dir }; + // Wenn wir an einer Sackgasse/gesperrten Weiche gestoppt sind: Warnung + if (r.blockedAt) { + const sw = sgEndpointSwitch(r.blockedAt.segId, r.blockedAt.endpoint); + if (sw) sgTriggerSwitchWarn(sw); + } + } // Rueckwaerts: Tail-Kupplung pruefen if (v < 0) { @@ -2671,17 +4604,25 @@ window.pickText = function(opts) { function sgMarkSolved() { SG.solved = true; - const elapsed = ((performance.now() - SG.startTime) / 1000).toFixed(1); + const elapsedMs = performance.now() - SG.startTime; + const elapsed = (elapsedMs / 1000).toFixed(1); const stars = (SG.rangierZuege <= 8) ? 3 : (SG.rangierZuege <= 14) ? 2 : 1; + const timeBonus = elapsedMs <= 4 * 60 * 1000; // Unter 4 Minuten -> Bonus aufs naechste Be-/Entladen const resEl = document.getElementById('shuntingResult'); resEl.hidden = false; resEl.className = 'lg-minigame-result success'; resEl.innerHTML = '🎉 Geschafft! ' + SG.rangierZuege + ' Rangiervorgänge · ' + SG.weichenstellungen + ' Weichenstellungen · ' + elapsed + ' s — ' - + '★'.repeat(stars) + '☆'.repeat(3 - stars); + + '★'.repeat(stars) + '☆'.repeat(3 - stars) + + (timeBonus ? '
⚡ Zeit-Bonus aktiv — nächste Lade-/Entlade-Zeit verkürzt.' : ''); try { LogistikEngine.applyMinigameResult && LogistikEngine.applyMinigameResult(game, { - type: 'shunting', stars, rangierZuege: SG.rangierZuege, weichenstellungen: SG.weichenstellungen, sec: parseFloat(elapsed), + type: 'shunting', + stars, + rangierZuege: SG.rangierZuege, + weichenstellungen: SG.weichenstellungen, + sec: parseFloat(elapsed), + timeBonus, // Engine kann das fuer Lade-/Entlade-Speedup nutzen }); } catch (e) { /* ignoriert wenn engine den Typ noch nicht kennt */ } } @@ -2755,6 +4696,7 @@ window.pickText = function(opts) { function sgToggleWeiche(name) { if (sgWeicheBlockiert(name)) { flashStatus('⚠ Weiche ' + name + ' blockiert (Zug steht drauf).'); + sgTriggerSwitchWarn(name); return; } SG.weichen[name] = SG.weichen[name] === 'gerade' ? 'abbiegen' : 'gerade'; @@ -2763,6 +4705,33 @@ window.pickText = function(opts) { flashStatus('Weiche ' + name + ': ' + SG.weichen[name]); } + /* ---- Warnung (Switch rot pulsiert + Web-Audio Ton) ---- */ + function sgTriggerSwitchWarn(name) { + SG.warnSwitch = name; + SG.warnTime = performance.now(); + sgPlayWarnTone(); + } + function sgPlayWarnTone() { + try { + if (!SG._audioCtx) SG._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + const ctx = SG._audioCtx; + const now = ctx.currentTime; + // Zwei kurze Pieptöne (wie Ampelwarnung), 880 Hz, square + for (let i = 0; i < 2; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'square'; + osc.frequency.setValueAtTime(880, now + i * 0.14); + gain.gain.setValueAtTime(0, now + i * 0.14); + gain.gain.linearRampToValueAtTime(0.12, now + i * 0.14 + 0.015); + gain.gain.linearRampToValueAtTime(0, now + i * 0.14 + 0.09); + osc.connect(gain).connect(ctx.destination); + osc.start(now + i * 0.14); + osc.stop(now + i * 0.14 + 0.10); + } + } catch (e) { /* Audio optional */ } + } + function flashStatus(msg) { const el = document.getElementById('sgStatus'); el.textContent = msg; @@ -2793,7 +4762,7 @@ window.pickText = function(opts) { ctx.fillStyle = '#1f6b3a'; ctx.font = 'bold 11px system-ui, sans-serif'; ctx.textAlign = 'right'; - ctx.fillText('Ziel: Lok mit W·M·H·P sortiert', goal.e1[0] - 4, goal.e1[1] + 38); + ctx.fillText('Ziel: Lok + #1·#2·#3·#4 sortiert', goal.e1[0] - 4, goal.e1[1] + 38); // Gleisschilder ctx.fillStyle = '#666'; @@ -2806,11 +4775,28 @@ window.pickText = function(opts) { ctx.fillText(name, s.e0[0] + 4, 222); }); + // Weichen-Warn-Ring (pulsierend rot, 1500ms nach Trigger) + const warnElapsed = SG.warnSwitch ? (performance.now() - SG.warnTime) : 9999; + if (SG.warnSwitch && warnElapsed < 1500) { + const pos = SG.weichenPos[SG.warnSwitch]; + const phase = (warnElapsed % 500) / 500; // Pulszyklus 500ms + const radius = 16 + 8 * Math.sin(phase * Math.PI * 2); + const alpha = 1 - warnElapsed / 1500; + ctx.strokeStyle = 'rgba(220, 50, 50, ' + (alpha * 0.85).toFixed(3) + ')'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2); + ctx.stroke(); + } else if (SG.warnSwitch) { + SG.warnSwitch = null; + } + // Weichen-Marker ['W1','W2','W3','W4','W5','W6'].forEach(name => { const pos = SG.weichenPos[name]; const sw = SG.weichen[name]; - ctx.fillStyle = sw === 'gerade' ? '#1f6b3a' : '#b88419'; + const isWarn = (SG.warnSwitch === name) && warnElapsed < 1500; + ctx.fillStyle = isWarn ? '#d23232' : (sw === 'gerade' ? '#1f6b3a' : '#b88419'); ctx.strokeStyle = '#111'; ctx.lineWidth = 2; ctx.beginPath(); @@ -2931,7 +4917,8 @@ window.pickText = function(opts) { ctx.fillStyle = '#fff'; ctx.font = 'bold 11px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText(wagon.emoji + ' ' + wagon.id, 0, 0); + ctx.font = 'bold 16px system-ui, sans-serif'; + ctx.fillText('#' + wagon.id, 0, 0); ctx.fillStyle = '#111'; ctx.fillRect(w/2, -2, 3, 4); ctx.fillRect(-w/2 - 3, -2, 3, 4); @@ -3053,7 +5040,10 @@ window.pickText = function(opts) { totalRewards: Math.round(a.totalRewardEarned || 0), totalPenalty: Math.round(a.totalPenaltyPaid || 0), totalFares: Math.round(a.totalFareCostPaid || 0), - runningCosts: Math.round(a.runningCostsTotal || 0), + runningCosts: Math.round(a.runningCostsTotal || 0), + rentalCosts: Math.round(a.runningCostsRental || 0), + idleCosts: Math.round(a.runningCostsIdle || 0), + portCosts: Math.round(a.runningCostsPort || 0), profit: game.balance - startBudget, minigameRate: a.minigamesPlayed ? Math.round(((a.minigamesSuccessful || 0) / a.minigamesPlayed) * 100) + ' %' @@ -3090,30 +5080,109 @@ window.pickText = function(opts) { /* End-Screen bei LEVEL_SUCCESS / LEVEL_FAILED */ const esOverlay = document.getElementById('endscreenOverlay'); const esModal = document.getElementById('endscreenModal'); + /** Sterne-Bewertung aus Endbilanz (Thomas 2026-04-24). + * Referenzwerte aus Balance-Kalkulation L1 (4 Tage, 1-3 LKW via Auto-Rent): + * Pessimist ~8.700 EUR, Median ~11.600 EUR, Optimist ~15.700 EUR, + * Optimum ~20.300 EUR. + * Bankrott (balance <= 0) = 0 Sterne, Level verloren. */ + function computeStars(balance) { + if (balance <= 0) return 0; + if (balance >= 16000) return 5; + if (balance >= 13000) return 4; + if (balance >= 10500) return 3; + if (balance >= 8500) return 2; + return 1; + } + const STAR_LABELS = { + 5: 'Spitze! Optimale Ausnutzung.', + 4: 'Sehr gut — so spielen Profis.', + 3: 'Solide — Median-Bereich.', + 2: 'Knapp, aber solvent.', + 1: 'Gerade noch überlebt.', + 0: 'Bankrott — Level verloren.', + }; + function showEndScreen(finalState) { const success = finalState === LogistikEngine.STATE.LEVEL_SUCCESS; esModal.className = 'lg-endscreen-modal ' + (success ? 'success' : 'fail'); - document.getElementById('endscreenTitle').textContent = success - ? pickText({ standard: '✅ Durchgang erfolgreich', easy: 'Geschafft!' }) - : pickText({ standard: '⚠ Durchgang beendet — Ziel verfehlt', easy: 'Leider nicht geschafft.' }); const cfg = game._config || {}; const a = computeAnalytics(); - const target = Math.round(cfg.minTargetEarnings || 0).toLocaleString('de-DE'); + const finalBal = game.balance; + const stars = computeStars(finalBal); + const isBankrupt = finalBal <= 0; + + document.getElementById('endscreenTitle').textContent = success + ? pickText({ standard: '✅ 4 Tage durchgestanden', easy: 'Geschafft!' }) + : pickText({ + standard: isBankrupt ? '💸 Pleite — Durchgang verloren' : '⚠ Durchgang beendet', + easy: 'Leider nicht geschafft.' + }); document.getElementById('endscreenText').textContent = pickText({ standard: success - ? 'Du hast das Min-Ziel von ' + target + ' € überschritten und den Durchgang positiv abgeschlossen.' - : 'Das Min-Ziel von ' + target + ' € wurde nicht erreicht. Versuche es nochmal!', + ? 'Endbilanz ' + fmtEuro(finalBal) + '. ' + STAR_LABELS[stars] + : (isBankrupt + ? 'Das Konto ist auf 0 € oder darunter gefallen — Spiel vorbei.' + : 'Der Durchgang ist beendet.'), easy: success - ? 'Super! Du hast das Ziel von ' + target + ' Euro erreicht.' - : 'Das Ziel hast du noch nicht erreicht. Probier es nochmal!', + ? 'Fertig! Endstand: ' + fmtEuro(finalBal) + '.' + : 'Kein Geld mehr übrig. Noch mal probieren!', }); + + // Sterne-Box + const starsBox = document.getElementById('endscreenStars'); + if (starsBox) { + starsBox.hidden = false; + const starsEl = document.getElementById('lgStars'); + starsEl.innerHTML = ''; + for (let i = 1; i <= 5; i++) { + const s = document.createElement('span'); + s.className = i <= stars ? 'lg-star-on' : 'lg-star-off'; + s.textContent = '★'; + starsEl.appendChild(s); + } + document.getElementById('lgStarsLabel').textContent = STAR_LABELS[stars] || ''; + } + // Voice: Sterne oder Bankrott-Line, eine einzige Ausgabe mit leichtem Delay + if (isBankrupt) setTimeout(() => speak('bankrupt'), 400); + else setTimeout(() => speak('stars_' + stars), 400); + + // Leerfahrten-Box + const emptyBox = document.getElementById('endscreenEmpty'); + if (emptyBox && a.totalKm > 0) { + emptyBox.hidden = false; + const pct = a.totalKm > 0 ? (a.emptyKm / a.totalKm * 100) : 0; + document.getElementById('lgEmptyPct').textContent = pct.toFixed(1) + ' %'; + document.getElementById('lgEmptyFill').style.width = Math.min(100, pct).toFixed(1) + '%'; + let sub = ''; + if (pct < 10) sub = 'Super — fast alle Kilometer mit Ladung.'; + else if (pct < 20) sub = 'Gut — wenig Leerzeit.'; + else if (pct < 35) sub = 'Ausbaufähig — versuch Rückfahrten mit Fracht zu füllen.'; + else sub = 'Viele Kilometer ohne Ladung — hier liegt viel Potenzial.'; + document.getElementById('lgEmptySub').textContent = sub + + ' (' + Math.round(a.emptyKm).toLocaleString('de-DE') + ' km leer / ' + + Math.round(a.totalKm).toLocaleString('de-DE') + ' km gesamt)'; + } else if (emptyBox) { + emptyBox.hidden = true; + } + + // Transparente Konto-Rechnung + const startBud = cfg.startBudget || 0; const stats = [ - ['Aufträge erledigt', a.completed], - ['Verspätungsquote', a.lateRatio], - ['Leerfahrten-Anteil', a.emptyRatio], - ['Ø Netto/Auftrag', fmtEuro(a.avgNet)], - ['Endbilanz', fmtEuroSigned(a.profit)], + ['Aufträge erledigt', a.completed], + ['Verspätungsquote', a.lateRatio], + ['Startbudget', fmtEuro(startBud)], + ['Erlöse (Basis + Bonus)','+' + fmtEuro(a.totalRewards || 0)], + ['Strafen', '−' + fmtEuro(a.totalPenalty || 0)], + ['Fahrkosten', '−' + fmtEuro(a.totalFares || 0)], ]; + // Laufende Kosten didaktisch aufgesplittet (nur zeigen was > 0 ist) + if (a.rentalCosts > 0) stats.push(['Fahrzeugmiete', '−' + fmtEuro(a.rentalCosts)]); + if (a.idleCosts > 0) stats.push(['Standkosten', '−' + fmtEuro(a.idleCosts)]); + if (a.portCosts > 0) stats.push(['Hafen-Standgeld', '−' + fmtEuro(a.portCosts)]); + stats.push( + ['Endbilanz', fmtEuro(finalBal)], + ['Gewinn/Verlust', fmtEuroSigned(a.profit)], + ); document.getElementById('endscreenStats').innerHTML = stats.map(([k, v]) => '
' + escapeHtml(k) + '
' + escapeHtml(String(v)) + '
').join(''); esOverlay.classList.add('open'); @@ -3129,7 +5198,8 @@ window.pickText = function(opts) { showEndScreen(evt.to); // Server-Durchgang abschliessen (best-effort) + letzte Analytics-Eintraege leeren const a = computeAnalytics(); - LG_API.endAttempt(evt.to === LogistikEngine.STATE.LEVEL_SUCCESS, { + const success = evt.to === LogistikEngine.STATE.LEVEL_SUCCESS; + LG_API.endAttempt(success, { completed: a.completed, lateRatio: a.lateRatio, emptyRatio: a.emptyRatio, @@ -3137,6 +5207,23 @@ window.pickText = function(opts) { profit: a.profit, levelNum: levelNum, }); + // Assessment: Level-Abschluss (Integrations-Vertrag §6.4) + LG_API.postAssessment('completed', { + durationMs: Math.round(performance.now() - sessionStartMs), + completedPhases: ['started', 'running', 'completed'], + results: { + success: success, + finalBalance: Math.round(game.balance || 0), + minTarget: (game._config && game._config.minTargetEarnings) || 0, + completed: a.completed, + lateRatio: a.lateRatio, + emptyRatio: a.emptyRatio, + avgNet: a.avgNet, + profit: a.profit, + levelNum: levelNum, + round: (game.progression && game.progression.round) || 1, + }, + }); drainAnalyticsQueue(); } }); @@ -3146,9 +5233,12 @@ window.pickText = function(opts) { })(); /* ================================================================= - Music-Player (Standard aus Design-System) — Pattern wie Klima/Fluss - Modul ruft einmalig ggsMusicSetup([{key, label, file}, ...]) auf. - Atlas liefert die finalen MP3-Dateien; bis dahin Platzhalter-Pfade. + Music-Player mit Playlist-Support + ggsMusicSetup([{ key, label, files: [...] }, ...]) — Playlist-Modus + ggsMusicSetup([{ key, label, file: '...' }, ...]) — Single-Track-Loop (rückwärtskompatibel) + + Playlist: Queue wird pro Auswahl geshuffled, bei track-Ende spielt der + nächste automatisch; am Ende der Queue wird neu geshuffled. ================================================================= */ window.ggsMusicSetup = function(tracks) { var el = document.getElementById('ggs-music'); @@ -3157,31 +5247,105 @@ window.ggsMusicSetup = function(tracks) { var vol = document.getElementById('ggs-music-vol'); if (!el || !tracks || !tracks.length) return; el.hidden = false; + + tracks.forEach(function(t) { if (!t.files && t.file) t.files = [t.file]; }); + var audio = new Audio(); - audio.loop = true; var lsKey = 'ggs-music-logistik'; var state = {}; try { state = JSON.parse(localStorage.getItem(lsKey) || '{}'); } catch (e) {} + var plQueue = []; + var plIndex = 0; + + function shuffle(arr) { + var a = arr.slice(); + for (var i = a.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = a[i]; a[i] = a[j]; a[j] = tmp; + } + return a; + } + function getTrack(key) { + for (var i = 0; i < tracks.length; i++) if (tracks[i].key === key) return tracks[i]; + return tracks[0]; + } + function loadPlaylist(key) { + var t = getTrack(key); + // Reihenfolge: wie in der Definition (Thomas 2026-04-25 — kein Shuffle). + plQueue = t.files.slice(); + plIndex = 0; + audio.src = plQueue[0]; + audio.loop = plQueue.length === 1; + } + function advance() { + plIndex++; + if (plIndex >= plQueue.length) plIndex = 0; // Loop von vorne + audio.src = plQueue[plIndex]; + audio.play().catch(function(){}); + } + audio.addEventListener('ended', function() { if (!audio.loop) advance(); }); + sel.innerHTML = tracks.map(function(t) { - return ''; + return ''; }).join(''); - if (state.track) sel.value = state.track; - audio.src = sel.options[sel.selectedIndex].dataset.file; + if (state.track && tracks.some(function(t){ return t.key === state.track; })) sel.value = state.track; + loadPlaylist(sel.value); + var defaultVol = 22; - audio.volume = (state.volume != null ? state.volume : defaultVol) / 100; + var userVol = (state.volume != null ? state.volume : defaultVol) / 100; + audio.volume = userVol; vol.value = state.volume != null ? state.volume : defaultVol; + // Ducking-API (Phase 8f++): waehrend Sprachausgabe Lautstaerke auf 25% + var duckActive = false; + var fadeRaf = null; + var DUCK_FACTOR = 0.25; + function fadeTo(targetVol, ms) { + if (fadeRaf) cancelAnimationFrame(fadeRaf); + var startVol = audio.volume; + var startT = performance.now(); + function step(now) { + var k = Math.min(1, (now - startT) / ms); + audio.volume = startVol + (targetVol - startVol) * k; + if (k < 1) fadeRaf = requestAnimationFrame(step); + else fadeRaf = null; + } + fadeRaf = requestAnimationFrame(step); + } + window.lgAmbientDuck = function() { + if (duckActive) return; + duckActive = true; + fadeTo(userVol * DUCK_FACTOR, 250); + }; + window.lgAmbientUnduck = function() { + if (!duckActive) return; + duckActive = false; + fadeTo(userVol, 350); + }; + window.lgAmbientStart = function(key) { + if (key) { + var t = getTrack(key); + if (t) { + sel.value = key; + loadPlaylist(key); + } + } + audio.play().then(function(){ + btn.textContent = '⏸'; btn.classList.add('playing'); save(); + }).catch(function(){}); + }; + function save() { try { localStorage.setItem(lsKey, JSON.stringify({ - track: sel.value, volume: +vol.value, playing: !audio.paused, + track: sel.value, volume: Math.round(userVol * 100), playing: !audio.paused, })); } catch (e) {} } sel.addEventListener('change', function() { var wasPlaying = !audio.paused; - audio.src = sel.options[sel.selectedIndex].dataset.file; + loadPlaylist(sel.value); if (wasPlaying) audio.play().catch(function(){}); save(); }); @@ -3194,20 +5358,42 @@ window.ggsMusicSetup = function(tracks) { } }); vol.addEventListener('input', function() { - audio.volume = vol.value / 100; + userVol = vol.value / 100; + audio.volume = duckActive ? userVol * DUCK_FACTOR : userVol; save(); }); }; (function () { - // Music-Tracks — von Thomas via .humanInput bereitgestellt - // (Country-Instrumentals, didaktisch passend trotz Genre-Tag) var BASE = window.LOGISTIK_BASE || ''; + var M = BASE + 'sounds/music/'; ggsMusicSetup([ - { key: 'metronome', label: '⚙ Metronom', file: BASE + 'sounds/music/saddlewood-metronome.mp3' }, - { key: 'gears', label: '🔩 Gears', file: BASE + 'sounds/music/gears-and-marbles.mp3' }, - { key: 'lullaby', label: '🌙 Lullaby', file: BASE + 'sounds/music/barbed-lullaby.mp3' }, - { key: 'instrumental', label: '🎼 Instrumental', file: BASE + 'sounds/music/untitled-instrumental.mp3' } + { key: 'alpine', label: '🏔 Alpine', files: [ + M+'alpine-freightlight-v1.mp3', M+'alpine-freightlight-v2.mp3', + M+'alpine-freight-light-v3.mp3', M+'alpine-freight-light-v4.mp3', + ]}, + { key: 'country', label: '🎸 Country', files: [ + M+'busted-wagon-wheel.mp3', + M+'dustlight-freight-v1.mp3', M+'dustlight-freight-v2.mp3', + M+'saddlewood-metronome.mp3', M+'saddlewood-metronome-v2.mp3', + M+'borderline-kilometers-v1.mp3', M+'borderline-kilometers-v2.mp3', + M+'diesel-and-dreams-v1.mp3', M+'diesel-and-dreams-v2.mp3', + M+'grenzen-auf-asphalt-v1.mp3', M+'grenzen-auf-asphalt-v2.mp3', + M+'grenzen-im-scheinwerfer-v1.mp3', M+'grenzen-im-scheinwerfer-v2.mp3', + M+'still-trucking-morgen-v1.mp3', M+'still-trucking-morgen-v2.mp3', + ]}, + { key: 'mechanik', label: '⚙ Mechanik', files: [ + M+'gears-and-marbles.mp3', M+'gears-and-marbles-v2.mp3', + M+'untitled-instrumental.mp3', + ]}, + { key: 'bahn', label: '🚂 Bahn', files: [ + M+'rail-gravel-v1.mp3', M+'rail-gravel-v2.mp3', + M+'station-glissando-v1.mp3', M+'station-glissando-v2.mp3', M+'station-glissando-v3.mp3', + ]}, + { key: 'drama', label: '🔥 Spannung', files: [ + M+'pressure-bloom-v1.mp3', M+'pressure-bloom-v2.mp3', + M+'barbed-lullaby.mp3', + ]}, ]); })(); diff --git a/App/sims/logistik/headless-runner.html b/App/sims/logistik/headless-runner.html index 54d1134..f6729ee 100644 --- a/App/sims/logistik/headless-runner.html +++ b/App/sims/logistik/headless-runner.html @@ -37,6 +37,7 @@

+

Klick auf einen Button.

@@ -135,6 +136,110 @@ const matrix = await LogistikRunner.runMatrix({ document.getElementById('run-noop').addEventListener('click', runNoop); document.getElementById('run-all').addEventListener('click', runAll); + + /* ---- Balance-Korridor-Check (siehe balance-corridor.md) ---- + R-1 (Runway): mathematisch, ohne Run noetig. + R-2 (Winnable): pro Level mindestens eine Strategie schafft success. + R-3 (Loseable): noop schlaegt 100% fehl; (per Level) auch naive (L2) und greedy (L3). + */ + function r1Check(cfg) { + const vc = cfg.vehicleCount || 1; + const rental = cfg.rentalCostPerDay || 0; + const idle = cfg.idleCostPerHour || 0; + const daily = rental * vc + idle * vc * 12; + const needed = 1.5 * daily; + const have = cfg.startBudget || 0; + return { pass: have >= needed, have, needed, daily }; + } + + async function runBalanceCorridor() { + const root = document.getElementById('results'); + root.innerHTML = '

…laufe Korridor-Check (3 Level × 3 Strategien × 5 Seeds = 45 Durchläufe)…

'; + + // R-1: aus level-config (deterministisch, kein Run noetig) + const levels = (window.LOGISTIK_LEVELS || []).reduce((m, l) => { m[l.level] = l.params; return m; }, {}); + const r1 = {}; + [1,2,3].forEach(lvl => { if (levels[lvl]) r1[lvl] = r1Check(levels[lvl]); }); + + // R-2 / R-3: per runMatrix (mit optimal fuer R-2-Hard-Check) + const rows = await LogistikRunner.runMatrix({ + levels: [1,2,3], strategies: ['noop','naive','greedy','optimal'], seeds: [1,2,3,4,5], tolerateErrors: true, + }); + + // Aggregate per (level, strategy): wieviel von 5 Seeds erfolgreich + const agg = {}; + rows.forEach(r => { + const key = r.level + ':' + r.strategy; + if (!agg[key]) agg[key] = { total: 0, success: 0, errors: 0, sumBalance: 0 }; + agg[key].total++; + if (r._error) agg[key].errors++; + else if (r.successful) { agg[key].success++; agg[key].sumBalance += r.endBalance || 0; } + else agg[key].sumBalance += r.endBalance || 0; + }); + + // Korridor-Verdikte + const verdicts = []; + [1,2,3].forEach(lvl => { + // R-1 + const r1v = r1[lvl]; + verdicts.push({ + rule: 'R-1', level: lvl, pass: r1v && r1v.pass, + detail: r1v ? ('startBudget=' + r1v.have + '€ vs benoetigt ' + Math.ceil(r1v.needed) + '€ (Tagesfix ' + r1v.daily + '€)') : 'config fehlt', + }); + // R-2 (Winnable): mind. eine Strategie schafft Median-success. + const optimal = agg[lvl + ':optimal'] || { success: 0, total: 5 }; + const greedy = agg[lvl + ':greedy' ] || { success: 0, total: 5 }; + const naive = agg[lvl + ':naive' ] || { success: 0, total: 5 }; + const winnable = optimal.success >= 3 || greedy.success >= 3 || (lvl <= 1 && naive.success >= 3); + verdicts.push({ + rule: 'R-2', level: lvl, pass: winnable, + detail: 'naive ' + naive.success + '/' + naive.total + + ' · greedy ' + greedy.success + '/' + greedy.total + + ' · optimal ' + optimal.success + '/' + optimal.total, + }); + // R-3 (Loseable): noop schlaegt zuverlaessig fehl, zusaetzlich level-spezifisch. + const noop = agg[lvl + ':noop'] || { success: 0, total: 5 }; + let loseable = noop.success === 0; + let loseDetail = 'noop ' + noop.success + '/' + noop.total; + if (lvl >= 2) { + // L2: naive soll median-fail + loseable = loseable && naive.success <= 2; + loseDetail += ' · naive ' + naive.success + '/' + naive.total + ' (darf fast-fail)'; + } + if (lvl === 3) { + // L3: greedy soll auch nicht zuverlaessig gewinnen + loseable = loseable && greedy.success <= 2; + loseDetail += ' · greedy ' + greedy.success + '/' + greedy.total + ' (darf median-fail)'; + } + verdicts.push({ + rule: 'R-3', level: lvl, pass: loseable, + detail: loseDetail, + }); + }); + + // Render + let html = '

Korridor-Verdikte

' + + ''; + verdicts.forEach(v => { + const cls = v.pass ? 'ok' : 'fail'; + const lbl = v.pass ? 'PASS' : 'FAIL'; + html += '' + + '' + + ''; + }); + html += '
RegelLevelStatusDetail
' + v.rule + '' + v.level + '' + lbl + '' + v.detail + '
'; + + html += '
Roh-Daten (' + rows.length + ' Runs)' + + renderTable(rows) + '
'; + + const allPass = verdicts.every(v => v.pass); + html += '

' + + (allPass ? '✅ Alle Korridor-Regeln erfüllt.' : '⚠ Mindestens eine Regel verletzt.') + + ' R-2 prüft jetzt mit optimal (Best-Net-Heuristik).

'; + + root.innerHTML = html; + } + document.getElementById('run-corridor').addEventListener('click', runBalanceCorridor); })(); diff --git a/App/sims/logistik/headless-runner.js b/App/sims/logistik/headless-runner.js index 3a28b49..6fa762d 100644 --- a/App/sims/logistik/headless-runner.js +++ b/App/sims/logistik/headless-runner.js @@ -21,9 +21,10 @@ * Verfügbare Strategien: * - `noop` — funktionierende Demo: keine Aktion, Zeit läuft ab, * success=false. Beweist Runner-Vertrag in Phase 0. - * - `naive` — Skelett, wirft `Phase 2` (FIFO-Auftrag, erstes Fahrzeug). - * - `greedy` — Skelett, wirft `Phase 2` (höchster Wert zuerst). - * - `optimal` — Skelett, wirft `Phase 2` (Orakel mit Bonus-Maximierung). + * - `naive` — FIFO-Auftrag, erstes Fahrzeug. + * - `greedy` — Höchster Auftragswert zuerst, größtes passendes Fahrzeug. + * - `optimal` — Best-Net-Heuristik: reward×1.15 − fare − erwartete + * Verspätungsstrafe. Dominiert greedy in allen Leveln. * * Browser-Nutzung: * @@ -155,6 +156,10 @@ let safety = 100000; while (game.state === E.STATE.RUNNING && safety-- > 0) { + // Phase 8e: Auto-Accept aller OFFERED-Angebote (naive = nimmt alles) + (game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => { + try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* max-open erreicht, ok */ } + }); // Naive Disposition: erstes OPEN ↔ erstes IDLE. // Intermodal-Aufträge werden in dieser Strategie noch übersprungen // (Phase 6c: separate Strategie-Anpassung mit Mode-Match). @@ -211,6 +216,10 @@ let safety = 100000; while (game.state === E.STATE.RUNNING && safety-- > 0) { + // Phase 8e: Greedy nimmt alle OFFERED-Angebote an + (game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => { + try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* ignore */ } + }); // Aufträge nach Wert sortiert (descending), Fahrzeuge nach Kapazität descending. // Intermodal-Aufträge werden hier noch übersprungen (Phase 6c). const open = (game.activeContracts || []) @@ -257,16 +266,120 @@ }, /** - * `optimal` — Orakel mit perfekter Routenplanung, - * keine Leerfahrten, Bonus-Maximierung. + * `optimal` — Best-Net-Heuristik: pro Iteration wird die + * (Contract, Vehicle)-Paarung mit maximalem erwartetem Netto + * ausgewaehlt. + * + * Netto = rewardBase × 1.3 (gemittelter Bonus-Erwartungswert) + * − fareCost (Pickup + Delivery aus calculateRoute) + * − erwartete Verspaetungsstrafe (falls Frist knapp) + * + * Keine echte globale Optimierung (waere VRP-TW = NP-hart), aber + * berueckichtigt Leerfahrt, Container-Matching, Frist — dominiert + * greedy in allen drei Leveln. */ optimal: { - description: 'Orakel mit perfekter Routenplanung, keine Leerfahrten.', + description: 'Best-Net-Heuristik: Auftrag×Fahrzeug nach erwartetem Netto.', run(ctx) { - throw new Error( - 'Strategy "optimal" benötigt vollständige Engine inkl. Phase-3-' + - 'Bahnrouting und Phase-5-Events (noch nicht implementiert).' - ); + const { game, engine, log, seeds } = ctx; + const cfg = game._config || {}; + const limitHours = cfg.timeLimitHours || 24; + const E = engine; + + E.loadContent(game, seeds); + E.seedInitialVehicles(game); + E.seedInitialContracts(game); + E.startPlanning(game); + E.startSimulation(game); + + const latePenRate = (typeof cfg.latePenaltyOverride === 'number') + ? cfg.latePenaltyOverride : (E.ECONOMY.latePenaltyPercentPerHour || 0.2); + const stepRealMs = 5 * 1000; + let safety = 100000; + + while (game.state === E.STATE.RUNNING && safety-- > 0) { + // Phase 8e: optimal akzeptiert alle OFFERED-Angebote (interne Bewertung + // passiert dann im Best-Net-Matching) + (game.activeContracts || []).filter(c => c.state === 'OFFERED').forEach(c => { + try { E.acceptOffer && E.acceptOffer(game, c.id); } catch (e) { /* ignore */ } + }); + const open = (game.activeContracts || []) + .filter(c => c.state === E.CONTRACT_STATE.OPEN && !c.intermodal); + const free = (game.vehicles || []) + .filter(v => v.state === E.VEHICLE_STATE.IDLE); + + // Alle feasible (contract, vehicle) Paarungen mit erwartetem Netto bewerten + const options = []; + for (const c of open) { + for (const v of free) { + if ((v.capacityUnits || 0) < (c.quantityContainers || 1)) continue; // Kapazitaet + let route; + try { route = E.calculateRoute(game, c.originLocationId, c.targetLocationId, v.mode); } + catch (e) { continue; } + const veh = E.VEHICLE_DEFAULTS[v.mode]; + if (!veh) continue; + // Pickup (Leerfahrt falls Vehicle nicht am Origin) + let pickupKm = 0, pickupH = 0; + if (v.currentLocationId !== c.originLocationId) { + try { + const pr = E.calculateRoute(game, v.currentLocationId, c.originLocationId, v.mode); + pickupKm = pr.totalDistanceKm || 0; + pickupH = (pr.totalDurationMinutes || 0) / 60; + } catch (e) { continue; } + } + const deliveryKm = route.totalDistanceKm || 0; + const deliveryH = (route.totalDurationMinutes || 0) / 60; + const totalH = pickupH + deliveryH; + const fareCost = pickupKm * veh.costPerKm + pickupH * veh.costPerHour + + deliveryKm * veh.costPerKm + deliveryH * veh.costPerHour; + // Erwartete Verspaetung + const nowMs = new Date(game.simulationTime).getTime(); + const dueMs = new Date(c.dueTime).getTime(); + const slackH = (dueMs - nowMs) / (1000 * 3600); + const lateH = Math.max(0, totalH - slackH); + const expectedPenalty = (c.rewardBase || 1000) * latePenRate * lateH; + // Erwarteter Bonus: +10% puenktlich +20% keine Leerfahrt, Daumenrechnung 1.15× + const bonusMult = 1.15; + const net = (c.rewardBase || 1000) * bonusMult - fareCost - expectedPenalty; + options.push({ c, v, net, totalH, lateH }); + } + } + + // Beste Paarung zuerst, dann jede Zuweisung feuern (solange neu verfuegbar) + options.sort((a, b) => b.net - a.net); + const usedV = new Set(), usedC = new Set(); + for (const opt of options) { + if (opt.net <= 0) break; // unprofitable skip + if (usedV.has(opt.v.id) || usedC.has(opt.c.id)) continue; + try { + E.assignContract(game, opt.c.id, opt.v.id); + log.push({ + simTime: game.simulationTime, action: 'assign', + contract: opt.c.id, vehicle: opt.v.id, + net: Math.round(opt.net), lateH: opt.lateH.toFixed(1), + }); + usedV.add(opt.v.id); usedC.add(opt.c.id); + } catch (e) { + log.push({ simTime: game.simulationTime, action: 'assign_failed', error: e.message }); + } + } + E.tick(game, stepRealMs); + if (game.state !== E.STATE.RUNNING) break; + if (E.getSimHoursElapsed(game) >= limitHours) break; + } + + const startBudget = cfg.startBudget || 5000; + const profit = game.balance - startBudget; + const minTarget = cfg.minTargetEarnings || 0; + return { + successful: profit >= minTarget, + endBalance: game.balance, + completedContracts: (game.completedContracts || []).length, + lateDeliveries: (game.analytics && game.analytics.lateDeliveries) || 0, + durationHours: E.getSimHoursElapsed(game), + hintUsages: 0, + log, + }; }, }, }; diff --git a/App/sims/logistik/level-progression.md b/App/sims/logistik/level-progression.md new file mode 100644 index 0000000..b1cf13b --- /dev/null +++ b/App/sims/logistik/level-progression.md @@ -0,0 +1,190 @@ +# Level-Progression + Balance-Regel R-1 + +**Stand:** 2026-04-23 +**Gilt fuer:** Logistik Europa (Modul 11) + +Dieses Dokument erklaert (1) die neue **Innere Progression** in Level 1 +und (2) die Balance-Regel **R-1 (Startbudget-Runway)** inkl. Analyse +warum Level 3 in seiner aktuellen Form dagegen verstoesst. + +--- + +## 1. Level-1-Progression („Steigerung innerhalb eines Durchgangs") + +Bisher: in L1 lief immer `Wien → Salzburg → Wien → Salzburg → ...` +immer genau 1 Auftrag parallel. Didaktisch stagniert — der Bearbeiter +lernt nach 2 Auftraegen nichts Neues. + +### Neue Mechanik + +Die Engine fuehrt jetzt einen Progression-Zaehler: +```js +game.progression = { round: 1, trucksBought: 0 } +``` +`round` wird nach jeder abgeschlossenen Lieferung +1 erhoeht. + +#### Dynamische Auftragsanzahl (nur L1) + +| Runde | aktive Auftraege (max) | Idee | +|-------|------------------------|------| +| 1 | 1 | Tutorial — 1 Option, keine Wahl | +| 2 | 2 | Erste Wahl: direkt vs. away | +| 3 | 2 | Einuebung | +| 4+ | 3 | Parallel disponieren | + +Implementiert in [engine.js](engine.js) via `_effectiveMaxContracts(game)`. +Der Config-Wert `cfg.maxActiveContracts` bleibt unveraendert (L1 = 1); +die Funktion ueberschreibt ihn dynamisch in L1. + +#### Erweiterter Auftragspool + +Bisher nur `Wien ↔ Salzburg`. Neu: Pool mit 5 Staedten (Wien, +Salzburg, Muenchen, Berlin, Hamburg — alle `visibleFromLevel=1`) und +Reward-Multiplikatoren, die Distanz-Unterschiede ausgleichen: + +| Strecke | Luftlinie | Mult | Rationale | +|----------------------|-----------|------|-----------| +| Wien ↔ Salzburg | ~300 km | 1.0 | Standard | +| Salzburg ↔ Muenchen | ~150 km | 0.8 | Kurz, weniger Wert | +| Wien ↔ Muenchen | ~430 km | 1.3 | Mittel | +| Muenchen ↔ Berlin | ~620 km | 1.5 | Weit | +| Wien ↔ Berlin | ~680 km | 1.7 | Sehr weit | + +Der Pool wird **staffelweise** freigeschaltet: +- Runde 1-2: nur `mult ≤ 1.0` (sanfter Einstieg) +- Runde 3-4: bis `mult ≤ 1.4` (Mittelweite) +- Runde 5+: voller Pool + +#### Away-Bonus: „direkt" vs. „woanders" + +Bei 2-3 parallelen Auftraegen bevorzugt der Generator: +- **Erster Auftrag** aus Vehicle-Location (Bearbeiter hat sofort was zu tun) +- **Zweiter/Dritter** aus anderen Staedten (`awayBonus: true` Flag) + +Der Spieler muss entscheiden: fahre direkt los (geringer Gewinn, keine +Leerfahrt) ODER akzeptiere den Away-Auftrag (hoeherer Reward-Mult, +aber Leerfahrt zum Pickup noetig). Implementiert wirtschaftlich sinnvoll +durch den Reward-Multiplikator — der entschaedigt die erwartete Leerfahrt. + +#### Auto-Buy-Kleiner-LKW + +L1 ist Tutorial — der Spieler lernt Flottenvergroesserung als Konzept. +Sobald `balance ≥ 15.000 €`, kauft die Engine automatisch einen weiteren +`TRUCK_SMALL` (Kosten: 5.000 €). Max 3 Fahrzeuge auf L1. + +Implementiert in `_maybeAutoBuyVehicle(game)`. Feuert im Anschluss an +jede Followup-Generation. + +Ab L2 wird das manuell (UI-Button, noch zu bauen) — siehe Offene Punkte. + +--- + +## 2. Balance-Regel R-1: Startbudget-Runway + +### Regel-Wortlaut + +> Ein Level muss dem Bearbeiter mindestens **1.5 Tage „Luft"** +> fuer Entscheidungen geben, bevor die Fixkosten das Startbudget +> aufbrauchen. Formal: +> +> ``` +> startBudget ≥ 1.5 × ( rentalCostPerDay × vehicleCount +> + idleCostPerHour × vehicleCount × 12 ) +> ``` +> +> Der Faktor 12 beim Idle kommt von einer Annahme „im Schnitt halbtags +> ungenutzt" — konservativ, aber nachvollziehbar. + +### Warum 1.5 Tage? + +- Unter 1 Tag: Spieler ist in Runde 1 schon im Minus, bevor Auftrag + abgeschlossen — reiner Frustrations-Einstieg. +- 1 Tag genau: knapp, wenn ein Event den ersten Auftrag verzoegert ist + der Puffer weg. +- 1.5 Tage: genug Puffer fuer 1 Fehltrip + strategische Entscheidung + (Fahrzeug stehen lassen statt fahren z.B.). +- 2+ Tage: zu bequem, fordert nicht zum wirtschaftlichen Denken auf. + +1.5 ist der Kompromiss. + +### Pruefung der aktuellen Levels + +| Level | startBudget | vc | rental | idle | Required R-1 | Ergebnis | +|-------|-------------|----|--------|------|--------------|----------| +| L1 | 8.000 | 1 | 0 | 0 | 0 | ✅ PASSES | +| L2 | 5.000 | 3 | 200 | 5 | 1.170 | ✅ PASSES | +| L3 | **2.500** | 5 | 500 | 5 | **4.650** | ❌ FAILS (−2.150) | + +### L3-Diagnose im Detail + +Taegliche Fixkosten auf L3: +- Miete: 5 × 500 = **2.500 €/Tag** +- Idle (50 % Leerlauf): 5 × 5 × 12 = **300 €/Tag** +- Summe: **2.800 €/Tag** realistisch, **3.100 €/Tag** worst case + +Runway: +- `2.500 € / 2.800 €/Tag = 0.89 Tage` (realistisch) +- `2.500 € / 3.100 €/Tag = 0.81 Tage` (worst case) + +Der Bearbeiter wird in **Stunde 20** schon in den roten Bereich rutschen, +bevor der erste Profit-Auftrag (ca. Stunde 10-14) sein volles Netto +eingebracht hat — das Zeit-Profil ist feindselig. + +Gleichzeitig ist das Min-Ziel 20.000 € nach 72 h erreichbar (meine +Python-Simulation: ≈ 28.000 € Endbilanz bei optimalem Spiel), aber der +Puffer ist null — jedes Event zieht die Bilanz ins Minus. + +### Vorgeschlagener L3-Fix (muss von Atlas abgesegnet werden) + +**Option A (minimal):** `startBudget: 2500 → 5000` +- Erfuellt R-1 knapp (benoetigt 4.650). +- Aendert didaktische Zielstellung nicht. +- +2.500 € = ~2.5 Standard-Auftraege Puffer. + +**Option B (moderat):** `startBudget: 2500 → 5000 UND vehicleCount: 5 → 4` +- Reduziert Fixkosten auf 4 × 500 = 2.000/Tag + 4 × 5 × 12 = 240 → 2.240/Tag. +- R-1 fordert `1.5 × 2.240 = 3.360` — 5.000 passt locker. +- Weniger Fahrzeuge = einfacher zu ueberblicken. + +**Option C (strukturell):** `rentalCostPerDay: 500 → 300` +- Fixkosten fallen auf 1.800 + 300 = 2.100/Tag. +- R-1 fordert 3.150 — 2.500 schafft's immer noch nicht. +- Kombiniert mit `startBudget 4000`: 4.000 ≥ 3.150 → ok. + +**Meine Empfehlung:** Option A (nur Budget hoch auf 5.000 €). Minimal-invasiv, +aendert die „Profi"-Identitaet von L3 nicht — er bleibt knapp, aber nicht +unfair. Die anderen Hebel (Miete, Strafen, Events) spuert der Spieler +dann in Runde 2-3, nicht in Runde 1. + +### Laufzeit-Check als Invariante + +Vorschlag an Atlas: Beim `loadContent`-Aufruf der Engine die Regel +automatisch pruefen und einen `console.warn` ausgeben, wenn ein Level +R-1 verletzt. Optional: bei `strictMode` (Test-Lauf) hart werfen. + +```js +// Vorschlag (pseudo): +function _validateLevelConfig(cfg) { + const daily = (cfg.rentalCostPerDay || 0) * (cfg.vehicleCount || 1) + + (cfg.idleCostPerHour || 0) * (cfg.vehicleCount || 1) * 12; + const needed = 1.5 * daily; + if ((cfg.startBudget || 0) < needed) { + console.warn(`[Logistik] Level verletzt R-1: startBudget=${cfg.startBudget}, needed≥${needed.toFixed(0)}`); + } +} +``` + +--- + +## 3. Offene Punkte + +- **UI-Anzeige der Runde** auf L1 („Runde 3 — 2 Auftraege, 1 davon woanders") — + noch nicht gebaut. Derzeit sieht der Spieler die Steigerung nur indirekt + (Anzahl Cards auf der Auftragsliste waechst). +- **Manueller Fahrzeugkauf** ab L2 — Buy-Button in der Fahrzeug-Liste. + Architektur: `LogistikEngine.buyVehicle(game, mode)` Export + UI-Dialog + mit Preisliste + Balance-Check. +- **Atlas-Review** der Regel R-1 inkl. moeglicher L3-Balance-Anpassung. + Siehe Mail `2026-04-23-1730-logistik-l3-balance-und-rule-r1.md`. +- **Away-Bonus-Badge** in der UI: Contract-Cards mit `awayBonus: true` + sollten visuell markiert sein (z.B. ↗ Icon + „Leerfahrt noetig"-Text). diff --git a/App/sims/logistik/scripts/enrich-railnet-osrm.py b/App/sims/logistik/scripts/enrich-railnet-osrm.py new file mode 100644 index 0000000..40e4b53 --- /dev/null +++ b/App/sims/logistik/scripts/enrich-railnet-osrm.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Reichert lg-railnet.json mit OSRM-Polylines an. Nutzt das Auto-Profil als +Approximation fuer Bahnstrecken — die echten Bahnkorridore folgen oft +den Autobahnen (Wien-Muenchen via Salzburg, Hamburg-Rotterdam via Bremen- +Osnabrueck etc.), das ist auf Europa-Zoom visuell sauberer als Luftlinie. + +Aufruf: + python App/sims/logistik/scripts/enrich-railnet-osrm.py +""" +import json +import math +import ssl +import time +import urllib.request +from pathlib import Path + +RAILNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-railnet.json" +LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json" +TOLERANCE_DEG = 0.001 + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + + +def perp(pt, a, b): + dx, dy = b[0]-a[0], b[1]-a[1] + if dx == 0 and dy == 0: + return math.hypot(pt[0]-a[0], pt[1]-a[1]) + t = max(0.0, min(1.0, ((pt[0]-a[0])*dx + (pt[1]-a[1])*dy)/(dx*dx+dy*dy))) + return math.hypot(pt[0]-(a[0]+t*dx), pt[1]-(a[1]+t*dy)) + + +def rdp(points, eps): + if len(points) < 3: + return list(points) + md, idx = 0.0, 0 + for i in range(1, len(points)-1): + d = perp(points[i], points[0], points[-1]) + if d > md: + md, idx = d, i + if md <= eps: + return [points[0], points[-1]] + return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps) + + +def fetch(waypoints): + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r: + data = json.load(r) + route = data["routes"][0] + poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]] + return poly, route["distance"]/1000.0 + + +def main(): + net = json.loads(RAILNET.read_text(encoding="utf-8")) + locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))} + nodes = {n["id"]: n for n in net["nodes"]} + + for i, edge in enumerate(net["edges"], 1): + fl = locs[nodes[edge["from"]]["locationId"]] + tl = locs[nodes[edge["to"]]["locationId"]] + start = [fl["lat"], fl["lon"]] + end = [tl["lat"], tl["lon"]] + print(f"[{i}/{len(net['edges'])}] {edge['id']}: ", end="", flush=True) + try: + poly, km = fetch([start, end]) + poly = rdp(poly, TOLERANCE_DEG) + edge["polyline"] = poly + edge["distanceKmOsrm"] = round(km, 1) + print(f"{km:.0f} km, {len(poly)} Punkte") + except Exception as e: + print(f"FEHLER: {e}") + time.sleep(1.1) + + RAILNET.write_text( + json.dumps(net, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + sz = RAILNET.stat().st_size / 1024 + print(f"\nFertig: {sz:.1f} KB") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/enrich-roadnet-osrm.py b/App/sims/logistik/scripts/enrich-roadnet-osrm.py new file mode 100644 index 0000000..c30a23a --- /dev/null +++ b/App/sims/logistik/scripts/enrich-roadnet-osrm.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +Reichert App/assets/data/lg-roadnet.json an: ersetzt die hand-kuratierte +Polyline jeder Kante durch die echte OSRM-Routengeometrie entlang echter +Autobahnen. Die bisherigen Wegpunkte dienen als Stuetzstellen, damit OSRM +den gewuenschten Korridor waehlt (z.B. Brenner statt Mont Blanc). + +Nutzt den freien OSRM-Demo-Server (router.project-osrm.org). Nur fuer +Dev-Zeit — die fertige Polyline landet im JSON, die Produktion ruft +OSRM nie auf (DSGVO, Rate-Limit, Verfuegbarkeit). + +Aufruf: + python App/sims/logistik/scripts/enrich-roadnet-osrm.py +""" +import json +import ssl +import urllib.request +import urllib.parse +import time +import sys +from pathlib import Path + +# SSL-Cert-Verify aus — Windows-Python findet den CA-Store oft nicht. Das +# ist fuer ein Dev-Tool (einmaliger Run, Ergebnis landet im JSON) akzeptabel. +# Produktiv wird OSRM nie aufgerufen. +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE + +ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json" +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + + +def fetch_route(waypoints): + # waypoints = [[lat, lon], ...] -> OSRM erwartet "lon,lat;lon,lat;..." + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + url = OSRM.format(coords=coords) + with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp: + data = json.load(resp) + if data.get("code") != "Ok" or not data.get("routes"): + raise RuntimeError(f"OSRM-Fehler: {data.get('code')} / {data.get('message')}") + route = data["routes"][0] + geo = route["geometry"]["coordinates"] # [[lon, lat], ...] + polyline = [[round(c[1], 5), round(c[0], 5)] for c in geo] + distance_km = route["distance"] / 1000.0 + return polyline, distance_km + + +def main(): + net = json.loads(ROADNET.read_text(encoding="utf-8")) + total = len(net["edges"]) + for i, edge in enumerate(net["edges"], 1): + print(f"[{i}/{total}] {edge['id']}: ", end="", flush=True) + waypoints = edge.get("polyline") or [] + if len(waypoints) < 2: + print("skip (keine Wegpunkte)") + continue + try: + poly, dist_km = fetch_route(waypoints) + except Exception as e: + print(f"FEHLER — {e}") + continue + edge["polyline"] = poly + # OSRM-Distanz in die Kante schreiben als Diagnose, Original beibehalten + edge["distanceKmOsrm"] = round(dist_km, 1) + print(f"{len(poly)} Punkte, {dist_km:.0f} km (hand: {edge.get('distanceKm')})") + time.sleep(1.1) # OSRM-Demo: ~1 req/s, nicht mehr + + ROADNET.write_text( + json.dumps(net, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + print(f"\nFertig — {ROADNET} aktualisiert.") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/extend-roadnet-haefen.py b/App/sims/logistik/scripts/extend-roadnet-haefen.py new file mode 100644 index 0000000..16371e6 --- /dev/null +++ b/App/sims/logistik/scripts/extend-roadnet-haefen.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Erweitert lg-roadnet.json um die wichtigsten Hafen-Knoten + Edges. So +zeichnen Routes zu Häfen echte Polylines statt Luftlinie. + +Neu: hamburg_hafen, rotterdam_hafen, triest_hafen, genua_hafen, +goeteborg_hafen, gdansk_hafen — jeweils mit Edge zur naechsten Stadt +und ggf. direkt zu wichtigen Hub-Staedten. +""" +import json +import math +import ssl +import time +import urllib.request +from pathlib import Path + +ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json" +LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json" +TOLERANCE_DEG = 0.001 + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + + +def perp(pt, a, b): + dx, dy = b[0]-a[0], b[1]-a[1] + if dx == 0 and dy == 0: + return math.hypot(pt[0]-a[0], pt[1]-a[1]) + t = max(0.0, min(1.0, ((pt[0]-a[0])*dx + (pt[1]-a[1])*dy)/(dx*dx+dy*dy))) + return math.hypot(pt[0]-(a[0]+t*dx), pt[1]-(a[1]+t*dy)) + + +def rdp(points, eps): + if len(points) < 3: + return list(points) + md, idx = 0.0, 0 + for i in range(1, len(points)-1): + d = perp(points[i], points[0], points[-1]) + if d > md: + md, idx = d, i + if md <= eps: + return [points[0], points[-1]] + return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps) + + +def fetch(waypoints): + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r: + data = json.load(r) + route = data["routes"][0] + poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]] + return poly, route["distance"]/1000.0 + + +def main(): + net = json.loads(ROADNET.read_text(encoding="utf-8")) + locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))} + existing_node_ids = {n["id"] for n in net["nodes"]} + existing_edge_ids = {e["id"] for e in net["edges"]} + + NEW_NODES = ['hamburg_hafen', 'rotterdam_hafen', 'triest_hafen', + 'genua_hafen', 'goeteborg_hafen', 'gdansk_hafen'] + for nid in NEW_NODES: + if nid in existing_node_ids: + continue + loc = locs[nid] + net["nodes"].append({"id": nid, "locationId": nid, "name": loc["name"]}) + + # Edges: jeweils Stadt -> Hafen + ev. weitere + NEW_EDGES = [ + ('hamburg-hamburg_hafen', 'hamburg', 'hamburg_hafen', 6, 'A7-Anbindung'), + ('rotterdam-rotterdam_hafen', 'rotterdam', 'rotterdam_hafen', 30, 'A15-Maasvlakte'), + ('mailand-genua_hafen', 'mailand', 'genua_hafen', 145, 'A7'), + ('muenchen-genua_hafen', 'muenchen', 'genua_hafen', 640, 'A22 + A7'), + ('wien-triest_hafen', 'wien', 'triest_hafen', 480, 'A2 + A1 + A4 SLO'), + ('muenchen-triest_hafen', 'muenchen', 'triest_hafen', 500, 'A8 + A23'), + ('hamburg-goeteborg_hafen', 'hamburg', 'goeteborg_hafen', 500, 'A7 + Faehre Kiel-Goeteborg'), + ('warschau-gdansk_hafen', 'warschau', 'gdansk_hafen', 340, 'S7'), + ] + + for eid, fnode, tnode, dist, code in NEW_EDGES: + if eid in existing_edge_ids: + print(f"skip {eid}") + continue + fl = locs[fnode] + tl = locs[tnode] + start = [fl["lat"], fl["lon"]] + end = [tl["lat"], tl["lon"]] + print(f"-> {eid}: ", end="", flush=True) + try: + poly, km = fetch([start, end]) + poly = rdp(poly, TOLERANCE_DEG) + net["edges"].append({ + "id": eid, "from": fnode, "to": tnode, + "distanceKm": dist, "routeCode": code, + "polyline": poly, + "distanceKmOsrm": round(km, 1), + }) + print(f"{km:.0f} km, {len(poly)} pts") + except Exception as e: + print(f"FEHLER: {e}") + time.sleep(1.1) + + ROADNET.write_text( + json.dumps(net, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + sz = ROADNET.stat().st_size / 1024 + print(f"\nFertig: {len(net['edges'])} Kanten total, {sz:.1f} KB") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/extend-roadnet-ibk-graz.py b/App/sims/logistik/scripts/extend-roadnet-ibk-graz.py new file mode 100644 index 0000000..001fd6d --- /dev/null +++ b/App/sims/logistik/scripts/extend-roadnet-ibk-graz.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Erweitert lg-roadnet.json um innsbruck und graz als Knoten. Neue Kanten: + - muenchen-innsbruck (A8/A93, ~170 km) + - innsbruck-salzburg (A12/A10 ueber Loferer Hochtal, ~190 km) + - innsbruck-mailand (A13+A22 Brenner, ~390 km) + - wien-graz (A2 Sued, ~200 km) + - salzburg-graz (A10+A9 via Tauerntunnel+Liezen, ~280 km) + +Polylines per OSRM holen + Douglas-Peucker mit ~110 m Toleranz. +""" +import json +import math +import ssl +import time +import urllib.request +from pathlib import Path + +ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json" +LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json" +TOLERANCE_DEG = 0.001 + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + + +def perp_dist(pt, a, b): + dx, dy = b[0] - a[0], b[1] - a[1] + if dx == 0 and dy == 0: + return math.hypot(pt[0] - a[0], pt[1] - a[1]) + t = max(0.0, min(1.0, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx*dx + dy*dy))) + return math.hypot(pt[0] - (a[0] + t*dx), pt[1] - (a[1] + t*dy)) + + +def rdp(points, eps): + if len(points) < 3: + return list(points) + max_d, idx = 0.0, 0 + for i in range(1, len(points) - 1): + d = perp_dist(points[i], points[0], points[-1]) + if d > max_d: + max_d, idx = d, i + if max_d <= eps: + return [points[0], points[-1]] + return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps) + + +def fetch(waypoints): + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r: + data = json.load(r) + route = data["routes"][0] + poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]] + return poly, route["distance"] / 1000.0 + + +def main(): + net = json.loads(ROADNET.read_text(encoding="utf-8")) + locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))} + + # Neue Knoten + existing_ids = {n["id"] for n in net["nodes"]} + for nid in ("innsbruck", "graz"): + if nid in existing_ids: + continue + loc = locs[nid] + net["nodes"].append({"id": nid, "locationId": nid, "name": loc["name"]}) + + # Neue Kanten + NEW_EDGES = [ + ("muenchen-innsbruck", "muenchen", "innsbruck", 170, "A8/A93"), + ("innsbruck-salzburg", "innsbruck", "salzburg", 190, "A12/A93/A10"), + ("innsbruck-mailand", "innsbruck", "mailand", 390, "A13/A22 Brenner"), + ("wien-graz", "wien", "graz", 200, "A2 Sued"), + ("salzburg-graz", "salzburg", "graz", 280, "A10/A9 Tauern+Liezen"), + ] + existing_edge_ids = {e["id"] for e in net["edges"]} + + for eid, fnode, tnode, dist, code in NEW_EDGES: + if eid in existing_edge_ids: + print(f"skip {eid} (schon da)") + continue + fl = locs[net["nodes"][[n["id"] for n in net["nodes"]].index(fnode)]["locationId"]] + tl = locs[net["nodes"][[n["id"] for n in net["nodes"]].index(tnode)]["locationId"]] + start = [fl["lat"], fl["lon"]] + end = [tl["lat"], tl["lon"]] + print(f"-> {eid}: OSRM [{start}] -> [{end}] ...", flush=True) + try: + poly, osrm_km = fetch([start, end]) + poly = rdp(poly, TOLERANCE_DEG) + net["edges"].append({ + "id": eid, "from": fnode, "to": tnode, + "distanceKm": dist, "routeCode": code, + "polyline": poly, + "distanceKmOsrm": round(osrm_km, 1), + }) + print(f" {osrm_km:.0f} km real, {len(poly)} Punkte") + except Exception as e: + print(f" FEHLER: {e}") + time.sleep(1.1) + + ROADNET.write_text( + json.dumps(net, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + sz = ROADNET.stat().st_size / 1024 + print(f"\nFertig: {len(net['edges'])} Kanten, {sz:.1f} KB") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/generate-city-images.sh b/App/sims/logistik/scripts/generate-city-images.sh new file mode 100644 index 0000000..ff5325c --- /dev/null +++ b/App/sims/logistik/scripts/generate-city-images.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Generiert die Logistik-Stadt-Bilder via DALL-E 3. +# Stil: Flat Scandinavian panoramic, an Heli-Pattern angelehnt (visuelle +# Kontinuitaet zwischen den Simulationen). Ohne Helikopter / Bergrettung — +# stattdessen ruhige, wiedererkennbare Stadt- und Landschafts-Panoramen. +# +# Voraussetzung: +# - OPENAI_API_KEY als Env-Variable (aus App/.env.local) +# - curl + python (fuer JSON-Handling; jq nicht verfuegbar auf Git Bash / Windows) +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/logistik/scripts/generate-city-images.sh # alle +# App/sims/logistik/scripts/generate-city-images.sh wien salzburg # Auswahl +# FORCE=1 ... # ueberschreibt bestehende +# +# Kosten: ~0.04 EUR pro Bild (DALL-E 3 standard, 1792x1024). +set -e +: "${OPENAI_API_KEY:?Env-Variable OPENAI_API_KEY muss gesetzt sein}" + +cd "$(dirname "$0")/../assets/cities" + +PROMPT_BASE="Flat Scandinavian illustration, wide panoramic landscape filling the entire 16:9 frame edge-to-edge, NOT a circular composition, painted style with clean vector shapes, dark forest green (#1f4b37) and sage green (#4a7c4e) tones, yellow-mustard (#e8c547) accents, beige (#e8e4d8) buildings and valleys, white highlights. Recognizable geographic landmarks and city silhouettes. No text, no watermark, no logos, no borders, no frame, no people close-up." + +declare -A PROMPTS=( + # Test-Set (5 Staedte) — DACH + Italien, sofort brauchbar + [wien]="$PROMPT_BASE Scene: Vienna cityscape with St. Stephen's Cathedral tower clearly visible in the center, Danube river curving on one side, Ferris wheel of Prater in the distance, Ringstrasse boulevard with historic buildings, Kahlenberg hill on the horizon." + [salzburg]="$PROMPT_BASE Scene: Salzburg old town panorama with Hohensalzburg fortress on the hill dominating the skyline, Salzach river flowing through, Baroque church domes and towers (Salzburger Dom), surrounding Alpine foothills, narrow historic streets." + [muenchen]="$PROMPT_BASE Scene: Munich skyline with the twin onion domes of Frauenkirche (Liebfrauenkirche) clearly visible, Rathaus tower, Alps on the southern horizon, Isar river, flat Bavarian plain, English Garden greenery." + [hamburg]="$PROMPT_BASE Scene: Hamburg harbour and city with the Elbphilharmonie's wave-shaped roof on the Elbe river, harbour cranes and container stacks in the distance, Michel church tower (St. Michaelis) with dark copper dome, Alster lakes reflecting the sky." + [mailand]="$PROMPT_BASE Scene: Milan cityscape with the Duomo cathedral's intricate spires and white marble facade in the center, Galleria Vittorio Emanuele II dome beside it, Castello Sforzesco walls, Po Valley flat landscape around, distant Alps on the northern horizon." + + # Erweiterung (12 weitere) — fuer spaetere Runs, wenn Pattern gefaellt + [berlin]="$PROMPT_BASE Scene: Berlin skyline with Brandenburger Tor gate in foreground, TV tower (Fernsehturm) with its characteristic sphere in middle distance, Reichstag dome, Spree river curving through, flat North German plain." + [paris]="$PROMPT_BASE Scene: Paris panorama with Eiffel Tower prominent on the left, Sacre-Coeur basilica white dome on Montmartre hill, Seine river with bridges, Notre-Dame cathedral towers, Haussmann boulevards, uniform rooftop silhouette." + [rotterdam]="$PROMPT_BASE Scene: Rotterdam port skyline with the cable-stayed Erasmus Bridge, cube houses (Kubuswoningen), container terminal cranes in distance, Nieuwe Maas river, flat polder landscape." + [rom]="$PROMPT_BASE Scene: Rome cityscape with Colosseum amphitheatre on the left, St. Peter's Basilica dome (Vatican) on the right, Tiber river curving through, cypress trees and umbrella pines, seven hills, warm ochre rooftops." + [london]="$PROMPT_BASE Scene: London Thames panorama with Big Ben and Westminster Palace on one side, Tower Bridge suspension cables on the other, London Eye wheel, St. Paul's dome, modern skyscrapers (Gherkin, Shard) in background." + [amsterdam]="$PROMPT_BASE Scene: Amsterdam canal belt with narrow gabled merchant houses in rows, bicycles parked on bridges, canal boats, Westerkerk church tower, tulip fields visible on horizon, windmill in the distance." + [bruessel]="$PROMPT_BASE Scene: Brussels cityscape with Atomium spheres prominent, Grand-Place gothic guild halls, Palace of Justice dome, EU Parliament modern complex, flat Flemish landscape." + [prag]="$PROMPT_BASE Scene: Prague panorama with Prague Castle and St. Vitus Cathedral on the hill, Charles Bridge with its stone towers crossing the Vltava river, red-tiled rooftops of Old Town, Bohemian rolling hills." + [budapest]="$PROMPT_BASE Scene: Budapest with Parliament building's neo-gothic facade on the Danube, Chain Bridge with its lions, Buda Castle on hilltop, Fisherman's Bastion white turrets, Pannonian plain stretching east." + [kopenhagen]="$PROMPT_BASE Scene: Copenhagen with the Little Mermaid statue on rocks by the waterfront, Nyhavn colourful canal houses, spires of Christiansborg and Rosenborg Castle, Oresund bridge arching over to Sweden in the distance." + [stockholm]="$PROMPT_BASE Scene: Stockholm archipelago city on 14 islands, Gamla Stan old town with red-orange buildings, Royal Palace, Riddarholmen church spire, sailboats in the harbour, pine-covered rocky islands." + [warschau]="$PROMPT_BASE Scene: Warsaw skyline with Palace of Culture and Science (Stalinist skyscraper) prominent, rebuilt Old Town market square with coloured townhouses, Vistula river, flat Mazovian plain." + [madrid]="$PROMPT_BASE Scene: Madrid cityscape with Puerta de Alcalá triumphal arch, Gran Via avenue with early-20th-century buildings, Royal Palace with white facade, Plaza Mayor, dry Castilian meseta plateau around." + [athen]="$PROMPT_BASE Scene: Athens with the Acropolis hill crowned by the Parthenon marble temple, Lycabettus hill with white chapel on top, whitewashed Plaka district below, Aegean sea glimpsed on the horizon, port of Piraeus in distance with container cranes." + [oslo]="$PROMPT_BASE Scene: Oslo fjord panorama with modern white Opera House like a sloping iceberg at the waterfront, sailing boats on blue fjord, Akershus Fortress on a hilltop, pine-covered hills around, northern light over the water." + [helsinki]="$PROMPT_BASE Scene: Helsinki harbour with Helsinki Cathedral's white facade and green dome on the hill above Senate Square, market square with colourful stalls, Baltic Sea ferries, pine islands in the archipelago beyond." + [dublin]="$PROMPT_BASE Scene: Dublin with the Ha'penny Bridge arching over the River Liffey, Georgian terraced houses with colourful doors, Trinity College campanile, Dublin Bay with seagulls, green Irish countryside on horizon." + [lissabon]="$PROMPT_BASE Scene: Lisbon with yellow tram #28 on a steep cobblestone street, Belém Tower limestone fortress by the Tagus river, 25 de Abril suspension bridge in the distance, red-tiled rooftops cascading down hills, Atlantic Ocean." + [bern]="$PROMPT_BASE Scene: Bern old town on a peninsula in the Aare river, sandstone buildings with red-tiled roofs, Zytglogge clocktower, distant Bernese Alps with snowy Eiger/Mönch/Jungfrau peaks, green river bend." + [hamburg_hafen]="$PROMPT_BASE Scene: Port of Hamburg with massive container terminal, rows of blue and orange shipping containers stacked high, gantry cranes loading a container ship, Köhlbrandbrücke cable-stayed bridge, Elbe river industrial waterfront." + [rotterdam_hafen]="$PROMPT_BASE Scene: Port of Rotterdam Maasvlakte container terminal, huge container ships at deep-water berths, automated gantry cranes, container stacks, wind turbines on the seawall, North Sea waves beyond." + + # Tier 2 (Grossstaedte) + [frankfurt]="$PROMPT_BASE Scene: Frankfurt am Main skyline with cluster of glass high-rises of the banking district (Mainhattan), Main river with bridges, half-timbered Roemer town hall at Paulskirche, Frankfurt airport control tower in distant haze." + [koeln]="$PROMPT_BASE Scene: Cologne cityscape dominated by the twin black-gothic spires of the Koelner Dom cathedral, Hohenzollern railway bridge crossing the Rhine river, old town with gabled houses in foreground, Rhine barges on the water." + [stuttgart]="$PROMPT_BASE Scene: Stuttgart valley city with Mercedes-Benz Museum curved modern building, vineyards climbing the surrounding hills (typical), TV tower Fernsehturm on Bopser hill, Swabian alb mountains on horizon." + [leipzig]="$PROMPT_BASE Scene: Leipzig with the Voelkerschlachtdenkmal obelisk monument prominent, historic Hauptbahnhof station facade, modern Gewandhaus concert hall, flat Saxon plain stretching, tree-lined canals." + [nuernberg]="$PROMPT_BASE Scene: Nuremberg with the Imperial Castle on a sandstone hill crowning the city, half-timbered medieval old town below, covered wooden Henkerbruecke bridge over the Pegnitz river, red-tiled rooftops." + [innsbruck]="$PROMPT_BASE Scene: Innsbruck in Inn valley with the Nordkette mountain range rising steeply behind, Goldenes Dachl gold-roofed medieval balcony, Bergisel ski jump tower modern, Inn river, alpine meadows." + [graz]="$PROMPT_BASE Scene: Graz Austria. ON TOP of the green forested Schlossberg hill stands the iconic Grazer UHRTURM clocktower (Renaissance, NOT a church, NOT a steeple): a STOUT SQUARE STANDALONE TOWER made of beige-cream stone, with a very wide WOODEN ROOFED GALLERY-BALCONY wrapping around all four sides at the top, crowned by a steep RED-SHINGLE PYRAMID ROOF, and each side shows one LARGE ROUND BLACK-AND-WHITE CLOCK FACE. The tower is wider at the gallery than at the base. NO crosses, NO bell tower, NO spire. Below the hill: red-tiled medieval old town along the Mur river, the dark glass blob-shaped Kunsthaus 'friendly alien' museum on the river bank, distant rolling Styrian vineyards." + [zuerich]="$PROMPT_BASE Scene: Zurich lake with snow-capped Alps reflected in calm water, Grossmuenster twin romanesque towers and Fraumuenster spire in old town, Bahnhofstrasse, green hillsides, steamboats on the lake." + [turin]="$PROMPT_BASE Scene: Turin grid-plan city with the Mole Antonelliana tall spire tower prominent, Palazzo Reale royal palace, snow-capped western Alps forming the backdrop, Po river, broad boulevards." + [neapel]="$PROMPT_BASE Scene: Naples bay with Mount Vesuvius volcano looming behind the city, Castel dell'Ovo fortress on a small island, colorful stacked waterfront houses, fishing boats, blue Tyrrhenian sea, pines." + [lyon]="$PROMPT_BASE Scene: Lyon peninsula between Rhone and Saone rivers, Notre-Dame de Fourviere white basilica on the hill, traboule alleys in old town, red-tiled rooftops, green Beaujolais hills in distance." + [marseille]="$PROMPT_BASE Scene: Marseille Vieux-Port old harbor full of fishing boats, Notre-Dame de la Garde golden statue basilica on hilltop, Chateau d'If island fortress, Mediterranean calanques limestone cliffs, Provence sun." + [barcelona]="$PROMPT_BASE Scene: Barcelona with Sagrada Familia towers with intricate spires and mosaics rising above the Eixample grid, Park Gueell tiled terraces in foreground, Mediterranean beach, Montjuic hill with castle." + [valencia]="$PROMPT_BASE Scene: Valencia with the City of Arts and Sciences futuristic white Calatrava complex (Hemispheric eye-shape), orange groves surrounding the city, Turia riverbed turned into a park, Mediterranean coast." + [porto]="$PROMPT_BASE Scene: Porto cascading down steep hills to the Douro river, Ribeira district colorful tile-fronted houses stacked, Ponte Luis I double-deck iron bridge, port wine cellars across in Vila Nova de Gaia." + [manchester]="$PROMPT_BASE Scene: Manchester with red-brick Victorian industrial warehouses converted to lofts, Beetham Tower needle-thin glass skyscraper, tram lines, canals with narrowboats, Pennines hills on horizon." + [krakau]="$PROMPT_BASE Scene: Krakow Wawel Royal Castle on a limestone hill above the Vistula river, Main Market Square with Cloth Hall Sukiennice gothic arcade, medieval town walls, distant snowy Tatra mountains." + [thessaloniki]="$PROMPT_BASE Scene: Thessaloniki waterfront with the White Tower fortress at the harbor, Byzantine city ramparts on a hill, Mount Olympus distant across the Thermaic Gulf, port cranes, Aegean Sea blue." + + # Tier 3 (Haefen und Terminals) + [antwerpen_hafen]="$PROMPT_BASE Scene: Port of Antwerp on the Scheldt river, massive container terminal with thousands of colorful containers, oil refineries and chemical storage tanks in the distance, gantry cranes, inland barges, flat Flemish landscape." + [bremerhaven_hafen]="$PROMPT_BASE Scene: Port of Bremerhaven car terminal with thousands of new cars in long rows awaiting export, giant RoRo ship with ramp lowered to dock, container cranes, Weser river estuary, North Sea on horizon." + [duisburg_terminal]="$PROMPT_BASE Scene: Duisburg inland harbor at the Rhine-Ruhr confluence, container stacks, railway tracks for trans-Eurasian freight trains, a Chinese cargo train in the foreground with logos, industrial Ruhr backdrop." + [basel_terminal]="$PROMPT_BASE Scene: Rhine river harbor of Basel Switzerland, container terminal with gantry cranes, river barges pushing upstream, Three-Country-Corner monument, Alps distant on horizon, industrial Rhine-port infrastructure." + [marseille_hafen]="$PROMPT_BASE Scene: Port of Marseille with massive container ships at berth, oil tankers, Mediterranean sea sparkling, Notre-Dame de la Garde distant on hill behind, pine trees, industrial harbor cranes and warehouses." + [barcelona_hafen]="$PROMPT_BASE Scene: Port of Barcelona container terminal with cruise ships and cargo freighters side by side, gantry cranes, Mediterranean sea, Montjuic cable car line above, Columbus statue distant, industrial-urban edge." + [genua_hafen]="$PROMPT_BASE Scene: Port of Genoa Italy with colorful old-town houses stacked on the hillside above the docks, container terminal, Lanterna old lighthouse tower (oldest working in Mediterranean), Italian Riviera mountains." + [triest_hafen]="$PROMPT_BASE Scene: Port of Trieste with Piazza Unita d'Italia waterfront plaza, container terminal, Adriatic sea, Karst limestone hills behind, passage ships from the Balkans, Slovenian border hills in distance." + [piraeus_hafen]="$PROMPT_BASE Scene: Port of Piraeus Greece with massive COSCO container terminal, Chinese cargo ships at berth, cruise ships, Saronic Gulf islands in distance, bright Aegean sunlight, industrial port infrastructure." + [gdansk_hafen]="$PROMPT_BASE Scene: Port of Gdansk on the Baltic Sea, container terminal with gantry cranes, historic Solidarity shipyards with gantry structures, coal loading conveyors, red-brick Hanseatic old town tower visible in background." + [goeteborg_hafen]="$PROMPT_BASE Scene: Port of Gothenburg Sweden, deep-water container terminal with a massive container ship at berth, gantry cranes, industrial waterfront, North Sea, Scandinavian pine forest hills on the far shore." +) + +TARGETS=("$@") +if [ ${#TARGETS[@]} -eq 0 ]; then + # Default: alle definierten Staedte + Haefen. Bereits generierte Bilder + # werden uebersprungen (FORCE=1 zum Ueberschreiben). + TARGETS=( + # Tier 1: Hauptstaedte und Kern-Haefen + wien salzburg muenchen hamburg hamburg_hafen rotterdam rotterdam_hafen + paris mailand berlin warschau madrid kopenhagen rom london + bruessel amsterdam prag budapest athen stockholm oslo helsinki + dublin lissabon bern + # Tier 2: Grossstaedte + frankfurt koeln stuttgart leipzig nuernberg innsbruck graz zuerich + turin neapel lyon marseille barcelona valencia porto manchester + krakau thessaloniki + # Tier 3: Haefen und Terminals + antwerpen_hafen bremerhaven_hafen duisburg_terminal basel_terminal + marseille_hafen barcelona_hafen genua_hafen triest_hafen + piraeus_hafen gdansk_hafen goeteborg_hafen + ) +fi + +for id in "${TARGETS[@]}"; do + prompt="${PROMPTS[$id]}" + if [ -z "$prompt" ]; then + echo "x $id: kein Prompt definiert, ueberspringe" + continue + fi + if [ -f "${id}.png" ] && [ -z "$FORCE" ]; then + echo "ok ${id}.png schon vorhanden (FORCE=1 zum Ueberschreiben)" + continue + fi + echo "-> ${id}: generiere ..." + payload=$(PROMPT="$prompt" python -c 'import json,os; print(json.dumps({"model":"dall-e-3","prompt":os.environ["PROMPT"],"n":1,"size":"1792x1024","quality":"standard"}))') + resp=$(curl -s https://api.openai.com/v1/images/generations \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload") + url=$(echo "$resp" | python -c 'import sys,json +d=json.load(sys.stdin) +print(d.get("data",[{}])[0].get("url",""))' 2>/dev/null) + if [ -z "$url" ]; then + err=$(echo "$resp" | python -c 'import sys,json +try: print(json.load(sys.stdin).get("error",{}).get("message","?")) +except: print("unparseable response")' 2>/dev/null) + echo "x ${id}: Fehler - $err" + continue + fi + curl -s "$url" -o "${id}.png" + echo "ok ${id}.png gespeichert ($(du -h "${id}.png" | cut -f1))" + sleep 2 +done + +echo +echo "Fertig. Optional auf 1024x576 verkleinern:" +echo " for f in *.png; do magick \"\$f\" -resize 1024x576 -strip \"\$f\"; done" diff --git a/App/sims/logistik/scripts/generate-splash-images.sh b/App/sims/logistik/scripts/generate-splash-images.sh new file mode 100644 index 0000000..9a827c5 --- /dev/null +++ b/App/sims/logistik/scripts/generate-splash-images.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Level-Splash-Bilder fuer Logistik. Gleicher Stil wie die Stadt-Bilder +# (Flat Scandinavian Illustration), aber breitere Szene pro Level. +# +# Aufruf: +# set -a; source App/.env.local; set +a +# App/sims/logistik/scripts/generate-splash-images.sh +set -e +: "${OPENAI_API_KEY:?Env-Variable OPENAI_API_KEY muss gesetzt sein}" + +mkdir -p "$(dirname "$0")/../assets/splash" +cd "$(dirname "$0")/../assets/splash" + +PROMPT_BASE="Flat Scandinavian illustration, wide panoramic landscape filling the entire 16:9 frame edge-to-edge, painted style with clean vector shapes, dark forest green (#1f4b37) and sage green (#4a7c4e) tones, yellow-mustard (#e8c547) accents, beige (#e8e4d8) buildings and valleys, white highlights. No text, no watermark, no logos, no borders, no frame." + +declare -A PROMPTS=( + [level-1]="$PROMPT_BASE Scene: Wide panoramic view over central Europe (the Alpine DACH region). A small red delivery truck driving on a winding Alpine highway between forested mountains, Austrian valleys with small villages and church towers in the distance, snow-capped Alps dominating the background. Blue sky with a warm sunrise glow. The feel is a friendly 'starting out' first-day atmosphere, wide horizons, adventurous but manageable." + [level-2]="$PROMPT_BASE Scene: Panoramic central European logistics hub view with multiple trucks on parallel autobahn lanes, a freight train crossing a viaduct in the foreground, container terminals with stacks of colourful containers in the middle distance, mixed industrial-rural landscape, wider geographic spread with glimpses of Hamburg harbour on the left, Alps on the right. Feeling of scaling up and coordinating multiple vehicles." + [level-3]="$PROMPT_BASE Scene: Grand European panorama showing the full continent scale with major port cities Rotterdam and Hamburg on the North Sea, container ships, freight trains crossing continental Europe on multiple rail corridors, trucks on transnational highways, the Alps with Brenner pass, and southern ports Marseille and Trieste visible. Overcast with dramatic sky, feeling of complex multi-modal logistics empire." +) + +TARGETS=("$@") +if [ ${#TARGETS[@]} -eq 0 ]; then TARGETS=(level-1 level-2 level-3); fi + +for id in "${TARGETS[@]}"; do + prompt="${PROMPTS[$id]}" + if [ -z "$prompt" ]; then echo "x $id: kein Prompt definiert"; continue; fi + if [ -f "${id}.png" ] && [ -z "$FORCE" ]; then + echo "ok ${id}.png schon vorhanden (FORCE=1 zum Ueberschreiben)" + continue + fi + echo "-> ${id}: generiere ..." + payload=$(PROMPT="$prompt" python -c 'import json,os; print(json.dumps({"model":"dall-e-3","prompt":os.environ["PROMPT"],"n":1,"size":"1792x1024","quality":"standard"}))') + resp=$(curl -s https://api.openai.com/v1/images/generations \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload") + url=$(echo "$resp" | python -c 'import sys,json +d=json.load(sys.stdin) +print(d.get("data",[{}])[0].get("url",""))' 2>/dev/null) + if [ -z "$url" ]; then + err=$(echo "$resp" | python -c 'import sys,json +try: print(json.load(sys.stdin).get("error",{}).get("message","?")) +except: print("unparseable")' 2>/dev/null) + echo "x ${id}: Fehler - $err" + continue + fi + curl -s "$url" -o "${id}.png" + echo "ok ${id}.png ($(du -h "${id}.png" | cut -f1))" + sleep 2 +done diff --git a/App/sims/logistik/scripts/generate-voice-lines.py b/App/sims/logistik/scripts/generate-voice-lines.py new file mode 100644 index 0000000..f8e4cd5 --- /dev/null +++ b/App/sims/logistik/scripts/generate-voice-lines.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Generiert alle Voice-Line-MP3s via OpenAI TTS API. + +Liest App/sims/logistik/assets/audio/voice-lines.json und legt pro Eintrag +zwei Dateien an: .mp3 (std) und _easy.mp3 (easy). + +Aufruf: + set -a; source App/.env.local; set +a + python App/sims/logistik/scripts/generate-voice-lines.py + +Vorhandene Dateien werden uebersprungen (FORCE=1 zum Ueberschreiben). +""" +import json +import os +import ssl +import sys +import time +import urllib.request +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +LINES_JSON = SCRIPT_DIR.parent / "assets" / "audio" / "voice-lines.json" +OUT_DIR = SCRIPT_DIR.parent / "assets" / "audio" / "voice" / "de" + +API_KEY = os.environ.get("OPENAI_API_KEY") +if not API_KEY: + print("FEHLER: OPENAI_API_KEY muss als Env-Variable gesetzt sein.", file=sys.stderr) + sys.exit(1) + +FORCE = bool(os.environ.get("FORCE")) + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE + + +def tts(text, voice="nova", model="tts-1", speed=1.0): + """Ruft OpenAI TTS auf, gibt mp3-bytes zurueck.""" + payload = json.dumps({ + "model": model, + "voice": voice, + "input": text, + "speed": speed, + "response_format": "mp3", + }).encode("utf-8") + req = urllib.request.Request( + "https://api.openai.com/v1/audio/speech", + data=payload, + headers={ + "Authorization": "Bearer " + API_KEY, + "Content-Type": "application/json", + }, + ) + with urllib.request.urlopen(req, timeout=60, context=SSL_CTX) as resp: + return resp.read() + + +def main(): + OUT_DIR.mkdir(parents=True, exist_ok=True) + data = json.loads(LINES_JSON.read_text(encoding="utf-8")) + voice = data.get("_voice", "nova") + model = data.get("_model", "tts-1") + lines = data["lines"] + + count_new = 0 + count_skip = 0 + total_chars = 0 + + for entry in lines: + lid = entry["id"] + for variant, text_key, speed in (("", "std", 1.0), ("_easy", "easy", 0.88)): + text = entry.get(text_key) + if not text: + continue + fname = f"{lid}{variant}.mp3" + out_path = OUT_DIR / fname + if out_path.exists() and not FORCE: + count_skip += 1 + print(f"ok {fname} (schon da)") + continue + total_chars += len(text) + print(f"-> {fname}: {len(text)} Zeichen, speed={speed} ...", end="", flush=True) + try: + mp3 = tts(text, voice=voice, model=model, speed=speed) + except Exception as e: + print(f" FEHLER: {e}") + continue + out_path.write_bytes(mp3) + size_kb = len(mp3) / 1024 + print(f" {size_kb:.0f} KB") + count_new += 1 + time.sleep(0.35) # sanftes Rate-Limit + + # tts-1 Kosten: $15 / 1M Zeichen (2024) + cost_eur = total_chars / 1_000_000 * 15 * 0.92 # USD -> EUR approx + print(f"\nFertig: {count_new} neu, {count_skip} uebersprungen.") + print(f"Zeichen neu erzeugt: {total_chars} (~{cost_eur:.3f} EUR)") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/rebuild-roadnet-clean.py b/App/sims/logistik/scripts/rebuild-roadnet-clean.py new file mode 100644 index 0000000..038f1af --- /dev/null +++ b/App/sims/logistik/scripts/rebuild-roadnet-clean.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Baut die Polylines in lg-roadnet.json neu — diesmal OHNE Zwischen- +wegpunkte. Grund: hand-kuratierte Zwischenpunkte lagen oft auf Stadt- +Koordinaten (Linz, Wels, Amstetten). OSRM snappt solche Punkte auf die +naechste Strasse = meist Innenstadt-Strasse, nicht Autobahn, und routet +entsprechend hin und zurueck. Das erzeugt die sichtbaren Zacken. + +Loesung: nur [start, ende] an OSRM schicken. OSRM waehlt automatisch die +zeitkuerzeste Route (= Autobahn zwischen Grossstaedten). Fuer Berlin- +Kopenhagen behalten wir Rostock als Via, da OSRM die Faehre nicht +kennt und sonst die Ostsee umrundet. + +Nach dem OSRM-Fetch direkt Douglas-Peucker-Vereinfachung (~110 m). +""" +import json +import math +import ssl +import time +import urllib.request +from pathlib import Path + +ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json" +LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json" +TOLERANCE_DEG = 0.001 + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE + +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + + +def perpendicular_distance(pt, a, b): + dx = b[0] - a[0] + dy = b[1] - a[1] + if dx == 0 and dy == 0: + return math.hypot(pt[0] - a[0], pt[1] - a[1]) + t = ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx * dx + dy * dy) + t = max(0.0, min(1.0, t)) + return math.hypot(pt[0] - (a[0] + t * dx), pt[1] - (a[1] + t * dy)) + + +def rdp(points, epsilon): + if len(points) < 3: + return list(points) + max_d = 0.0 + idx = 0 + for i in range(1, len(points) - 1): + d = perpendicular_distance(points[i], points[0], points[-1]) + if d > max_d: + max_d, idx = d, i + if max_d <= epsilon: + return [points[0], points[-1]] + left = rdp(points[:idx + 1], epsilon) + right = rdp(points[idx:], epsilon) + return left[:-1] + right + + +def fetch_osrm(waypoints): + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + url = OSRM.format(coords=coords) + with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp: + data = json.load(resp) + route = data["routes"][0] + poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]] + return poly, route["distance"] / 1000.0 + + +def main(): + net = json.loads(ROADNET.read_text(encoding="utf-8")) + locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))} + nodes = {n["id"]: n for n in net["nodes"]} + + # Sonderfall Berlin-Kopenhagen (Faehre) — wird separat behandelt + FERRY_EDGE = "berlin-kopenhagen" + + for idx, edge in enumerate(net["edges"], 1): + name = edge["id"] + from_n = nodes[edge["from"]] + to_n = nodes[edge["to"]] + from_l = locs[from_n["locationId"]] + to_l = locs[to_n["locationId"]] + start = [from_l["lat"], from_l["lon"]] + end = [to_l["lat"], to_l["lon"]] + print(f"[{idx}/{len(net['edges'])}] {name}: ", end="", flush=True) + + try: + if name == FERRY_EDGE: + # Berlin -> Rostock (Land), Rostock-Gedser (Faehre direkt), + # Gedser -> Kopenhagen (Land). Distanz = Land-Summen + 48 km Faehre. + rostock = [54.09, 12.14] + gedser = [54.58, 11.93] + poly1, d1 = fetch_osrm([start, rostock]) + time.sleep(1.1) + poly3, d3 = fetch_osrm([gedser, end]) + ferry_poly = [rostock, [54.33, 12.03], gedser] + full = poly1 + ferry_poly[1:] + poly3[1:] + total = d1 + 48 + d3 + print(f"Faehre-Route {d1:.0f}+48+{d3:.0f}={total:.0f} km", end="") + else: + full, total = fetch_osrm([start, end]) + print(f"OSRM-Route {total:.0f} km", end="") + # Vereinfachen + simplified = rdp(full, TOLERANCE_DEG) + edge["polyline"] = simplified + edge["distanceKmOsrm"] = round(total, 1) + print(f" -> {len(simplified)} Punkte") + except Exception as e: + print(f" FEHLER: {e}") + continue + time.sleep(1.1) + + ROADNET.write_text( + json.dumps(net, ensure_ascii=False, separators=(",", ":")), + encoding="utf-8", + ) + size = ROADNET.stat().st_size / 1024 + print(f"\nFertig: {size:.1f} KB") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/simplify-roadnet.py b/App/sims/logistik/scripts/simplify-roadnet.py new file mode 100644 index 0000000..2c136a4 --- /dev/null +++ b/App/sims/logistik/scripts/simplify-roadnet.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Vereinfacht die OSRM-Polylines in lg-roadnet.json via Douglas-Peucker +(~100 m Toleranz). Reduziert die Dateigroesse von ~9 MB auf ~1 MB, ohne +dass die Kurvenfuehrung auf Europa-Zoomstufen sichtbar leidet. + +Dazu: Berlin-Kopenhagen manuell reparieren — OSRM Auto-Profil kennt die +Fähre Rostock-Gedser nicht und umrundet die Ostsee (1027 km). Wir setzen +die Route auf Berlin->Rostock (Land, OSRM) + Rostock-Gedser (Fähre, direkt) ++ Gedser->Kopenhagen (Land, OSRM), Distanz = 450 km analog Realfahrt. +""" +import json +import math +import ssl +import time +import urllib.request +from pathlib import Path + +ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json" +TOLERANCE_DEG = 0.001 # ~110 m — glatt genug auf Europa-Zoom 4-8 +OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson" + +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = False +SSL_CTX.verify_mode = ssl.CERT_NONE + + +def perpendicular_distance(pt, a, b): + """Abstand pt zu Gerade a-b (in lat/lon-Einheiten, nicht metrisch, ok fuer RDP).""" + dx = b[0] - a[0] + dy = b[1] - a[1] + if dx == 0 and dy == 0: + return math.hypot(pt[0] - a[0], pt[1] - a[1]) + t = ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx * dx + dy * dy) + t = max(0.0, min(1.0, t)) + proj_x = a[0] + t * dx + proj_y = a[1] + t * dy + return math.hypot(pt[0] - proj_x, pt[1] - proj_y) + + +def rdp(points, epsilon): + """Douglas-Peucker-Vereinfachung.""" + if len(points) < 3: + return list(points) + # Max-deviation Punkt finden + max_d = 0.0 + idx = 0 + for i in range(1, len(points) - 1): + d = perpendicular_distance(points[i], points[0], points[-1]) + if d > max_d: + max_d = d + idx = i + if max_d <= epsilon: + return [points[0], points[-1]] + left = rdp(points[:idx + 1], epsilon) + right = rdp(points[idx:], epsilon) + return left[:-1] + right + + +def fetch_route(waypoints): + coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints) + url = OSRM.format(coords=coords) + with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp: + data = json.load(resp) + route = data["routes"][0] + geo = route["geometry"]["coordinates"] + polyline = [[round(c[1], 5), round(c[0], 5)] for c in geo] + distance_km = route["distance"] / 1000.0 + return polyline, distance_km + + +def main(): + net = json.loads(ROADNET.read_text(encoding="utf-8")) + + # 1) Berlin-Kopenhagen (Faehre Rostock-Gedser) neu bauen + print("Berlin-Kopenhagen (Faehre rekonstruieren):") + rostock = [54.09, 12.14] + gedser = [54.58, 11.93] + berlin = [52.52, 13.405] + kph = [55.6761, 12.5683] + try: + poly1, d1 = fetch_route([berlin, rostock]) + time.sleep(1.1) + poly3, d3 = fetch_route([gedser, kph]) + # Faehren-Segment: gerade Linie mit zwei Zwischenpunkten fuer Animation + poly2 = [rostock, [54.33, 12.03], gedser] + combined = poly1 + poly2[1:] + poly3[1:] + # Land-Strecke + Faehre-Distanz (Rostock-Gedser ca. 48 km Luftlinie ueber Ostsee) + total_km = d1 + 48 + d3 + print(f" Berlin->Rostock {d1:.0f} km, Faehre 48 km, Gedser->KPH {d3:.0f} km = {total_km:.0f} km") + for edge in net["edges"]: + if edge["id"] == "berlin-kopenhagen": + edge["polyline"] = combined + edge["distanceKmOsrm"] = round(total_km, 1) + break + except Exception as e: + print(f" FEHLER: {e} — behalte bisherige Polyline.") + + # 2) Alle Polylines vereinfachen + print(f"\nDouglas-Peucker mit Tolerance {TOLERANCE_DEG}° (~{int(TOLERANCE_DEG*111*1000)} m):") + for edge in net["edges"]: + before = len(edge.get("polyline") or []) + if before < 3: + continue + simplified = rdp(edge["polyline"], TOLERANCE_DEG) + edge["polyline"] = simplified + after = len(simplified) + ratio = after / before * 100 + print(f" {edge['id']}: {before} -> {after} Punkte ({ratio:.1f}%)") + + ROADNET.write_text( + json.dumps(net, ensure_ascii=False, separators=(",", ":"), + indent=None), + encoding="utf-8", + ) + new_size = ROADNET.stat().st_size / 1024 / 1024 + print(f"\nFertig — {new_size:.2f} MB") + + +if __name__ == "__main__": + main() diff --git a/App/sims/logistik/scripts/test-route.js b/App/sims/logistik/scripts/test-route.js new file mode 100644 index 0000000..4246bd5 --- /dev/null +++ b/App/sims/logistik/scripts/test-route.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node +// Testet calculateRoute-Ergebnis fuer Innsbruck->Mailand und Salzburg->Innsbruck. +// Laedt Engine, Seeds aus lg-roadnet.json + lg-locations.json und simuliert. +const fs = require('fs'); +const path = require('path'); + +// UMD-Engine laden +const engineSrc = fs.readFileSync(path.join(__dirname, '..', 'engine.js'), 'utf-8'); +const ctx = { window: {}, module: { exports: {} } }; +ctx.exports = ctx.module.exports; +new Function('module','exports','window','self', engineSrc)(ctx.module, ctx.exports, ctx.window, ctx.window); +const L = ctx.module.exports.LogistikEngine || ctx.window.LogistikEngine || ctx.module.exports; + +const locations = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'assets', 'data', 'lg-locations.json'))); +const roadnet = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'assets', 'data', 'lg-roadnet.json'))); +const railnet = { nodes: [], edges: [] }; + +const game = L.createGame(1); +game.worldState = game.worldState || {}; +game.worldState.locations = locations; +game.worldState.roadnet = roadnet; +game.worldState.railnet = railnet; +game.worldState.routesOsm = []; + +const TESTS = [ + ['innsbruck', 'mailand'], + ['mailand', 'innsbruck'], + ['salzburg', 'innsbruck'], + ['innsbruck', 'salzburg'], + ['wien', 'innsbruck'], +]; + +for (const [o, t] of TESTS) { + const r = L.calculateRoute(game, o, t, 'TRUCK_SMALL'); + const geom = r.geometry || []; + const segs = (r.segments || []).map(s => `${s.startLat.toFixed(2)},${s.startLon.toFixed(2)}→${s.endLat.toFixed(2)},${s.endLon.toFixed(2)}`).join(' · '); + console.log(`${o}→${t}: provider=${r.provider} dist=${r.totalDistanceKm.toFixed(0)}km geom=${geom.length} pts, start=[${geom[0] && geom[0].map(n => n.toFixed(2)).join(',')}] end=[${geom[geom.length-1] && geom[geom.length-1].map(n => n.toFixed(2)).join(',')}]`); + console.log(` Segmente: ${segs}`); +} diff --git a/App/sims/logistik/sounds/music/alpine-freight-light-v3.mp3 b/App/sims/logistik/sounds/music/alpine-freight-light-v3.mp3 new file mode 100644 index 0000000..bbafc2d Binary files /dev/null and b/App/sims/logistik/sounds/music/alpine-freight-light-v3.mp3 differ diff --git a/App/sims/logistik/sounds/music/alpine-freight-light-v4.mp3 b/App/sims/logistik/sounds/music/alpine-freight-light-v4.mp3 new file mode 100644 index 0000000..f266f12 Binary files /dev/null and b/App/sims/logistik/sounds/music/alpine-freight-light-v4.mp3 differ diff --git a/App/sims/logistik/sounds/music/alpine-freightlight-v1.mp3 b/App/sims/logistik/sounds/music/alpine-freightlight-v1.mp3 new file mode 100644 index 0000000..d126f6d Binary files /dev/null and b/App/sims/logistik/sounds/music/alpine-freightlight-v1.mp3 differ diff --git a/App/sims/logistik/sounds/music/alpine-freightlight-v2.mp3 b/App/sims/logistik/sounds/music/alpine-freightlight-v2.mp3 new file mode 100644 index 0000000..6d9da50 Binary files /dev/null and b/App/sims/logistik/sounds/music/alpine-freightlight-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/busted-wagon-wheel.mp3 b/App/sims/logistik/sounds/music/busted-wagon-wheel.mp3 new file mode 100644 index 0000000..cf94c2a Binary files /dev/null and b/App/sims/logistik/sounds/music/busted-wagon-wheel.mp3 differ diff --git a/App/sims/logistik/sounds/music/dustlight-freight-v1.mp3 b/App/sims/logistik/sounds/music/dustlight-freight-v1.mp3 new file mode 100644 index 0000000..81176df Binary files /dev/null and b/App/sims/logistik/sounds/music/dustlight-freight-v1.mp3 differ diff --git a/App/sims/logistik/sounds/music/dustlight-freight-v2.mp3 b/App/sims/logistik/sounds/music/dustlight-freight-v2.mp3 new file mode 100644 index 0000000..07a43da Binary files /dev/null and b/App/sims/logistik/sounds/music/dustlight-freight-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/gears-and-marbles-v2.mp3 b/App/sims/logistik/sounds/music/gears-and-marbles-v2.mp3 new file mode 100644 index 0000000..77e7baf Binary files /dev/null and b/App/sims/logistik/sounds/music/gears-and-marbles-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/pressure-bloom-v1.mp3 b/App/sims/logistik/sounds/music/pressure-bloom-v1.mp3 new file mode 100644 index 0000000..162b024 Binary files /dev/null and b/App/sims/logistik/sounds/music/pressure-bloom-v1.mp3 differ diff --git a/App/sims/logistik/sounds/music/pressure-bloom-v2.mp3 b/App/sims/logistik/sounds/music/pressure-bloom-v2.mp3 new file mode 100644 index 0000000..f7e8b84 Binary files /dev/null and b/App/sims/logistik/sounds/music/pressure-bloom-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/rail-gravel-v1.mp3 b/App/sims/logistik/sounds/music/rail-gravel-v1.mp3 new file mode 100644 index 0000000..a981634 Binary files /dev/null and b/App/sims/logistik/sounds/music/rail-gravel-v1.mp3 differ diff --git a/App/sims/logistik/sounds/music/rail-gravel-v2.mp3 b/App/sims/logistik/sounds/music/rail-gravel-v2.mp3 new file mode 100644 index 0000000..0cebafb Binary files /dev/null and b/App/sims/logistik/sounds/music/rail-gravel-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/saddlewood-metronome-v2.mp3 b/App/sims/logistik/sounds/music/saddlewood-metronome-v2.mp3 new file mode 100644 index 0000000..5ce52a4 Binary files /dev/null and b/App/sims/logistik/sounds/music/saddlewood-metronome-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/station-glissando-v1.mp3 b/App/sims/logistik/sounds/music/station-glissando-v1.mp3 new file mode 100644 index 0000000..267b857 Binary files /dev/null and b/App/sims/logistik/sounds/music/station-glissando-v1.mp3 differ diff --git a/App/sims/logistik/sounds/music/station-glissando-v2.mp3 b/App/sims/logistik/sounds/music/station-glissando-v2.mp3 new file mode 100644 index 0000000..267b857 Binary files /dev/null and b/App/sims/logistik/sounds/music/station-glissando-v2.mp3 differ diff --git a/App/sims/logistik/sounds/music/station-glissando-v3.mp3 b/App/sims/logistik/sounds/music/station-glissando-v3.mp3 new file mode 100644 index 0000000..ba0ff47 Binary files /dev/null and b/App/sims/logistik/sounds/music/station-glissando-v3.mp3 differ diff --git a/App/src/sims/sim-05-treibhaus/game-renderer.ts b/App/src/sims/sim-05-treibhaus/game-renderer.ts index 5b40dbe..31bb39e 100644 --- a/App/src/sims/sim-05-treibhaus/game-renderer.ts +++ b/App/src/sims/sim-05-treibhaus/game-renderer.ts @@ -16,7 +16,7 @@ import { KlimawaechterGame } from './game' interface PlacedObject { - type: 'tree' | 'solar' | 'wind' | 'green-roof' | 'dike' | 'sea-wall' + type: 'tree' | 'solar' | 'wind' | 'green-roof' | 'dike' | 'sea-wall' | 'bikes' | 'mangrove' | 'sand-fill' | 'coal' | 'airport' | 'cloud-seed' x: number // 0..1 scale: number ownerId: string @@ -184,13 +184,14 @@ export class KlimawaechterRenderer { return { type: 'green-roof', x: 0, scale: 1, ownerId: stableId, builtTick: tick } } if (id === 'dike') { - const slot = index % 4 - const x = 0.83 + slot * 0.035 + // Deiche breiter entlang der Kueste verteilen + const x = 0.55 + index * 0.06 + this.seededRand(index * 41 + 9) * 0.02 return { type: 'dike', x, scale: 1, ownerId: stableId, builtTick: tick } } if (id === 'sea-wall') { - const x = 0.95 - return { type: 'sea-wall', x, scale: 1 + index * 0.08, ownerId: stableId, builtTick: tick } + // Mehrere Mauern entlang der Kueste verteilen + const x = 0.6 + index * 0.07 + this.seededRand(index * 29 + 3) * 0.03 + return { type: 'sea-wall', x, scale: 1, ownerId: stableId, builtTick: tick } } if (id === 'bikes') { return { type: 'bikes', x: 0.35 + index * 0.04, scale: 1, ownerId: stableId, builtTick: tick } @@ -200,8 +201,9 @@ export class KlimawaechterRenderer { return { type: 'mangrove', x, scale: baseScale, ownerId: stableId, builtTick: tick } } if (id === 'sand-fill') { - const x = 0.82 + index * 0.025 - return { type: 'sand-fill', x, scale: 1, ownerId: stableId, builtTick: tick } + // Entlang der ganzen Kuestenlinie verteilen + const x = 0.55 + index * 0.06 + this.seededRand(index * 37 + 5) * 0.03 + return { type: 'sand-fill', x, scale: 1 + this.seededRand(index * 19) * 0.3, ownerId: stableId, builtTick: tick } } if (id === 'coal') { return { type: 'coal', x: 0.12, scale: 1 + index * 0.05, ownerId: stableId, builtTick: tick } @@ -468,16 +470,22 @@ export class KlimawaechterRenderer { // ===== KÜSTENBAUWERKE ===== for (const obj of this.placed.filter(p => p.type === 'sand-fill')) { - // Sand-Aufschüttung: kleine Hügel an der Wasserlinie + // Sand-Aufschüttung: sichtbare Hügel an der Wasserlinie const sx = obj.x * W, sy = seaY + const sc = obj.scale ctx.fillStyle = '#d4c090' ctx.beginPath() - ctx.ellipse(sx, sy, 12, 5, 0, Math.PI, 0) + ctx.ellipse(sx, sy, 16 * sc, 7 * sc, 0, Math.PI, 0) ctx.fill() ctx.fillStyle = '#c8b480' ctx.beginPath() - ctx.ellipse(sx + 8, sy, 8, 3, 0, Math.PI, 0) + ctx.ellipse(sx + 10 * sc, sy, 10 * sc, 4 * sc, 0, Math.PI, 0) ctx.fill() + // Rand + ctx.strokeStyle = '#b8a470'; ctx.lineWidth = 0.5 + ctx.beginPath() + ctx.ellipse(sx, sy, 16 * sc, 7 * sc, 0, Math.PI, 0) + ctx.stroke() } for (const obj of this.placed.filter(p => p.type === 'mangrove')) { // Mangroven: grüne Büsche an der Uferlinie @@ -521,18 +529,20 @@ export class KlimawaechterRenderer { } // Flughafen for (const obj of this.placed.filter(p => p.type === 'airport')) { - const ax = obj.x * W, ay = groundY - 8 + const ax = obj.x * W, ay = groundY + 4 // weiter unten, nicht auf der Strasse // Landebahn - ctx.fillStyle = '#5a5a5a'; ctx.fillRect(ax - 18, ay, 36, 4) + ctx.fillStyle = '#5a5a5a'; ctx.fillRect(ax - 20, ay, 40, 5) ctx.strokeStyle = '#fff'; ctx.lineWidth = 0.5; ctx.setLineDash([3, 2]) - ctx.beginPath(); ctx.moveTo(ax - 16, ay + 2); ctx.lineTo(ax + 16, ay + 2); ctx.stroke() + ctx.beginPath(); ctx.moveTo(ax - 18, ay + 2.5); ctx.lineTo(ax + 18, ay + 2.5); ctx.stroke() ctx.setLineDash([]) - // Flugzeug - const flyX = ax + Math.sin(this.t * 0.3) * 40 - const flyY = ay - 15 - Math.abs(Math.sin(this.t * 0.3)) * 20 - ctx.fillStyle = '#eee'; ctx.globalAlpha = 0.7 - ctx.beginPath(); ctx.ellipse(flyX, flyY, 6, 1.5, Math.sin(this.t * 0.3) * 0.3, 0, Math.PI * 2); ctx.fill() - ctx.globalAlpha = 1 + // Helikopter auf der Landebahn (klein) + ctx.font = '8px serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle' + ctx.fillText('🚁', ax, ay + 2.5) + // Fliegender Helikopter oben + const flyX = ax + Math.sin(this.t * 0.3) * 50 + const flyY = ay - 25 - Math.abs(Math.sin(this.t * 0.3)) * 25 + ctx.font = '6px serif' + ctx.fillText('🚁', flyX, flyY) } // Wolkenimpfung for (const obj of this.placed.filter(p => p.type === 'cloud-seed')) { diff --git a/App/stadt.html b/App/stadt.html index 5a27499..913acf5 100644 --- a/App/stadt.html +++ b/App/stadt.html @@ -4,8 +4,8 @@ GeoGraSim — Stadt & Raumplanung - - + +