Atlas: Logistik-Gerüst + Heli-Fix + Waypoints-Regen + Level-Picker
- Neues Modul 'Logistik Europa': module_info-Eintrag, Landing-Card, PHP-Wrapper (logistik.php, modul-logistik.php), Engine-Skelett mit Enums + Helpern (travelCost, latePenalty, Bonus, Dijkstra, Polyline-Interpolation), Test-Harness, Kompetenzen-Draft, 5 Seed-Dateien (locations, vehicle-types, cargo-types, railnet, contract-templates), 3 Level-Einträge in game_levels, Phase 0 durch die Logistik-Instanz geliefert und Atlas-Review bestanden - Heli-Fix: heli-game.php mit BASE_PATH (production-sicher) + Asset-Pfad-Injection, geo_waypoints-Tabelle per Seed-Script auffindbar gemacht - Waypoints-Regen-Tool: regen-waypoints-sql.php synchronisiert waypoints.sql aus DB (70 Einträge) - Design-System: .ggs-level-grid / .ggs-level-card als Standard gepromotet (Klimas Muster, iPad-hover-safe) - Inbox-Nachrichten: Kickoff-Briefings für Heli und Logistik, Asset-Map für Heli, DALL-E-Pipeline-Anleitung, Koordinations- nachrichten an Fluss, Klima, Glossar, Lehrplan Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Logistik Europa — Engine (Skelett)
|
||||
* ------------------------------------------------------------------
|
||||
* Headless Simulationslogik ohne DOM, ohne Rendering. Die UI
|
||||
* (game.html, test.html) ruft Methoden auf und verarbeitet
|
||||
* zurückgegebene Events selbst.
|
||||
*
|
||||
* Exportiert als globales `window.LogistikEngine` (kein ES-Module,
|
||||
* kein Bundler).
|
||||
*
|
||||
* Dieses Skelett enthält:
|
||||
* - State-Machine-Enum (aus Pflichtenheft Kap 44)
|
||||
* - Enum-Konstanten für Fahrzeugmodi, Vertrags-/Fahrzeug-Zustände,
|
||||
* Ereignistypen, Hilfestufen (Kap 42)
|
||||
* - Platzhalter-Funktionen für createGame, tick, assignContract,
|
||||
* calculateRoute, useHint, applyMinigameResult
|
||||
* - Test-Hooks für die Regressionstests aus dem Kickoff-Briefing
|
||||
*
|
||||
* Phase 0 (Balance-Matrix + Test-Harness) soll dieses Skelett nutzen,
|
||||
* aber die Implementierung bleibt bewusst Stub, damit die Tests
|
||||
* zuerst das Contract testen (TDD-ähnlich).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ============================================================
|
||||
ENUMS (aus Pflichtenheft Kap 42, 44)
|
||||
============================================================ */
|
||||
|
||||
const STATE = Object.freeze({
|
||||
INIT: 'INIT',
|
||||
LOADING_CONTENT: 'LOADING_CONTENT',
|
||||
READY: 'READY',
|
||||
PLANNING: 'PLANNING',
|
||||
RUNNING: 'RUNNING',
|
||||
PAUSED_BY_EVENT: 'PAUSED_BY_EVENT',
|
||||
PAUSED_BY_ARRIVAL: 'PAUSED_BY_ARRIVAL',
|
||||
MINIGAME: 'MINIGAME',
|
||||
LEVEL_SUCCESS: 'LEVEL_SUCCESS',
|
||||
LEVEL_FAILED: 'LEVEL_FAILED',
|
||||
SAVING: 'SAVING',
|
||||
ERROR: 'ERROR',
|
||||
});
|
||||
|
||||
const VEHICLE_MODE = Object.freeze({
|
||||
TRUCK_SMALL: 'TRUCK_SMALL',
|
||||
TRUCK_LARGE: 'TRUCK_LARGE',
|
||||
TRAIN: 'TRAIN',
|
||||
});
|
||||
|
||||
const VEHICLE_STATE = Object.freeze({
|
||||
IDLE: 'IDLE',
|
||||
RESERVED: 'RESERVED',
|
||||
MOVING: 'MOVING',
|
||||
LOADING: 'LOADING',
|
||||
UNLOADING: 'UNLOADING',
|
||||
WAITING: 'WAITING',
|
||||
BLOCKED: 'BLOCKED',
|
||||
BROKEN: 'BROKEN',
|
||||
});
|
||||
|
||||
const CONTRACT_STATE = Object.freeze({
|
||||
OPEN: 'OPEN',
|
||||
RESERVED: 'RESERVED',
|
||||
ASSIGNED: 'ASSIGNED',
|
||||
PICKUP_PENDING: 'PICKUP_PENDING',
|
||||
LOADING: 'LOADING',
|
||||
IN_TRANSIT: 'IN_TRANSIT',
|
||||
AT_TRANSFER: 'AT_TRANSFER',
|
||||
DELIVERED: 'DELIVERED',
|
||||
LATE: 'LATE',
|
||||
FAILED: 'FAILED',
|
||||
CANCELLED: 'CANCELLED',
|
||||
});
|
||||
|
||||
const LOCATION_TYPE = Object.freeze({
|
||||
CITY: 'CITY',
|
||||
CAPITAL: 'CAPITAL',
|
||||
PORT: 'PORT',
|
||||
TERMINAL: 'TERMINAL',
|
||||
INDUSTRY: 'INDUSTRY',
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
HUB: 'HUB',
|
||||
});
|
||||
|
||||
const EVENT_TYPE = Object.freeze({
|
||||
TRAFFIC_ACCIDENT: 'TRAFFIC_ACCIDENT',
|
||||
SNOW: 'SNOW',
|
||||
STORM: 'STORM',
|
||||
PORT_DELAY: 'PORT_DELAY',
|
||||
RAIL_STRIKE: 'RAIL_STRIKE',
|
||||
SPECIAL_ORDER: 'SPECIAL_ORDER',
|
||||
MAINTENANCE: 'MAINTENANCE',
|
||||
});
|
||||
|
||||
const HINT_MODE = Object.freeze({
|
||||
BLINK_EXACT: 'BLINK_EXACT',
|
||||
SHOW_COUNTRY: 'SHOW_COUNTRY',
|
||||
SHOW_REGION: 'SHOW_REGION',
|
||||
DISTANCE_FEEDBACK: 'DISTANCE_FEEDBACK',
|
||||
NONE: 'NONE',
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
KONSTANTEN — Pflichtwerte aus Pflichtenheft Kap 65
|
||||
Diese sind Startwerte. Balance-Matrix in Phase 0 überschreibt
|
||||
sie ggf. pro Level.
|
||||
============================================================ */
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
const ECONOMY = Object.freeze({
|
||||
standCostContainerPerHour: 10, // Container im Hafen
|
||||
standCostVehicleIdlePerHour: 5,
|
||||
latePenaltyPercentPerHour: 0.2, // 20% des Auftragswerts
|
||||
punctualBonusPercent: 0.1, // +10%
|
||||
optimalBonusPercent: 0.2, // +20% (keine Leerfahrt)
|
||||
standardContractBase: 1000,
|
||||
expressContractBase: 1500,
|
||||
});
|
||||
|
||||
const EVENT_RULES = Object.freeze({
|
||||
TRAFFIC_ACCIDENT: { probabilityPerHour: 0.05, speedMult: 0.5 },
|
||||
SNOW: { probabilityPerHour: 0.10, speedMult: 0.7, regions: ['alps'] },
|
||||
PORT_DELAY: { probabilityPerHour: 0.08, loadMult: 1.5 },
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
FACTORIES
|
||||
============================================================ */
|
||||
|
||||
/**
|
||||
* Erzeugt einen neuen Spielzustand für das gegebene Level.
|
||||
* Merged Level-Params aus `window.LOGISTIK_LEVELS` mit Defaults.
|
||||
*
|
||||
* @param {number} levelNum
|
||||
* @returns {Object} gameState
|
||||
*/
|
||||
function createGame(levelNum) {
|
||||
const levelCfg = (window.LOGISTIK_LEVELS || []).find(l => l.level === levelNum)?.params || {};
|
||||
return {
|
||||
sessionId: _uuid(),
|
||||
levelId: 'level_' + levelNum,
|
||||
state: STATE.INIT,
|
||||
simulationTime: new Date().toISOString(),
|
||||
timeScale: levelCfg.initialTimeScale ?? 1,
|
||||
paused: true,
|
||||
balance: levelCfg.startBudget ?? 5000,
|
||||
score: 0,
|
||||
activeContracts: [],
|
||||
completedContracts: [],
|
||||
vehicles: [],
|
||||
activeRoutes: [],
|
||||
activeEvents: [],
|
||||
pendingNotifications: [],
|
||||
usedHints: [],
|
||||
analytics: {
|
||||
totalKm: 0,
|
||||
emptyKm: 0,
|
||||
lateDeliveries: 0,
|
||||
hintUsages: 0,
|
||||
misclicks: 0,
|
||||
},
|
||||
worldState: {},
|
||||
version: '0.1.0',
|
||||
_pendingEvents: [],
|
||||
_config: levelCfg,
|
||||
};
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PLACEHOLDER / STUB FUNKTIONEN
|
||||
(Logistik-Instanz implementiert diese in Phase 1–7)
|
||||
============================================================ */
|
||||
|
||||
/** @throws nicht implementiert — Phase 2 */
|
||||
function tick(game, deltaMs) {
|
||||
throw new Error('LogistikEngine.tick: nicht implementiert (Phase 2)');
|
||||
}
|
||||
|
||||
/** @throws nicht implementiert — Phase 2 */
|
||||
function assignContract(game, contractId, vehicleId) {
|
||||
throw new Error('LogistikEngine.assignContract: nicht implementiert (Phase 2)');
|
||||
}
|
||||
|
||||
/** @throws nicht implementiert — Phase 2/3 */
|
||||
function calculateRoute(game, originId, targetId, mode) {
|
||||
throw new Error('LogistikEngine.calculateRoute: nicht implementiert (Phase 2)');
|
||||
}
|
||||
|
||||
/** @throws nicht implementiert — Phase 5 */
|
||||
function useHint(game, contractId, hintMode) {
|
||||
throw new Error('LogistikEngine.useHint: nicht implementiert (Phase 5)');
|
||||
}
|
||||
|
||||
/** @throws nicht implementiert — Phase 6 */
|
||||
function applyMinigameResult(game, result) {
|
||||
throw new Error('LogistikEngine.applyMinigameResult: nicht implementiert (Phase 6)');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
REINE HELPER (können / sollen Phase 0 schon getestet werden)
|
||||
============================================================ */
|
||||
|
||||
/**
|
||||
* Fahrkosten für eine Strecke. Pflichttest #1 aus Kickoff-Briefing.
|
||||
* @param {number} distanceKm
|
||||
* @param {number} durationHours
|
||||
* @param {'TRUCK_SMALL'|'TRUCK_LARGE'|'TRAIN'} mode
|
||||
* @returns {number} Kosten in €
|
||||
*/
|
||||
function travelCost(distanceKm, durationHours, mode) {
|
||||
const v = VEHICLE_DEFAULTS[mode];
|
||||
if (!v) throw new Error('Unbekannter Fahrzeugmodus: ' + mode);
|
||||
return distanceKm * v.costPerKm + durationHours * v.costPerHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strafkosten bei Verspätung. Pflichttest #3.
|
||||
* @param {number} contractValue
|
||||
* @param {number} hoursLate
|
||||
* @returns {number} Strafe in €
|
||||
*/
|
||||
function latePenalty(contractValue, hoursLate) {
|
||||
return Math.max(0, contractValue * ECONOMY.latePenaltyPercentPerHour * hoursLate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus-Kombi: pünktlich + optimal. Pflichttest #4.
|
||||
* @param {number} contractValue
|
||||
* @param {{ onTime: boolean, noEmpty: boolean }} flags
|
||||
* @returns {number} Bonus in €
|
||||
*/
|
||||
function calculateBonus(contractValue, flags) {
|
||||
let bonus = 0;
|
||||
if (flags.onTime) bonus += contractValue * ECONOMY.punctualBonusPercent;
|
||||
if (flags.noEmpty) bonus += contractValue * ECONOMY.optimalBonusPercent;
|
||||
return bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolation entlang einer Polyline (lat/lon-Paare).
|
||||
* Pflichttest #2.
|
||||
* @param {[number,number][]} polyline
|
||||
* @param {number} progress 0..1
|
||||
* @returns {[number,number]} [lat, lon]
|
||||
*/
|
||||
function interpolateAlongPolyline(polyline, progress) {
|
||||
if (!polyline || polyline.length < 2) throw new Error('Polyline zu kurz');
|
||||
const p = Math.max(0, Math.min(1, progress));
|
||||
if (p === 0) return polyline[0];
|
||||
if (p === 1) return polyline[polyline.length - 1];
|
||||
const total = _polylineLength(polyline);
|
||||
const target = p * total;
|
||||
let acc = 0;
|
||||
for (let i = 0; i < polyline.length - 1; i++) {
|
||||
const d = _segmentLength(polyline[i], polyline[i + 1]);
|
||||
if (acc + d >= target) {
|
||||
const local = (target - acc) / d;
|
||||
const [lat1, lon1] = polyline[i];
|
||||
const [lat2, lon2] = polyline[i + 1];
|
||||
return [lat1 + (lat2 - lat1) * local, lon1 + (lon2 - lon1) * local];
|
||||
}
|
||||
acc += d;
|
||||
}
|
||||
return polyline[polyline.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dijkstra über Bahnnetz. Pflichttest #7.
|
||||
* Graph: { nodes:[{id}], edges:[{from,to,distanceKm}] } — ungerichtet.
|
||||
* @param {{nodes:Array, edges:Array}} graph
|
||||
* @param {string} startId
|
||||
* @param {string} endId
|
||||
* @returns {{path:string[], distanceKm:number}|null}
|
||||
*/
|
||||
function railShortestPath(graph, startId, endId) {
|
||||
const adj = new Map();
|
||||
graph.nodes.forEach(n => adj.set(n.id, []));
|
||||
graph.edges.forEach(e => {
|
||||
if (!adj.has(e.from) || !adj.has(e.to)) return;
|
||||
adj.get(e.from).push({ to: e.to, d: e.distanceKm });
|
||||
adj.get(e.to).push({ to: e.from, d: e.distanceKm });
|
||||
});
|
||||
const dist = new Map();
|
||||
const prev = new Map();
|
||||
const queue = new Set(graph.nodes.map(n => n.id));
|
||||
graph.nodes.forEach(n => dist.set(n.id, Infinity));
|
||||
dist.set(startId, 0);
|
||||
while (queue.size) {
|
||||
let u = null, best = Infinity;
|
||||
for (const id of queue) if (dist.get(id) < best) { best = dist.get(id); u = id; }
|
||||
if (u === null || u === endId) break;
|
||||
queue.delete(u);
|
||||
for (const { to, d } of adj.get(u) || []) {
|
||||
if (!queue.has(to)) continue;
|
||||
const alt = dist.get(u) + d;
|
||||
if (alt < dist.get(to)) {
|
||||
dist.set(to, alt);
|
||||
prev.set(to, u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isFinite(dist.get(endId))) return null;
|
||||
const path = [];
|
||||
let cur = endId;
|
||||
while (cur !== undefined) { path.unshift(cur); cur = prev.get(cur); }
|
||||
return { path, distanceKm: dist.get(endId) };
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PRIVATE
|
||||
============================================================ */
|
||||
|
||||
function _uuid() {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function _segmentLength(a, b) {
|
||||
const [lat1, lon1] = a;
|
||||
const [lat2, lon2] = b;
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s));
|
||||
}
|
||||
|
||||
function _polylineLength(poly) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < poly.length - 1; i++) sum += _segmentLength(poly[i], poly[i + 1]);
|
||||
return sum;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
EXPORT
|
||||
============================================================ */
|
||||
|
||||
window.LogistikEngine = {
|
||||
// Enums
|
||||
STATE, VEHICLE_MODE, VEHICLE_STATE, CONTRACT_STATE,
|
||||
LOCATION_TYPE, EVENT_TYPE, HINT_MODE,
|
||||
// Konstanten
|
||||
VEHICLE_DEFAULTS, ECONOMY, EVENT_RULES,
|
||||
// Factories
|
||||
createGame,
|
||||
// Stubs (Phase 1+)
|
||||
tick, assignContract, calculateRoute, useHint, applyMinigameResult,
|
||||
// Helper (Phase 0 testbar)
|
||||
travelCost, latePenalty, calculateBonus,
|
||||
interpolateAlongPolyline, railShortestPath,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user