1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
408 lines
20 KiB
PHP
408 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Heli Audio-Timeline API
|
|
*
|
|
* Endpunkte:
|
|
* GET /api/heli-timeline?mission=m1 → liefert Timeline, generiert Default falls keine Datei existiert
|
|
* POST /api/heli-timeline?mission=m1 → speichert Timeline-JSON (body) ins File-System, mit Backup
|
|
* GET /api/heli-timeline?missions=1 → Liste aller Missionen + Status (default/edited/saved)
|
|
*
|
|
* GET /api/heli-timeline?durations=1 → liefert audio-durations.json
|
|
* POST /api/heli-timeline?durations=1 → speichert durations (body: {key: seconds})
|
|
*
|
|
* GET /api/heli-timeline?audios=1 → liste alle verfuegbaren Audio-IDs + Texte aus audio-texts.json
|
|
*
|
|
* Datenpfade:
|
|
* App/sims/heli/data/missions.json
|
|
* App/sims/heli/data/audio-timeline-<mid>.json
|
|
* App/sims/heli/data/audio-timeline-<mid>.json.bak-YYYYMMDD-HHMMSS
|
|
* App/sims/heli/data/audio-durations.json
|
|
* App/sims/heli/data/briefings.json
|
|
* App/sims/heli/data/heli-stations.json
|
|
* App/sims/heli/scripts/audio-texts.json
|
|
*
|
|
* Konzept: docs/konzept-audio-schnitt-tool.md
|
|
*/
|
|
|
|
require_once __DIR__ . '/../config/app.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$dataDir = __DIR__ . '/../../sims/heli/data';
|
|
$scriptsDir = __DIR__ . '/../../sims/heli/scripts';
|
|
|
|
function jsonReadOr($path, $fallback) {
|
|
if (!file_exists($path)) return $fallback;
|
|
$j = json_decode(file_get_contents($path), true);
|
|
return is_array($j) ? $j : $fallback;
|
|
}
|
|
function jsonWrite($path, $data) {
|
|
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
return file_put_contents($path, $json) !== false;
|
|
}
|
|
function backupFile($path) {
|
|
if (!file_exists($path)) return;
|
|
$bak = $path . '.bak-' . date('Ymd-His');
|
|
@copy($path, $bak);
|
|
}
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
|
|
// === GET: Audios-Liste ===
|
|
if ($method === 'GET' && isset($_GET['audios'])) {
|
|
$texts = jsonReadOr($scriptsDir . '/audio-texts.json', []);
|
|
$durations = jsonReadOr($dataDir . '/audio-durations.json', ['durations' => []]);
|
|
$durMap = $durations['durations'] ?? [];
|
|
$out = [];
|
|
foreach ($texts as $id => $text) {
|
|
if (!is_string($text) || strpos($id, '_') === 0) continue;
|
|
$out[] = [
|
|
'id' => $id,
|
|
'text' => $text,
|
|
'dur' => $durMap[$id] ?? null,
|
|
];
|
|
}
|
|
echo json_encode(['audios' => $out], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// === GET/POST: Audio-Dauern-Cache ===
|
|
if (isset($_GET['durations'])) {
|
|
$path = $dataDir . '/audio-durations.json';
|
|
if ($method === 'GET') {
|
|
$existing = jsonReadOr($path, ['_doc' => 'Audio-Dauern in Sekunden (clientseitig gemessen)', 'durations' => []]);
|
|
echo json_encode($existing, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
if ($method === 'POST') {
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
if (!is_array($body)) { http_response_code(400); echo json_encode(['error' => 'invalid body']); exit; }
|
|
$existing = jsonReadOr($path, ['durations' => []]);
|
|
$existing['_doc'] = 'Audio-Dauern in Sekunden (clientseitig gemessen)';
|
|
$existing['lastUpdated'] = date('c');
|
|
$existing['durations'] = array_merge($existing['durations'] ?? [], $body['durations'] ?? []);
|
|
jsonWrite($path, $existing);
|
|
echo json_encode(['ok' => true, 'count' => count($existing['durations'])]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// === GET: Missionen-Liste mit Status ===
|
|
if ($method === 'GET' && isset($_GET['missions'])) {
|
|
$missions = jsonReadOr($dataDir . '/missions.json', ['missions' => []]);
|
|
$out = [];
|
|
foreach (($missions['missions'] ?? []) as $m) {
|
|
$tlPath = $dataDir . '/audio-timeline-' . $m['id'] . '.json';
|
|
$status = 'default';
|
|
$lastSaved = null;
|
|
if (file_exists($tlPath)) {
|
|
$tl = jsonReadOr($tlPath, []);
|
|
$status = !empty($tl['edited']) ? 'edited' : 'saved';
|
|
$lastSaved = $tl['lastSaved'] ?? null;
|
|
}
|
|
$out[] = array_merge($m, ['status' => $status, 'lastSaved' => $lastSaved]);
|
|
}
|
|
echo json_encode(['missions' => $out], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
// === GET/POST: Timeline pro Mission ===
|
|
$missionId = $_GET['mission'] ?? '';
|
|
if (!preg_match('/^m[1-9][0-9]?$/', $missionId)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'invalid mission id']);
|
|
exit;
|
|
}
|
|
|
|
$tlPath = $dataDir . '/audio-timeline-' . $missionId . '.json';
|
|
|
|
if ($method === 'GET') {
|
|
$forceDefault = !empty($_GET['forceDefault']);
|
|
if (!$forceDefault && file_exists($tlPath)) {
|
|
// existierende Timeline laden + zurueckgeben
|
|
$tl = jsonReadOr($tlPath, null);
|
|
if ($tl) { echo json_encode($tl, JSON_UNESCAPED_UNICODE); exit; }
|
|
}
|
|
// Default-Timeline generieren
|
|
$tl = generateDefaultTimeline($missionId, $dataDir, $scriptsDir);
|
|
echo json_encode($tl, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
if ($method === 'POST') {
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
if (!is_array($body)) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'invalid body']);
|
|
exit;
|
|
}
|
|
// Backup + Save
|
|
backupFile($tlPath);
|
|
$body['missionId'] = $missionId;
|
|
$body['lastSaved'] = date('c');
|
|
$body['edited'] = true;
|
|
if (!isset($body['version'])) $body['version'] = 1;
|
|
if (jsonWrite($tlPath, $body)) {
|
|
echo json_encode(['ok' => true, 'lastSaved' => $body['lastSaved']]);
|
|
} else {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'write failed']);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
http_response_code(405);
|
|
echo json_encode(['error' => 'method not allowed']);
|
|
|
|
|
|
// ===================================================================
|
|
// Default-Timeline-Generator
|
|
// Erzeugt eine vollstaendige Vorbefuellung pro Mission aus:
|
|
// - missions.json (Mission-Daten)
|
|
// - audio-texts.json (welche Audios existieren)
|
|
// - audio-durations.json (echte Dauern, sonst Schaetzung)
|
|
// - briefings.json (Wetter-Flag pro Mission)
|
|
// - audio-drehbuch-regelwerk.md (Reihenfolge — hier in Code)
|
|
// ===================================================================
|
|
function generateDefaultTimeline($mid, $dataDir, $scriptsDir) {
|
|
$missionsJ = jsonReadOr($dataDir . '/missions.json', ['missions' => []]);
|
|
$mission = null;
|
|
foreach ($missionsJ['missions'] as $m) {
|
|
if ($m['id'] === $mid) { $mission = $m; break; }
|
|
}
|
|
if (!$mission) {
|
|
return ['error' => 'mission not found', 'missionId' => $mid];
|
|
}
|
|
$texts = jsonReadOr($scriptsDir . '/audio-texts.json', []);
|
|
$durJ = jsonReadOr($dataDir . '/audio-durations.json', ['durations' => []]);
|
|
$dur = $durJ['durations'] ?? [];
|
|
$brief = jsonReadOr($dataDir . '/briefings.json', ['missions' => []]);
|
|
$hasWetter = !empty($brief['missions'][$mid]['wetter']);
|
|
|
|
$gap = 0.5;
|
|
|
|
// Hilfsfunktion: Audio-Slot anhaengen
|
|
$addAudio = function(&$phase, $audioId, &$t, $note = '') use ($texts, $dur, $gap) {
|
|
if (!isset($texts[$audioId])) return; // existiert nicht → ueberspringen
|
|
$d = $dur[$audioId] ?? estimateDur($texts[$audioId]);
|
|
$phase['audios'][] = [
|
|
'id' => 'a' . count($phase['audios']),
|
|
'audioId' => $audioId,
|
|
't' => round($t, 2),
|
|
'dur' => round($d, 2),
|
|
'note' => $note,
|
|
];
|
|
$t += $d + $gap;
|
|
};
|
|
|
|
// Hilfsfunktion: Event-Marker
|
|
$addEvent = function(&$phase, $label, $t, $extra = []) {
|
|
$phase['events'][] = array_merge([
|
|
'id' => 'ev' . count($phase['events']),
|
|
't' => round($t, 2),
|
|
'label' => $label,
|
|
], $extra);
|
|
};
|
|
|
|
// === PLAN-PHASE ===
|
|
$plan = ['id' => 'plan', 'label' => 'Routenplanung', 'color' => '#4a7c8a', 'audios' => [], 'events' => []];
|
|
$t = 0;
|
|
$addEvent($plan, 'Plan-Start', $t);
|
|
$addAudio($plan, 'r_mission_intro_' . $mid, $t, 'Mission-Intro (Tower)');
|
|
|
|
// Pro Wegpunkt ab Index 1 (Index 0 = Stuetzpunkt). V3-Konzept (2026-06-01):
|
|
// r_nav_plan_correct ist KEIN Sprach-Audio mehr in der Pipe, sondern ein
|
|
// UI-Klick-Sound (click-ok.mp3), den engine.notifyClick(wp, true) parallel
|
|
// sofort beim User-Klick abspielt. Nur r_wp_geo_<wp> ist in der Sprach-Pipe.
|
|
$wps = $mission['waypoints'];
|
|
for ($i = 1; $i < count($wps); $i++) {
|
|
$wpKey = $wps[$i];
|
|
$isLast = $i === count($wps) - 1;
|
|
// Event "sichtbar": Audio-START → Wegpunkt-Marker enthuellt sich
|
|
$addEvent($plan, ($isLast ? 'Ziel' : 'Wegpunkt') . ' sichtbar: ' . $wpKey, $t, ['waypoint' => $wpKey]);
|
|
// Audio: Geo-Info (Sprach-Pipe)
|
|
$addAudio($plan, 'r_wp_geo_' . $wpKey, $t, ($isLast ? 'Ziel-Hinweis' : 'Wegpunkt-Hinweis'));
|
|
// Event "erreicht": Audio-ENDE → Spieler wird zum Klicken erwartet
|
|
$addEvent($plan, ($isLast ? 'Ziel' : 'Wegpunkt') . ' erreicht: ' . $wpKey, $t, ['waypoint' => $wpKey, 'click' => true]);
|
|
}
|
|
// Plan-Ende-Sprache (am Ende der Sprach-Pipe)
|
|
$addAudio($plan, 'r_nav_plan_done', $t, 'Plan fertig');
|
|
$addEvent($plan, 'Plan-Ende', $t);
|
|
|
|
// === STARTFLUG-PHASE (start.html iframe) ===
|
|
// Pilot-Hinweise sind szenario-spezifisch (start.html Builder waehlen pro scen
|
|
// ein eigenes radio.messages Array). Wir spiegeln das hier.
|
|
$hk = $mission['heliKey'];
|
|
$scen = $mission['scen'] ?? 'berg-fels';
|
|
$startflug = ['id' => 'startflug', 'label' => 'Startflug', 'color' => '#c97a3f', 'audios' => [], 'events' => []];
|
|
$t = 0;
|
|
$addEvent($startflug, 'Startflug-Beginn (Szenario: ' . $scen . ')', $t);
|
|
$addAudio($startflug, 'r_tower_start_' . $hk, $t, 'Tower: Startplatz frei');
|
|
$addAudio($startflug, 'r_mission_start_' . $mid, $t, 'Tower: Mission-Freigabe');
|
|
|
|
// Pilot-Wetter-Kommentare (immer, Szenario-unabhaengig)
|
|
$addEvent($startflug, 'Pool: Pilot-Wetter (immer)', $t);
|
|
foreach (['r_pilot_calm1','r_pilot_calm2','r_pilot_calm3'] as $aid) $addAudio($startflug, $aid, $t, 'Wetter-OK');
|
|
|
|
// Szenario-spezifische Pre-Warn (Hindernis-Hinweise)
|
|
$startflugScenAudios = [
|
|
'berg-schnee' => ['r_pilot_trees', 'r_pilot_flock', 'r_pilot_wind', 'r_pilot_narrow', 'r_pilot_fog1', 'r_pilot_fog2', 'r_pilot_fog3', 'r_warn_nebel'],
|
|
'berg-fels' => ['r_pilot_trees', 'r_pilot_flock', 'r_pilot_wind', 'r_pilot_narrow', 'r_pilot_fog1', 'r_pilot_fog2', 'r_warn_nebel'],
|
|
'schlucht' => ['r_pilot_narrow', 'r_pilot_gusts', 'r_pilot_wind', 'r_pilot_flock', 'r_pilot_trees'],
|
|
'wasser' => ['r_pilot_flock', 'r_pilot_gusts', 'r_pilot_wind', 'r_pilot_fog1', 'r_pilot_fog2', 'r_warn_nebel'],
|
|
'autobahn' => ['r_warn_kran', 'r_warn_antenne', 'r_warn_strom', 'r_warn_schornstein', 'r_warn_fabrik', 'r_pilot_gap', 'r_pilot_gusts', 'r_pilot_time'],
|
|
'tal-dorf' => ['r_warn_kran', 'r_warn_antenne', 'r_pilot_gap', 'r_pilot_trees', 'r_pilot_flock', 'r_pilot_wind', 'r_pilot_fog1'],
|
|
];
|
|
$addEvent($startflug, 'Pool: Pilot-Strecken-Hinweise (szenario-spezifisch)', $t);
|
|
foreach (($startflugScenAudios[$scen] ?? $startflugScenAudios['berg-fels']) as $aid) {
|
|
$addAudio($startflug, $aid, $t, 'Strecke ' . $scen);
|
|
}
|
|
|
|
// Proximity-Warns (immer — Spieler-Fehler unabhaengig von Szenario)
|
|
$addEvent($startflug, 'Pool: Proximity-Warns (immer)', $t);
|
|
foreach (['r_pilot_bird_ahead','r_pilot_plane_ahead','r_pilot_sportplane_level','r_pilot_storm_detour','r_pilot_fog_thickens','r_pilot_wind_turn','r_warn_gewitter','r_warn_fuel'] as $aid) {
|
|
$addAudio($startflug, $aid, $t, 'Proximity');
|
|
}
|
|
|
|
// Crash-Reaktionen — gefiltert nach Szenario (was kann ueberhaupt getroffen werden)
|
|
$crashScenAudios = [
|
|
'berg-schnee' => ['r_warn_vogel', 'r_warn_baum', 'r_warn_blitz', 'r_warn_flugzeug', 'r_warn_sport'],
|
|
'berg-fels' => ['r_warn_vogel', 'r_warn_baum', 'r_warn_blitz', 'r_warn_flugzeug', 'r_warn_sport'],
|
|
'schlucht' => ['r_warn_vogel', 'r_warn_baum', 'r_warn_blitz', 'r_warn_flugzeug'],
|
|
'wasser' => ['r_warn_vogel', 'r_warn_flugzeug', 'r_warn_sport', 'r_warn_blitz'],
|
|
'autobahn' => ['r_warn_haus', 'r_warn_klima', 'r_warn_vogel', 'r_warn_flugzeug', 'r_warn_blitz'],
|
|
'tal-dorf' => ['r_warn_haus', 'r_warn_baum', 'r_warn_vogel', 'r_warn_klima', 'r_warn_flugzeug'],
|
|
];
|
|
$addEvent($startflug, 'Crash-Reaktionen (szenario-spezifisch)', $t);
|
|
foreach (($crashScenAudios[$scen] ?? $crashScenAudios['berg-fels']) as $aid) {
|
|
$addAudio($startflug, $aid, $t, 'Crash');
|
|
}
|
|
|
|
// Strikes (Pilot-Schreck + Tower-Reaktion — immer)
|
|
$addEvent($startflug, 'Pool: Strikes (immer)', $t);
|
|
foreach (['r_pilot_strike_1','r_pilot_strike_2','r_pilot_strike_3','r_pilot_strike_4','r_pilot_strike_5'] as $aid) $addAudio($startflug, $aid, $t, 'Pilot-Schreck');
|
|
$addAudio($startflug, 'r_pilot_three_strikes', $t, '3 Strikes erreicht');
|
|
$addAudio($startflug, 'r_tower_strike_' . $hk, $t, 'Tower nach Strike');
|
|
$addAudio($startflug, 'r_tower_handover_' . $hk, $t, 'Tower: Chefpilotin uebernimmt');
|
|
foreach (['r_pilot_too_high','r_pilot_time_up'] as $aid) $addAudio($startflug, $aid, $t, 'Fehler-Hinweis');
|
|
|
|
// Tower-generic (selten, immer)
|
|
$addEvent($startflug, 'Pool: Tower-generic', $t);
|
|
foreach (['r_tower1','r_tower2','r_tower3','r_tower4'] as $aid) $addAudio($startflug, $aid, $t, 'generic Tower');
|
|
|
|
$addAudio($startflug, 'r_start_erfolg', $t, 'Tower: Start erfolgreich');
|
|
$addEvent($startflug, 'Startflug-Ende', $t);
|
|
|
|
// === KARTENFLUG-PHASE (game.html flight-Phase) ===
|
|
// r_mission_geo_<mid> bewusst NICHT mehr im Kartenflug (war Doppelung zu
|
|
// r_tower_geo_<mid> in Bergung). Beide hatten denselben Geo-Lehrtext, nur
|
|
// einmal als Briefing, einmal als Tower-Roger. Spart 1 Slot pro Mission.
|
|
$flug = ['id' => 'flug', 'label' => 'Kartenflug', 'color' => '#5a8a5e', 'audios' => [], 'events' => []];
|
|
$t = 0;
|
|
$addEvent($flug, 'Flug-Start', $t);
|
|
$addAudio($flug, 'r_nav_heli_intro_' . $hk, $t, 'Crew-Vorstellung');
|
|
$addAudio($flug, 'r_nav_fuel_half', $t, 'Treibstoff-Halbzeit');
|
|
if ($hasWetter) {
|
|
$addAudio($flug, 'r_nav_weather_enter', $t, 'Wetter-Eintritt');
|
|
}
|
|
// r_nav_altitude_* ans ENDE verlegt (2026-06-02 nach Thomas): didaktisch beim
|
|
// Zielanflug sinnvoller — Spieler kommt grad in die Berge / ans Tal.
|
|
$isMountain = in_array($mission['scen'] ?? '', ['berg-schnee', 'berg-fels', 'schlucht']);
|
|
$addAudio($flug, $isMountain ? 'r_nav_altitude_mountain' : 'r_nav_altitude_valley', $t, 'Hoehen-Info am Zielanflug');
|
|
// r_im_zielgebiet bewusst NICHT mehr im Kartenflug (war zeitbasiert nach
|
|
// Stack, kam bei langen Routen viel zu frueh — z.B. bei M4 ueber Oetz).
|
|
// landing.html spielt es beim hover_drop wenn Heli physisch unter dem Patient steht.
|
|
$addEvent($flug, 'Flug-Ende (Anflug)', $t);
|
|
|
|
// === BERGUNG-PHASE (landing.html iframe) ===
|
|
// Drei Bergungs-Modi:
|
|
// - HIGHWAY (hw): Heli LANDET auf gesperrter Fahrbahn → KEIN Winch, Pilot steigt aus
|
|
// - WASSER (sea): Heli schwebt, Rettungsschwimmer/Winch, Patient ueber Wasser
|
|
// - BERG (default): Heli schwebt, Flugretter abgeseilt, Winch
|
|
$isHw = $scen === 'autobahn';
|
|
$isSea = $scen === 'wasser';
|
|
$modeLabel = $isHw ? 'HIGHWAY-Landung' : ($isSea ? 'WASSER-Schwebeflug' : 'BERG-Winch');
|
|
// r_caution_* Audios verwenden Underscore (berg_fels), missions.json hat Bindestrich (berg-fels)
|
|
$scenU = str_replace('-', '_', $scen);
|
|
|
|
$bergung = ['id' => 'bergung', 'label' => 'Bergungsflug', 'color' => '#a25cc8', 'audios' => [], 'events' => []];
|
|
$t = 0;
|
|
$addEvent($bergung, 'Bergung-Beginn (' . $modeLabel . ')', $t);
|
|
$addAudio($bergung, 'r_pilot_call_' . $hk, $t, 'Pilot: Einsatzort erreicht');
|
|
$addAudio($bergung, 'r_tower_geo_' . $mid, $t, 'Tower: Roger + Region-Info');
|
|
|
|
if ($isHw) {
|
|
// HIGHWAY: Heli landet, kein Winch
|
|
$addEvent($bergung, 'Tower-Anflug (Highway)', $t);
|
|
$addAudio($bergung, 'r_tower_hw_approach_' . $hk, $t, 'Tower-HW: Sperrzone erreicht');
|
|
$addEvent($bergung, 'Landezone-Checks (Sequenz)', $t);
|
|
$addAudio($bergung, 'r_pilot_hw_zone_check', $t, '1. Pilotin prueft Landezone');
|
|
$addAudio($bergung, 'r_pilot_hw_zone_blocked', $t, '2a. Falls Zone blockiert');
|
|
$addAudio($bergung, 'r_pilot_hw_zone_clear', $t, '2b. Falls Zone frei → absinken');
|
|
$addAudio($bergung, 'r_land_approach', $t, '3. Anflug auf Fahrbahn');
|
|
$addAudio($bergung, 'r_pilot_hw_ground_hold', $t, '4. Pilotin am Boden — wartet auf Patient');
|
|
$addAudio($bergung, 'r_patient_aboard', $t, '5. Patient an Bord');
|
|
$addEvent($bergung, 'Klinik-Freigabe + Wieder-Start', $t);
|
|
$addAudio($bergung, 'r_tower_hw_extract_' . $hk, $t, '6. Tower-HW: Strecke frei');
|
|
$addAudio($bergung, 'r_pilot_hw_clinic_lift', $t, '7. Pilotin: Klinik-Lift');
|
|
} elseif ($isSea) {
|
|
// WASSER: Schwebeflug, Rettungsschwimmer/Winch
|
|
$addEvent($bergung, 'Tower-Anflug (Wasser)', $t);
|
|
$addAudio($bergung, 'r_tower_sea_approach_' . $hk, $t, 'Tower-See: Wasserlage');
|
|
$addAudio($bergung, 'r_caution_wasser', $t, 'Sicherheit: Wasser');
|
|
$addEvent($bergung, 'Wasser-Bergung (Sequenz Schritt 1-6)', $t);
|
|
$addAudio($bergung, 'r_land_approach', $t, '1. Anflug zum Patienten');
|
|
$addAudio($bergung, 'r_land_winch_down', $t, '2. Winde fährt aus, Retter abgeseilt');
|
|
$addAudio($bergung, 'r_land_rescuer_down', $t, '3. Retter ist im Wasser');
|
|
$addAudio($bergung, 'r_warteschleife', $t, '4. Heli in Warteschleife/Zwischenland.');
|
|
$addAudio($bergung, 'r_land_rescuer_ready', $t, '5. Retter signalisiert bereit');
|
|
$addAudio($bergung, 'r_land_hook_connected', $t, '6. Haken eingeklinkt');
|
|
$addAudio($bergung, 'r_land_lift_success', $t, '7. Patient gehoben');
|
|
$addAudio($bergung, 'r_patient_aboard', $t, '8. Patient an Bord');
|
|
$addEvent($bergung, 'Klinik-Freigabe', $t);
|
|
$addAudio($bergung, 'r_tower_sea_extract_' . $hk, $t, '9. Tower-See: Klinik freigegeben');
|
|
} else {
|
|
// BERG (default): Schwebeflug + Winch + Flugretter abgeseilt
|
|
$addEvent($bergung, 'Tower-Anflug (Berg)', $t);
|
|
$addAudio($bergung, 'r_tower_approach_' . $hk, $t, 'Tower: Zielanflug freigegeben');
|
|
$addAudio($bergung, 'r_caution_' . $scenU, $t, 'Sicherheit: ' . $scen);
|
|
$addEvent($bergung, 'Berg-Winch-Bergung (Sequenz Schritt 1-8)', $t);
|
|
$addAudio($bergung, 'r_land_approach', $t, '1. Schwebeflug zum Patienten');
|
|
$addAudio($bergung, 'r_land_winch_down', $t, '2. Winde fährt aus, Retter abgeseilt');
|
|
$addAudio($bergung, 'r_retter_intermediate', $t, '3. Retter unten, Versorgung lauft');
|
|
$addAudio($bergung, 'r_warteschleife', $t, '4. Heli kreist / landet zwischen');
|
|
$addAudio($bergung, 'r_land_rescuer_ready', $t, '5. Retter signalisiert bereit');
|
|
$addAudio($bergung, 'r_land_hook_connected', $t, '6. Haken eingeklinkt');
|
|
$addAudio($bergung, 'r_land_lift_success', $t, '7. Patient gehoben');
|
|
$addAudio($bergung, 'r_patient_aboard', $t, '8. Patient an Bord');
|
|
$addEvent($bergung, 'Klinik-Freigabe', $t);
|
|
$addAudio($bergung, 'r_tower_extract_' . $hk, $t, '9. Tower: Direktfreigabe Klinik');
|
|
}
|
|
|
|
// Klinik-Anflug + Ende (alle Modi)
|
|
$addEvent($bergung, 'Klinik-Anflug + Ende', $t);
|
|
$addAudio($bergung, 'r_kurs_klinik', $t, 'Pilot: Kurs Klinik');
|
|
$addAudio($bergung, 'r_land_clinic_ahead', $t, 'Land: Klinik voraus');
|
|
$addAudio($bergung, 'r_land_disembark', $t, 'Land: Patient uebergeben');
|
|
$addAudio($bergung, 'r_mission_success', $t, 'Mission erfolgreich');
|
|
$addAudio($bergung, 'r_mission_abort', $t, 'Mission abgebrochen (Alternative)');
|
|
$addEvent($bergung, 'Bergung-Ende', $t);
|
|
|
|
return [
|
|
'_doc' => 'Default-Timeline (Atlas-generiert). 4 Phasen: plan/startflug/flug/bergung. Tool ueberschreibt beim ersten Save.',
|
|
'missionId' => $mid,
|
|
'version' => 2,
|
|
'gap' => $gap,
|
|
'edited' => false,
|
|
'lastSaved' => null,
|
|
'phases' => [$plan, $startflug, $flug, $bergung],
|
|
];
|
|
}
|
|
|
|
// TTS-Sprechdauer-Schaetzung aus Text (Fallback wenn keine echte Dauer da)
|
|
function estimateDur($text) {
|
|
if (!$text) return 3;
|
|
$sec = mb_strlen($text) / 12.0 + 1.5;
|
|
return max(3, min(25, round($sec, 1)));
|
|
}
|