Files
geograsim/App/sims/heli/scripts/funk-pille.js
T
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- 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>
2026-07-08 02:27:02 +02:00

219 lines
9.3 KiB
JavaScript

/**
* Heli-Funk-Pille — shared Modul fuer game.html, start.html, landing.html,
* Drehbuch-Tool. Zeigt waehrend laufender Sprachausgabe einen Sprecher mit
* Bild + Untertitel + Restzeit-Balken oben mittig im Viewport.
*
* Quellen:
* - speakers.json (zentrales Sprecher-Mapping)
* - audio-texts.json (konsolidierte Audio-ID → Text-Map)
*
* Init asynchron:
* await HeliFunk.init(scriptsBaseUrl, imagesBaseUrl)
*
* Anzeigen/Verstecken (audioId stammt aus dem Spiel-Code):
* HeliFunk.show(audioId, { duration: bufferDurationSeconds, heliKey: 'c8' })
* HeliFunk.tick(elapsedSec) // optional, kontinuierlich
* HeliFunk.hide()
*
* Skip-Button: ruft die optional uebergebene onSkip-Callback auf.
* HeliFunk.show(audioId, { duration, heliKey, onSkip: function(){ ... } })
*/
(function(global){
'use strict';
var SPEAKERS = null;
var TEXTS = null;
var IMG_BASE = '';
var ready = false;
var pendingShows = [];
var current = null; // {audioId, durationSec, startMs, onSkip}
var rafHandle = 0;
// ============================================================
// CSS — wird einmal per <style>-Tag injiziert
// ============================================================
var CSS = ''
+ '#heliFunkPille{position:fixed;top:.6rem;left:50%;transform:translateX(-50%);z-index:9500;'
+ 'background:rgba(31,75,55,.94);color:#f5efe0;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,.35);'
+ 'padding:.55rem .8rem;display:none;align-items:center;gap:.7rem;max-width:min(720px,calc(100vw - 1.4rem));'
+ 'font-family:"Inter",system-ui,sans-serif;pointer-events:none}'
+ '#heliFunkPille.show{display:flex}'
+ '#heliFunkPille .fp-img{width:54px;height:54px;border-radius:50%;background:#1f4b37;'
+ 'border:2.5px solid #e8c547;flex-shrink:0;background-size:cover;background-position:center}'
+ '#heliFunkPille .fp-body{display:flex;flex-direction:column;gap:.15rem;min-width:240px;flex:1;min-width:0}'
+ '#heliFunkPille .fp-meta{display:flex;align-items:center;gap:.5rem;font-size:.7rem;font-weight:800;letter-spacing:.05em;opacity:.85}'
+ '#heliFunkPille .fp-meta .fp-icon{color:#e8c547}'
+ '#heliFunkPille .fp-text{font-size:.85rem;line-height:1.35;font-weight:500;'
+ 'overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;max-width:520px}'
+ '#heliFunkPille .fp-bar{margin-top:.18rem;height:4px;background:rgba(255,255,255,.18);border-radius:2px;overflow:hidden}'
+ '#heliFunkPille .fp-bar-fill{height:100%;background:#e8c547;width:0%;transition:width .12s linear}'
+ '#heliFunkPille .fp-skip{pointer-events:auto;margin-left:.4rem;background:rgba(255,255,255,.12);'
+ 'color:#f5efe0;border:none;border-radius:6px;padding:.3rem .55rem;font-size:.7rem;font-weight:700;cursor:pointer;font-family:inherit}'
+ '#heliFunkPille .fp-skip:hover{background:rgba(255,255,255,.2)}'
+ '#heliFunkPille .fp-skip:disabled{opacity:.4;cursor:not-allowed}'
+ '@media(max-width:600px){#heliFunkPille .fp-img{width:42px;height:42px}#heliFunkPille .fp-text{font-size:.78rem;-webkit-line-clamp:2}#heliFunkPille .fp-body{min-width:160px}}';
function injectCss(){
if (document.getElementById('heliFunkPilleStyle')) return;
var s = document.createElement('style');
s.id = 'heliFunkPilleStyle';
s.textContent = CSS;
document.head.appendChild(s);
}
function buildElement(){
if (document.getElementById('heliFunkPille')) return;
var d = document.createElement('div');
d.id = 'heliFunkPille';
d.innerHTML = ''
+ '<div class="fp-img" id="heliFunkPilleImg"></div>'
+ '<div class="fp-body">'
+ '<div class="fp-meta"><span class="fp-icon">🎙️</span><span id="heliFunkPilleLabel">—</span></div>'
+ '<div class="fp-text" id="heliFunkPilleText">—</div>'
+ '<div class="fp-bar"><div class="fp-bar-fill" id="heliFunkPilleFill"></div></div>'
+ '</div>'
+ '<button class="fp-skip" id="heliFunkPilleSkip" disabled>⏭ Skip</button>';
document.body.appendChild(d);
document.getElementById('heliFunkPilleSkip').addEventListener('click', function(){
if (current && typeof current.onSkip === 'function') {
try { current.onSkip(); } catch(_){}
}
});
}
// ============================================================
// Init: speakers.json + audio-texts.json laden
// ============================================================
async function init(scriptsBaseUrl, imagesBaseUrl){
if (ready) return;
IMG_BASE = imagesBaseUrl || '';
try {
var [s, t] = await Promise.all([
fetch(scriptsBaseUrl + '/speakers.json').then(function(r){ return r.json(); }),
fetch(scriptsBaseUrl + '/audio-texts.json').then(function(r){ return r.json(); })
]);
SPEAKERS = s;
TEXTS = t;
ready = true;
// Seitenelemente erst nach DOM-ready einhaengen
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function(){ injectCss(); buildElement(); flushPending(); });
} else {
injectCss(); buildElement(); flushPending();
}
} catch(e) {
console.warn('HeliFunk init failed:', e);
}
}
function flushPending(){
while (pendingShows.length){
var p = pendingShows.shift();
doShow(p.audioId, p.opts);
}
}
// ============================================================
// Sprecher-Resolver (mit Regeln aus speakers.json)
// ============================================================
function resolveSpeaker(audioId, heliKey){
if (!SPEAKERS || !SPEAKERS.rules) return null;
for (var i = 0; i < SPEAKERS.rules.length; i++){
var r = SPEAKERS.rules[i];
if (new RegExp(r.match).test(audioId)) {
var speakerKey = r.speaker;
if (speakerKey === '{pilot}') {
// Fallback wenn heliKey fehlt: Marlene (pilot_b_f) — laut speakers.json
// _pilot_by_helikey aktuell ALLE Stuetzpunkte auf Marlene gemappt.
speakerKey = (SPEAKERS.pilot_by_helikey || {})[heliKey] || 'pilot_b_f';
}
var sp = (SPEAKERS.speakers || {})[speakerKey];
if (!sp) continue;
// Label-Variablen aufloesen
var label = r.label || sp.label;
if (label && label.indexOf('{tower_city}') !== -1) {
var city = (SPEAKERS.tower_city_by_helikey || {})[heliKey];
if (city) {
label = label.replace('{tower_city}', city);
} else {
// heliKey unbekannt -> doppeltes "Tower" vermeiden, ' {tower_city}' rausnehmen
label = label.replace(/\s*\{tower_city\}/g, '').trim() || 'Tower';
}
}
if (label && label.indexOf('{pilot_label}') !== -1) {
label = label.replace('{pilot_label}', sp.label || speakerKey);
}
return { key: speakerKey, label: label, image: IMG_BASE + '/' + sp.image, role: sp.role, gender: sp.gender };
}
}
return null;
}
function getText(audioId){
if (!TEXTS) return '';
return TEXTS[audioId] || '';
}
// ============================================================
// show / hide / tick
// ============================================================
function show(audioId, opts){
opts = opts || {};
if (!ready) { pendingShows.push({audioId: audioId, opts: opts}); return; }
doShow(audioId, opts);
}
function doShow(audioId, opts){
var heliKey = opts.heliKey || '';
var speaker = resolveSpeaker(audioId, heliKey);
var text = getText(audioId) || opts.fallbackText || '';
var dur = Math.max(2, opts.duration || 6);
var d = document.getElementById('heliFunkPille');
if (!d) { pendingShows.push({audioId: audioId, opts: opts}); return; }
var img = document.getElementById('heliFunkPilleImg');
var lbl = document.getElementById('heliFunkPilleLabel');
var txt = document.getElementById('heliFunkPilleText');
var fill = document.getElementById('heliFunkPilleFill');
var skip = document.getElementById('heliFunkPilleSkip');
if (speaker) {
img.style.backgroundImage = 'url(' + speaker.image + ')';
lbl.textContent = speaker.label || speaker.key;
} else {
img.style.backgroundImage = '';
lbl.textContent = audioId;
}
txt.textContent = text;
fill.style.width = '0%';
skip.disabled = !opts.onSkip;
d.classList.add('show');
current = { audioId: audioId, durationSec: dur, startMs: performance.now(), onSkip: opts.onSkip || null };
if (rafHandle) cancelAnimationFrame(rafHandle);
rafHandle = requestAnimationFrame(loop);
}
function hide(){
var d = document.getElementById('heliFunkPille');
if (d) d.classList.remove('show');
current = null;
if (rafHandle) { cancelAnimationFrame(rafHandle); rafHandle = 0; }
}
function loop(){
if (!current) return;
var elapsed = (performance.now() - current.startMs) / 1000;
var fill = document.getElementById('heliFunkPilleFill');
if (fill) {
var f = Math.min(1, elapsed / current.durationSec);
fill.style.width = (f * 100).toFixed(1) + '%';
}
if (elapsed > current.durationSec + 0.3) {
// Auto-Hide ueber Buffer-Ende — sicherer Fallback wenn onended nicht feuert
hide();
return;
}
rafHandle = requestAnimationFrame(loop);
}
global.HeliFunk = { init: init, show: show, hide: hide, resolveSpeaker: resolveSpeaker, getText: getText };
})(window);