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>
219 lines
9.3 KiB
JavaScript
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);
|