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>
298 lines
16 KiB
HTML
298 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Heli — Audio-Übersicht aller Missionen</title>
|
|
<style>
|
|
:root{
|
|
--cream:#f5efe0; --forest:#2d5434; --sage:#7a9b76;
|
|
--orange:#c97a3f; --red:#c85c4a; --slate:#3d5165; --ink:#1f2a24;
|
|
--border:rgba(45,84,52,.18);
|
|
}
|
|
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
|
background:var(--cream);color:var(--ink);margin:0;padding:0;line-height:1.5}
|
|
header{background:var(--forest);color:var(--cream);padding:16px 24px;position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
header h1{margin:0;font-size:18px;font-weight:700}
|
|
header nav{display:flex;gap:4px;flex-wrap:wrap;margin-left:auto}
|
|
header nav a{color:var(--cream);text-decoration:none;background:rgba(255,255,255,.08);padding:3px 8px;border-radius:4px;font-size:11px;font-weight:600}
|
|
header nav a:hover{background:rgba(255,255,255,.2)}
|
|
.container{max-width:1240px;margin:0 auto;padding:24px}
|
|
.mission{background:#fff;border:1px solid var(--border);border-radius:12px;padding:18px 24px;margin-bottom:20px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
|
|
.mission h2{margin:0 0 6px;color:var(--forest);font-size:18px}
|
|
.meta{color:var(--slate);font-size:13px;margin-bottom:8px}
|
|
.route{background:rgba(45,84,52,.05);border-left:3px solid var(--forest);padding:6px 10px;font-size:12px;color:var(--slate);margin-bottom:14px;border-radius:4px}
|
|
.route b{color:var(--forest)}
|
|
.audios{display:grid;gap:6px}
|
|
.phase-header{font-weight:700;color:var(--orange);font-size:12px;text-transform:uppercase;letter-spacing:.05em;margin:10px 0 4px;padding-bottom:4px;border-bottom:1px solid var(--border)}
|
|
.audio{display:grid;grid-template-columns:70px 180px 220px 1fr;gap:10px;align-items:center;padding:4px 8px;border-radius:4px;font-size:12px}
|
|
.audio:hover{background:rgba(45,84,52,.03)}
|
|
.audio .tag{font-family:"SF Mono","Consolas",monospace;font-weight:700;color:var(--orange);font-size:11px;background:rgba(201,122,63,.1);padding:2px 6px;border-radius:3px;text-align:center;letter-spacing:.02em;cursor:pointer;user-select:all}
|
|
.audio .tag:hover{background:rgba(201,122,63,.2)}
|
|
.audio .label{font-weight:600;color:var(--slate)}
|
|
.audio audio{width:100%;height:28px}
|
|
.audio .text{color:var(--ink);font-style:italic}
|
|
.audio .missing{color:var(--red);font-size:11px}
|
|
.audio.group-random{grid-template-columns:70px 180px 1fr}
|
|
.random-list{display:grid;gap:4px;margin-left:10px}
|
|
.random-list .audio{grid-template-columns:70px 220px 1fr}
|
|
@media (max-width:760px){
|
|
.audio{grid-template-columns:1fr;gap:4px}
|
|
.audio audio{width:100%}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>🚁 Heli · Audio-Übersicht aller Missionen</h1>
|
|
<nav id="nav"></nav>
|
|
</header>
|
|
<div class="container" id="content">
|
|
<p style="color:var(--slate)">Lade Texte …</p>
|
|
</div>
|
|
|
|
<script>
|
|
// --- Alle 25 Missionen (aus game.html kopiert) ---
|
|
const BASE_CONFIG = {
|
|
nenzing: {heli:'oamtc', heliKey:'c8', heliName:'Nenzing OE-XEN'},
|
|
hohenems: {heli:'ara', heliKey:'ara', heliName:'ARA Flugrettung RK-1'},
|
|
innsbruck: {heli:'oamtc', heliKey:'c1', heliName:'Innsbruck OE-XVB'},
|
|
hall: {heli:'oamtc', heliKey:'c1', heliName:'Innsbruck OE-XVB'},
|
|
zams_c5: {heli:'oamtc', heliKey:'c5', heliName:'Zams OE-XEZ'},
|
|
kitzbuehel: {heli:'oamtc', heliKey:'c4', heliName:'Kitzbühel OE-XEY'},
|
|
lienz: {heli:'oamtc', heliKey:'c7', heliName:'Lienz OE-XER'},
|
|
krems_c2: {heli:'oamtc', heliKey:'c2', heliName:'Krems OE-XVI'},
|
|
wr_neustadt_c3: {heli:'oamtc', heliKey:'c3', heliName:'Wr. Neustadt OE-XEL'},
|
|
salzburg_c6: {heli:'oamtc', heliKey:'c6', heliName:'Salzburg OE-XVL'},
|
|
wien_c9: {heli:'oamtc', heliKey:'c9', heliName:'Wien-Baumgasse OE-XVO'},
|
|
linz_c10: {heli:'oamtc', heliKey:'c10', heliName:'Linz OE-XVP'},
|
|
klagenfurt_c11: {heli:'oamtc', heliKey:'c11', heliName:'Klagenfurt OE-XEK'},
|
|
graz_c12: {heli:'oamtc', heliKey:'c12', heliName:'Graz OE-XEM'},
|
|
niederoeblarn_c14:{heli:'oamtc', heliKey:'c14', heliName:'Niederöblarn OE-XEW'},
|
|
oberwart_c16: {heli:'oamtc', heliKey:'c16', heliName:'Pinkafeld OE-XEU'},
|
|
};
|
|
|
|
const ALL_MISSIONS = [
|
|
{id:'m1', title:'Skiunfall Damüls', difficulty:'medium', base:'nenzing', region:'Vorarlberg', scen:'berg-schnee',
|
|
waypoints:['nenzing','thueringen','zwischenwasser','laterns','damuels','bergrettung_muels']},
|
|
{id:'m2', title:'Verkehrsunfall A14', difficulty:'easy', base:'hohenems', region:'Vorarlberg', scen:'autobahn',
|
|
waypoints:['hohenems','goetzis','rankweil','verkehr_a14']},
|
|
{id:'m3', title:'Bergrettung Brand', difficulty:'medium', base:'nenzing', region:'Vorarlberg', scen:'berg-fels',
|
|
waypoints:['nenzing','buers','brand']},
|
|
{id:'m4', title:'Skiunfall Sölden', difficulty:'medium', base:'innsbruck', region:'Tirol', scen:'berg-schnee',
|
|
waypoints:['innsbruck','telfs','oetz','soelden','skiunfall_soelden']},
|
|
{id:'m5', title:'Seerettung Bregenz', difficulty:'easy', base:'hohenems', region:'Vorarlberg', scen:'wasser',
|
|
waypoints:['hohenems','dornbirn','bregenz']},
|
|
{id:'m6', title:'Canyoning Mellau', difficulty:'medium', base:'hohenems', region:'Vorarlberg', scen:'schlucht',
|
|
waypoints:['hohenems','dornbirn','egg','bezau','mellau']},
|
|
{id:'m7', title:'Wanderunfall Kufstein', difficulty:'easy', base:'innsbruck', region:'Tirol', scen:'berg-fels',
|
|
waypoints:['innsbruck','hall','schwaz','jenbach','woergl','kufstein']},
|
|
{id:'m8', title:'Notfall Gaschurn', difficulty:'hard', base:'nenzing', region:'Vorarlberg', scen:'tal-dorf',
|
|
waypoints:['nenzing','bludenz','schruns','stgallenk','gaschurn']},
|
|
{id:'m9', title:'Lawineneinsatz Mayrhofen', difficulty:'hard', base:'innsbruck', region:'Tirol', scen:'berg-schnee',
|
|
waypoints:['innsbruck','hall','schwaz','jenbach','zell_ziller','mayrhofen']},
|
|
{id:'m10', title:'Verkehrsunfall A2 Baden', difficulty:'easy', base:'wien_c9', region:'Wien', scen:'autobahn',
|
|
waypoints:['wien_c9','wien','moedling','baden','verkehr_a2_baden']},
|
|
{id:'m11', title:'Sog in der Neuen Donau', difficulty:'medium', base:'wien_c9', region:'Wien', scen:'wasser',
|
|
waypoints:['wien_c9','wien','wien_donau','see_donau_wien']},
|
|
{id:'m12', title:'Kletterunfall Schneeberg', difficulty:'hard', base:'wr_neustadt_c3', region:'Niederösterreich', scen:'berg-fels',
|
|
waypoints:['wr_neustadt_c3','wr_neustadt','puchberg','schneeberg','berg_schneeberg']},
|
|
{id:'m13', title:'Wanderunfall Rax', difficulty:'medium', base:'wr_neustadt_c3', region:'Niederösterreich', scen:'berg-fels',
|
|
waypoints:['wr_neustadt_c3','wr_neustadt','semmering','rax','berg_rax']},
|
|
{id:'m14', title:'Surfer Neusiedler See', difficulty:'easy', base:'oberwart_c16', region:'Burgenland', scen:'wasser',
|
|
waypoints:['oberwart_c16','mattersburg','eisenstadt','podersdorf','see_neusiedl']},
|
|
{id:'m15', title:'Verkehrsunfall A1 Sattledt', difficulty:'medium', base:'linz_c10', region:'Oberösterreich', scen:'autobahn',
|
|
waypoints:['linz_c10','linz','wels','sattledt','verkehr_a1_sattledt']},
|
|
{id:'m16', title:'Boot in Not Traunsee', difficulty:'easy', base:'linz_c10', region:'Oberösterreich', scen:'wasser',
|
|
waypoints:['linz_c10','wels','gmunden','traunsee','see_traun']},
|
|
{id:'m17', title:'LKW-Unfall A10 Tauern', difficulty:'medium', base:'salzburg_c6', region:'Salzburg', scen:'autobahn',
|
|
waypoints:['salzburg_c6','hallein','golling','flachau','verkehr_a10_tauern']},
|
|
{id:'m18', title:'Seerettung Wolfgangsee', difficulty:'easy', base:'salzburg_c6', region:'Salzburg', scen:'wasser',
|
|
waypoints:['salzburg_c6','hallein','st_wolfgang','see_wolfgang']},
|
|
{id:'m19', title:'Absturz Großglockner', difficulty:'hard', base:'salzburg_c6', region:'Salzburg', scen:'berg-schnee',
|
|
waypoints:['salzburg_c6','zell_see','kaprun','heiligenblut','grossglockner','berg_glockner']},
|
|
{id:'m20', title:'Dachstein Südwand', difficulty:'hard', base:'niederoeblarn_c14', region:'Steiermark', scen:'berg-schnee',
|
|
waypoints:['niederoeblarn_c14','bad_aussee','schladming','dachstein','berg_dachstein']},
|
|
{id:'m21', title:'Alpinunfall Hochschwab', difficulty:'medium', base:'graz_c12', region:'Steiermark', scen:'berg-fels',
|
|
waypoints:['graz_c12','bruck_mur','mariazell','hochschwab','berg_hochschwab']},
|
|
{id:'m22', title:'Unfall A9 Bosruck', difficulty:'medium', base:'graz_c12', region:'Steiermark', scen:'autobahn',
|
|
waypoints:['graz_c12','bruck_mur','leoben','verkehr_a9_pyhrn']},
|
|
{id:'m23', title:'Badegast Wörthersee', difficulty:'easy', base:'klagenfurt_c11', region:'Kärnten', scen:'wasser',
|
|
waypoints:['klagenfurt_c11','klagenfurt','poertschach','see_woerth']},
|
|
{id:'m24', title:'Glockner Südseite', difficulty:'hard', base:'klagenfurt_c11', region:'Kärnten', scen:'berg-schnee',
|
|
waypoints:['klagenfurt_c11','villach','spittal','heiligenblut','berg_glockner']},
|
|
{id:'m25', title:'Unfall A2 Klagenfurt', difficulty:'easy', base:'klagenfurt_c11', region:'Kärnten', scen:'autobahn',
|
|
waypoints:['klagenfurt_c11','klagenfurt','verkehr_a2_klagenfurt']},
|
|
];
|
|
|
|
const AUDIO_PATH = '../../sims/heli/sounds/radio/';
|
|
|
|
// --- Loader: Texte der JSON-Bücher ---
|
|
async function loadTextLibrary() {
|
|
const files = [
|
|
'../../sims/heli/scripts/tower-lines.json',
|
|
'../../sims/heli/scripts/tower-geo-lines.json',
|
|
'../../sims/heli/scripts/tower-start-lines.json',
|
|
'../../sims/heli/scripts/tower-sea-lines.json',
|
|
'../../sims/heli/scripts/tower-heliintro-lines.json',
|
|
'../../sims/heli/scripts/nav-flight-lines.json',
|
|
'../../sims/heli/scripts/pilot-situations-lines.json',
|
|
];
|
|
const lib = {};
|
|
for (const f of files) {
|
|
try {
|
|
const r = await fetch(f);
|
|
if (!r.ok) continue;
|
|
const d = await r.json();
|
|
for (const line of (d.lines || [])) lib[line.id] = line.text;
|
|
} catch(e) { console.warn('skip', f, e); }
|
|
}
|
|
return lib;
|
|
}
|
|
|
|
// Pro Mission ein Counter, damit jede Zeile eine Nummer bekommt (M1-01 etc.)
|
|
let _rowCounter = 0;
|
|
let _currentMissionId = '';
|
|
function nextTag() {
|
|
_rowCounter++;
|
|
return `${_currentMissionId.toUpperCase()}-${String(_rowCounter).padStart(2,'0')}`;
|
|
}
|
|
|
|
// Audio-Zeile rendern
|
|
function audioRow(id, label, text) {
|
|
const tag = nextTag();
|
|
const file = AUDIO_PATH + id + '.mp3';
|
|
return `<div class="audio" data-tag="${tag}">
|
|
<span class="tag" title="Nummer zum Referenzieren: ${tag}">${tag}</span>
|
|
<span class="label">${label}</span>
|
|
<audio controls preload="none" src="${file}"></audio>
|
|
<span class="text">${text || '<span class="missing">(kein Text hinterlegt)</span>'}</span>
|
|
</div>`;
|
|
}
|
|
|
|
function audioGroupRandom(label, list, lib) {
|
|
const outerTag = nextTag();
|
|
return `<div class="audio group-random" data-tag="${outerTag}">
|
|
<span class="tag" title="Gruppe (zufaellig): ${outerTag}">${outerTag}</span>
|
|
<span class="label">${label}</span>
|
|
<div class="random-list">
|
|
${list.map(id => {
|
|
const t = nextTag();
|
|
return `<div class="audio" data-tag="${t}">
|
|
<span class="tag" title="${t}">${t}</span>
|
|
<audio controls preload="none" src="${AUDIO_PATH + id + '.mp3'}"></audio>
|
|
<span class="text">${lib[id] || '<span class="missing">(kein Text)</span>'}</span>
|
|
</div>`;
|
|
}).join('')}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// Rendert alle Missionen
|
|
async function render() {
|
|
const lib = await loadTextLibrary();
|
|
const nav = document.getElementById('nav');
|
|
nav.innerHTML = ALL_MISSIONS.map(m => `<a href="#${m.id}">${m.id}</a>`).join('');
|
|
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = ALL_MISSIONS.map(m => {
|
|
// Reset Counter pro Mission — so startet jede bei 01
|
|
_rowCounter = 0;
|
|
_currentMissionId = m.id;
|
|
const cfg = BASE_CONFIG[m.base] || {};
|
|
const key = cfg.heliKey || 'c1';
|
|
const isHw = m.scen === 'autobahn';
|
|
const isSea = m.scen === 'wasser';
|
|
|
|
// Landing-Audio-Variante
|
|
let approachKey = 'tower_approach_' + key;
|
|
let extractKey = 'tower_extract_' + key;
|
|
if (isHw) { approachKey = 'tower_hw_approach_' + key; extractKey = 'tower_hw_extract_' + key; }
|
|
if (isSea) { approachKey = 'tower_sea_approach_' + key; extractKey = 'tower_sea_extract_' + key; }
|
|
|
|
// Himmelsrichtungen der Segmente — nur Start→Ziel grob, Demo-Zweck
|
|
const dirAudios = ['dir_nord','dir_nordost','dir_ost','dir_suedost','dir_sued','dir_suedwest','dir_west','dir_nordwest'];
|
|
|
|
return `<section class="mission" id="${m.id}">
|
|
<h2>${m.id} · ${m.title}</h2>
|
|
<div class="meta">
|
|
<b>Basis:</b> ${m.base} · ${cfg.heliName || '?'} ·
|
|
<b>Region:</b> ${m.region} · <b>Schwierigkeit:</b> ${m.difficulty} ·
|
|
<b>Landeszene:</b> ${m.scen}
|
|
</div>
|
|
<div class="route"><b>Route:</b> ${m.waypoints.join(' → ')}</div>
|
|
|
|
<div class="audios">
|
|
<div class="phase-header">Phase 2 · Plan (Feedback)</div>
|
|
${audioRow('r_nav_plan_start', 'Planung beginnt', lib['r_nav_plan_start'])}
|
|
${audioRow('r_nav_plan_correct', 'Wegpunkt richtig', lib['r_nav_plan_correct'])}
|
|
${audioRow('r_nav_plan_wrong_near', 'Fast — nahe', lib['r_nav_plan_wrong_near'])}
|
|
${audioRow('r_nav_plan_wrong_far', 'Weit daneben', lib['r_nav_plan_wrong_far'])}
|
|
${audioRow('r_nav_plan_done', 'Route vollständig', lib['r_nav_plan_done'])}
|
|
|
|
<div class="phase-header">Phase 3 · Start am Stützpunkt</div>
|
|
${audioRow('r_tower_start_' + key, 'Tower-Freigabe (${key})'.replace('${key}', key), lib['r_tower_start_' + key])}
|
|
|
|
<div class="phase-header">Phase 3→4 · Abflug</div>
|
|
${audioRow('r_nav_route_check', 'Route wird geprüft', lib['r_nav_route_check'])}
|
|
${audioRow('r_nav_departure_ok', 'Abflug freigegeben', lib['r_nav_departure_ok'])}
|
|
${audioRow('r_nav_departure_takeover','Chefpilot übernimmt',lib['r_nav_departure_takeover'])}
|
|
|
|
<div class="phase-header">Phase 4 · Kartenflug</div>
|
|
${audioRow('r_nav_heli_intro_' + key, 'Heli-Flug-Intro (einmalig)', lib['r_nav_heli_intro_' + key])}
|
|
${audioGroupRandom('Richtungs-Audios', dirAudios.map(d => d), {})}
|
|
<div class="audio"><span class="label">Himmelsrichtungs-Audios</span>
|
|
<span style="font-style:italic;color:#666">liegen in sounds/nav/ (dir_nord.mp3 etc.)</span>
|
|
<span></span>
|
|
</div>
|
|
${audioRow('r_nav_fuel_half', 'Treibst. halb', lib['r_nav_fuel_half'])}
|
|
${audioRow('r_nav_fuel_quarter', 'Treibst. viertel', lib['r_nav_fuel_quarter'])}
|
|
${audioRow('r_nav_altitude_mountain','Höhe Bergkette', lib['r_nav_altitude_mountain'])}
|
|
${audioRow('r_nav_altitude_valley','Höhe Tal', lib['r_nav_altitude_valley'])}
|
|
|
|
<div class="phase-header">Phase 5 · Zielanflug (ICAO-Protokoll)</div>
|
|
${audioRow('r_pilot_call_' + key, 'Pilot meldet sich', lib['r_pilot_call_' + key])}
|
|
${audioRow('r_tower_geo_' + m.id, 'Tower + Geografie', lib['r_tower_geo_' + m.id])}
|
|
${audioRow('r_' + approachKey, 'Tower-Anflug (' + (isHw?'Autobahn':isSea?'Wasser':'Standard') + ')', lib['r_' + approachKey])}
|
|
|
|
<div class="phase-header">Phase 5 · Landung</div>
|
|
${audioRow('r_land_winch_down', 'Seil läuft', lib['r_land_winch_down'])}
|
|
${audioRow('r_land_rescuer_down', 'Retter am Boden', lib['r_land_rescuer_down'])}
|
|
${audioRow('r_land_rescuer_ready', 'Patient bereit', lib['r_land_rescuer_ready'])}
|
|
${audioRow('r_land_hook_connected', 'Haken eingeklinkt', lib['r_land_hook_connected'])}
|
|
${audioRow('r_land_lift_success', 'Patient an Bord', lib['r_land_lift_success'])}
|
|
${audioRow('r_' + extractKey, 'Tower-Extract (' + (isHw?'Autobahn':isSea?'Wasser':'Standard') + ')', lib['r_' + extractKey])}
|
|
${audioRow('r_land_clinic_ahead', 'Klinik voraus', lib['r_land_clinic_ahead'])}
|
|
${audioRow('r_land_disembark', 'Saubere Landung', lib['r_land_disembark'])}
|
|
|
|
<div class="phase-header">Strike / Fehler</div>
|
|
${audioRow('r_tower_strike_' + key, 'Tower-Strike (' + key + ')', lib['r_tower_strike_' + key])}
|
|
${audioGroupRandom('Pilot-Reaktion (zufällig)', ['r_pilot_strike_1','r_pilot_strike_2','r_pilot_strike_3','r_pilot_strike_4','r_pilot_strike_5'], lib)}
|
|
${audioRow('r_pilot_three_strikes', 'Drei Berührungen', lib['r_pilot_three_strikes'])}
|
|
${audioRow('r_tower_handover_' + key, 'Chefpilot-Übernahme', lib['r_tower_handover_' + key])}
|
|
|
|
<div class="phase-header">Situations-Warnungen (wenn Situation eintritt)</div>
|
|
${audioRow('r_pilot_wind_turn', 'Wind dreht', lib['r_pilot_wind_turn'])}
|
|
${audioRow('r_pilot_bird_ahead', 'Vogelschwarm voraus', lib['r_pilot_bird_ahead'])}
|
|
${audioRow('r_pilot_plane_ahead', 'Verkehrsflugzeug', lib['r_pilot_plane_ahead'])}
|
|
${audioRow('r_pilot_sportplane_level', 'Sportflieger', lib['r_pilot_sportplane_level'])}
|
|
${audioRow('r_pilot_storm_detour', 'Gewitter', lib['r_pilot_storm_detour'])}
|
|
${audioRow('r_pilot_fog_thickens', 'Nebel dichter', lib['r_pilot_fog_thickens'])}
|
|
${audioRow('r_pilot_time_up', 'Zeit abgelaufen', lib['r_pilot_time_up'])}
|
|
${audioRow('r_pilot_too_high', 'Zu hoch/viel Sprit', lib['r_pilot_too_high'])}
|
|
</div>
|
|
</section>`;
|
|
}).join('');
|
|
|
|
// Stoppe andere Audio-Elemente, wenn eines abgespielt wird
|
|
document.querySelectorAll('audio').forEach(a => {
|
|
a.addEventListener('play', () => {
|
|
document.querySelectorAll('audio').forEach(b => { if (b !== a) b.pause(); });
|
|
});
|
|
});
|
|
}
|
|
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|