MarianaCodebase commited on
Commit
5067e91
·
verified ·
1 Parent(s): 0b8a828

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. static/css/radio.css +72 -1
  2. static/js/radio.js +103 -51
static/css/radio.css CHANGED
@@ -1583,6 +1583,76 @@ body {
1583
  }
1584
  .notes-list li.just-added { animation: note-flash 1.1s ease-out; }
1585
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1586
  /* ===================================================================
1587
  Responsive
1588
  =================================================================== */
@@ -1658,7 +1728,8 @@ body {
1658
  }
1659
  .guide-btn::after,
1660
  .intercept.urgent,
1661
- .notes-list li.just-added {
 
1662
  animation: none;
1663
  }
1664
  .noise-canvas,
 
1583
  }
1584
  .notes-list li.just-added { animation: note-flash 1.1s ease-out; }
1585
 
1586
+ /* ===================================================================
1587
+ Endgame mission HUD (fragment hunt in the secret band)
1588
+ =================================================================== */
1589
+ .mission-hud {
1590
+ display: flex;
1591
+ flex-direction: column;
1592
+ gap: 7px;
1593
+ width: 100%;
1594
+ max-width: 620px;
1595
+ margin: 0 auto;
1596
+ padding: 9px 16px;
1597
+ border: 1px solid rgba(255, 246, 216, 0.4);
1598
+ border-radius: 9px;
1599
+ background: rgba(8, 14, 6, 0.72);
1600
+ box-shadow: 0 0 26px rgba(255, 246, 216, 0.12), inset 0 0 18px rgba(0, 0, 0, 0.5);
1601
+ animation: mission-glow 2.6s ease-in-out infinite;
1602
+ }
1603
+
1604
+ .mission-hud.hidden { display: none; }
1605
+
1606
+ @keyframes mission-glow {
1607
+ 0%, 100% { box-shadow: 0 0 18px rgba(255, 246, 216, 0.1), inset 0 0 18px rgba(0, 0, 0, 0.5); }
1608
+ 50% { box-shadow: 0 0 34px rgba(255, 246, 216, 0.22), inset 0 0 18px rgba(0, 0, 0, 0.5); }
1609
+ }
1610
+
1611
+ .mission-hud-text {
1612
+ font-size: 0.82rem;
1613
+ letter-spacing: 0.08em;
1614
+ text-align: center;
1615
+ color: #fff6d8;
1616
+ text-shadow: 0 0 10px rgba(255, 246, 216, 0.4);
1617
+ }
1618
+
1619
+ .mission-hud-track {
1620
+ height: 9px;
1621
+ border-radius: 5px;
1622
+ background: rgba(0, 0, 0, 0.55);
1623
+ border: 1px solid rgba(255, 246, 216, 0.2);
1624
+ overflow: hidden;
1625
+ }
1626
+
1627
+ .mission-hud-fill {
1628
+ height: 100%;
1629
+ width: 0%;
1630
+ background: linear-gradient(90deg, #8a7a3a, #fff6d8);
1631
+ box-shadow: 0 0 10px rgba(255, 246, 216, 0.6);
1632
+ transition: width 0.12s linear;
1633
+ }
1634
+
1635
+ .mission-hud-pips {
1636
+ display: flex;
1637
+ justify-content: center;
1638
+ gap: 9px;
1639
+ }
1640
+
1641
+ .mission-hud-pips span {
1642
+ width: 11px;
1643
+ height: 11px;
1644
+ border-radius: 50%;
1645
+ border: 1px solid rgba(255, 246, 216, 0.45);
1646
+ background: rgba(255, 246, 216, 0.06);
1647
+ transition: background 0.25s, box-shadow 0.25s;
1648
+ }
1649
+
1650
+ .mission-hud-pips span.on {
1651
+ background: #fff6d8;
1652
+ border-color: #fff6d8;
1653
+ box-shadow: 0 0 10px rgba(255, 246, 216, 0.8);
1654
+ }
1655
+
1656
  /* ===================================================================
1657
  Responsive
1658
  =================================================================== */
 
1728
  }
1729
  .guide-btn::after,
1730
  .intercept.urgent,
1731
+ .notes-list li.just-added,
1732
+ .mission-hud {
1733
  animation: none;
1734
  }
1735
  .noise-canvas,
static/js/radio.js CHANGED
@@ -66,6 +66,10 @@ const notesScratchLabelEl = document.getElementById("notes-scratch-label");
66
  const notesFoundEl = document.getElementById("notes-found");
67
  const notesCipherEl = document.getElementById("notes-cipher");
68
  const notesScratch = document.getElementById("notes-scratch");
 
 
 
 
69
 
70
  // =====================================================================
71
  // i18n: the listener picks a language (affects UI + model broadcasts)
@@ -110,9 +114,12 @@ const I18N = {
110
  numberHint:
111
  "Escucha el Morse · escribe la secuencia de números en la consola para entrar",
112
  cipherBroken: "— CIFRADO ROTO — la banda 108-112 MHz existe ahora —",
113
- unlockHint: (code) => `código ${code} aceptado · el dial creció · busca los fragmentos`,
114
  downloading: (name, pct) => `↓ ${name} — ${pct}% · sostén el dial`,
115
- received: (name, a, b) => `✓ ${name} recibido (${a}/${b})`,
 
 
 
116
  finaleTitle: "— EL ÚLTIMO MENSAJE —",
117
  finaleEnd: "— fin de todas las transmisiones · gracias por escuchar —",
118
  intro:
@@ -162,9 +169,12 @@ const I18N = {
162
  numberHint:
163
  "Listen to the Morse · type the number sequence into the console to break in",
164
  cipherBroken: "— CIPHER BROKEN — the 108-112 MHz band exists now —",
165
- unlockHint: (code) => `code ${code} accepted · the dial grew · find the fragments`,
166
  downloading: (name, pct) => `↓ ${name} — ${pct}% · hold the dial steady`,
167
- received: (name, a, b) => `✓ ${name} received (${a}/${b})`,
 
 
 
168
  finaleTitle: "— THE LAST MESSAGE —",
169
  finaleEnd: "— end of all transmissions · thank you for listening —",
170
  intro:
@@ -214,9 +224,12 @@ const I18N = {
214
  numberHint:
215
  "Écoute le morse · tape la séquence de nombres dans la console pour entrer",
216
  cipherBroken: "— CHIFFREMENT BRISÉ — la bande 108-112 MHz existe maintenant —",
217
- unlockHint: (code) => `code ${code} accepté · le cadran a grandi · trouvez les fragments`,
218
  downloading: (name, pct) => `↓ ${name} — ${pct}% · tenez le cadran`,
219
- received: (name, a, b) => `✓ ${name} reçu (${a}/${b})`,
 
 
 
220
  finaleTitle: "— LE DERNIER MESSAGE —",
221
  finaleEnd: "— fin de toutes les transmissions · merci d'avoir écouté —",
222
  intro:
@@ -614,8 +627,15 @@ async function restoreProgress() {
614
  unlockCode = saved.code;
615
  collectedFragments = new Set(saved.collected || []);
616
  const ok = await loadHiddenBand(unlockCode);
617
- if (ok) expandBand(false);
618
- else unlockCode = null;
 
 
 
 
 
 
 
619
  }
620
  } catch (_) {
621
  /* corrupted state: ignored */
@@ -1345,26 +1365,30 @@ function animate(now) {
1345
  inertiaActive = false;
1346
  freqVel = 0;
1347
  frequency = Math.round(frequency * 10) / 10;
1348
- updateDialUI();
1349
- tuneNow();
1350
  } else {
1351
  updateDialUI();
1352
  }
1353
  }
1354
 
1355
- // hidden band: the world powers down and the dial drifts on its own
1356
- if (
1357
- unlockCode &&
1358
- !finaleStarted &&
1359
- !isDragging &&
1360
- !inertiaActive &&
1361
- frequency >= BASE_FREQ_MAX
1362
- ) {
1363
- driftVel += (Math.random() - 0.5) * 0.012;
1364
- driftVel = Math.max(-0.05, Math.min(0.05, driftVel));
1365
- frequency = Math.min(freqMax, Math.max(FREQ_MIN, frequency + driftVel * dt));
1366
- updateDialUI();
1367
  updateDownload(dt);
 
 
 
 
 
 
1368
  }
1369
 
1370
  // under-damped needle spring (overshoot while settling)
@@ -1468,7 +1492,7 @@ function onPointerUp(evt) {
1468
  } else {
1469
  freqVel = 0;
1470
  setFrequency(frequency); // rounds to 0.1
1471
- tuneNow();
1472
  }
1473
  }
1474
 
@@ -1491,7 +1515,7 @@ function snapToStation(direction) {
1491
  if (!candidates.length) return;
1492
  const target = direction > 0 ? Math.min(...candidates) : Math.max(...candidates);
1493
  setFrequency(target);
1494
- tuneNow();
1495
  }
1496
 
1497
  function onKeyDown(evt) {
@@ -1571,7 +1595,7 @@ function onKnobUp(evt) {
1571
  } else {
1572
  freqVel = 0;
1573
  setFrequency(frequency);
1574
- tuneNow();
1575
  }
1576
  }
1577
 
@@ -1591,7 +1615,7 @@ function onDoubleClick() {
1591
  if (freq === null) return;
1592
  cancelIntro();
1593
  setFrequency(freq);
1594
- tuneNow();
1595
  }
1596
 
1597
  // =====================================================================
@@ -1608,20 +1632,17 @@ async function ensureClient() {
1608
  }
1609
 
1610
  function cancelTune() {
 
 
 
 
 
1611
  tuneGeneration += 1;
1612
- const job = currentJob;
1613
  currentJob = null;
1614
- if (!job) return;
1615
- try {
1616
- if (typeof job.done === "function" && job.done()) return;
1617
- job.cancel();
1618
- } catch (_) {
1619
- // cancel on an already finished job → 422; ignore
1620
- }
1621
  }
1622
 
1623
  async function tuneNow() {
1624
- if (introActive) return;
1625
  const strength = signalStrength(frequency);
1626
  const locked = strength >= 0.95;
1627
  const stationFreq = stationFrequency(frequency);
@@ -2019,18 +2040,21 @@ function updateDownload(dt) {
2019
  const sf = stationFrequency(frequency);
2020
  const meta = stationMeta(sf);
2021
  const strength = signalStrength(frequency);
 
 
2022
 
2023
  if (
2024
  meta?.fragment &&
2025
  sf !== null &&
2026
  !collectedFragments.has(sf.toFixed(1)) &&
2027
- strength >= 0.5
2028
  ) {
2029
  if (downloadTarget !== sf) {
2030
  downloadTarget = sf;
2031
  downloadProgress = 0;
2032
  }
2033
- downloadProgress = Math.min(1, downloadProgress + dt / 7);
 
2034
  if (hint) {
2035
  hint.textContent = t().downloading(
2036
  stationDisplayName(meta),
@@ -2041,23 +2065,45 @@ function updateDownload(dt) {
2041
  collectedFragments.add(sf.toFixed(1));
2042
  persistProgress();
2043
  playJingle(meta.voice);
 
2044
  downloadTarget = null;
2045
  downloadProgress = 0;
2046
- const total = totalFragments();
2047
- if (hint) {
2048
- hint.textContent = t().received(
2049
- stationDisplayName(meta),
2050
- collectedFragments.size,
2051
- total
2052
- );
2053
- }
2054
- if (total > 0 && collectedFragments.size >= total) {
2055
- setTimeout(startFinale, 1600);
2056
- }
2057
  }
2058
- } else if (downloadTarget !== null) {
2059
- downloadProgress = Math.max(0, downloadProgress - dt * 0.3);
2060
- if (downloadProgress === 0) downloadTarget = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2061
  }
2062
  }
2063
 
@@ -2272,6 +2318,12 @@ function renderNotes(flashLast) {
2272
  `<span class="nc-code">${escapeHtml(cipherCode)}</span>`;
2273
  notesCipherEl.appendChild(d);
2274
  }
 
 
 
 
 
 
2275
  }
2276
  }
2277
  }
 
66
  const notesFoundEl = document.getElementById("notes-found");
67
  const notesCipherEl = document.getElementById("notes-cipher");
68
  const notesScratch = document.getElementById("notes-scratch");
69
+ const missionHud = document.getElementById("mission-hud");
70
+ const missionHudText = document.getElementById("mission-hud-text");
71
+ const missionHudFill = document.getElementById("mission-hud-fill");
72
+ const missionHudPips = document.getElementById("mission-hud-pips");
73
 
74
  // =====================================================================
75
  // i18n: the listener picks a language (affects UI + model broadcasts)
 
114
  numberHint:
115
  "Escucha el Morse · escribe la secuencia de números en la consola para entrar",
116
  cipherBroken: "— CIFRADO ROTO — la banda 108-112 MHz existe ahora —",
117
+ unlockHint: (code) => `código ${code} aceptado · el dial creció a 108–112 MHz · reúne los 5 fragmentos`,
118
  downloading: (name, pct) => `↓ ${name} — ${pct}% · sostén el dial`,
119
+ received: (name, a, b) => `✓ ${name} recuperado (${a}/${b})`,
120
+ bandLabel: (c, total) => `EL ÚLTIMO MENSAJE — ${c}/${total} fragmentos`,
121
+ recoverHint: "banda secreta 108–112 · sintoniza un fragmento y sostén el dial para recuperarlo",
122
+ fragmentsLabel: "FRAGMENTOS",
123
  finaleTitle: "— EL ÚLTIMO MENSAJE —",
124
  finaleEnd: "— fin de todas las transmisiones · gracias por escuchar —",
125
  intro:
 
169
  numberHint:
170
  "Listen to the Morse · type the number sequence into the console to break in",
171
  cipherBroken: "— CIPHER BROKEN — the 108-112 MHz band exists now —",
172
+ unlockHint: (code) => `code ${code} accepted · the dial grew to 108–112 MHz · recover all 5 fragments`,
173
  downloading: (name, pct) => `↓ ${name} — ${pct}% · hold the dial steady`,
174
+ received: (name, a, b) => `✓ ${name} recovered (${a}/${b})`,
175
+ bandLabel: (c, total) => `THE LAST MESSAGE — ${c}/${total} fragments`,
176
+ recoverHint: "secret band 108–112 · tune a fragment and hold the dial steady to recover it",
177
+ fragmentsLabel: "FRAGMENTS",
178
  finaleTitle: "— THE LAST MESSAGE —",
179
  finaleEnd: "— end of all transmissions · thank you for listening —",
180
  intro:
 
224
  numberHint:
225
  "Écoute le morse · tape la séquence de nombres dans la console pour entrer",
226
  cipherBroken: "— CHIFFREMENT BRISÉ — la bande 108-112 MHz existe maintenant —",
227
+ unlockHint: (code) => `code ${code} accepté · le cadran s'étend à 108–112 MHz · récupère les 5 fragments`,
228
  downloading: (name, pct) => `↓ ${name} — ${pct}% · tenez le cadran`,
229
+ received: (name, a, b) => `✓ ${name} récupéré (${a}/${b})`,
230
+ bandLabel: (c, total) => `LE DERNIER MESSAGE — ${c}/${total} fragments`,
231
+ recoverHint: "bande secrète 108–112 · accroche un fragment et tiens le cadran pour le récupérer",
232
+ fragmentsLabel: "FRAGMENTS",
233
  finaleTitle: "— LE DERNIER MESSAGE —",
234
  finaleEnd: "— fin de toutes les transmissions · merci d'avoir écouté —",
235
  intro:
 
627
  unlockCode = saved.code;
628
  collectedFragments = new Set(saved.collected || []);
629
  const ok = await loadHiddenBand(unlockCode);
630
+ if (ok) {
631
+ expandBand(false);
632
+ // re-populate the field log so the cipher/progress survive a refresh
633
+ logCipherSeq(MORSE_SEQUENCE);
634
+ logCipherCode(unlockCode);
635
+ renderNotes();
636
+ } else {
637
+ unlockCode = null;
638
+ }
639
  }
640
  } catch (_) {
641
  /* corrupted state: ignored */
 
1365
  inertiaActive = false;
1366
  freqVel = 0;
1367
  frequency = Math.round(frequency * 10) / 10;
1368
+ updateDialUI();
1369
+ scheduleTune();
1370
  } else {
1371
  updateDialUI();
1372
  }
1373
  }
1374
 
1375
+ // hidden band: the world is powering down, so the dial slips gently when you
1376
+ // let go — but the download advances whether you hold it by hand or let it
1377
+ // ride, so the minigame is actually winnable.
1378
+ if (unlockCode && !finaleStarted && frequency >= BASE_FREQ_MAX) {
1379
+ if (!isDragging && !knobDragging && !inertiaActive) {
1380
+ driftVel += (Math.random() - 0.5) * 0.006;
1381
+ driftVel = Math.max(-0.02, Math.min(0.02, driftVel)); // gentle slip
1382
+ frequency = Math.min(freqMax, Math.max(FREQ_MIN, frequency + driftVel * dt));
1383
+ updateDialUI();
1384
+ }
 
 
1385
  updateDownload(dt);
1386
+ // safety: if every fragment is already recovered (e.g. after a refresh),
1387
+ // deliver the last message instead of leaving the player stuck at 5/5
1388
+ const tot = totalFragments();
1389
+ if (tot > 0 && collectedFragments.size >= tot) startFinale();
1390
+ } else {
1391
+ updateMissionHud(false);
1392
  }
1393
 
1394
  // under-damped needle spring (overshoot while settling)
 
1492
  } else {
1493
  freqVel = 0;
1494
  setFrequency(frequency); // rounds to 0.1
1495
+ scheduleTune();
1496
  }
1497
  }
1498
 
 
1515
  if (!candidates.length) return;
1516
  const target = direction > 0 ? Math.min(...candidates) : Math.max(...candidates);
1517
  setFrequency(target);
1518
+ scheduleTune();
1519
  }
1520
 
1521
  function onKeyDown(evt) {
 
1595
  } else {
1596
  freqVel = 0;
1597
  setFrequency(frequency);
1598
+ scheduleTune();
1599
  }
1600
  }
1601
 
 
1615
  if (freq === null) return;
1616
  cancelIntro();
1617
  setFrequency(freq);
1618
+ scheduleTune();
1619
  }
1620
 
1621
  // =====================================================================
 
1632
  }
1633
 
1634
  function cancelTune() {
1635
+ // Bumping the generation makes every in-flight for-await loop break, which
1636
+ // closes its stream client-side; the server self-terminates the superseded
1637
+ // generation via its own epoch. We deliberately do NOT call job.cancel():
1638
+ // gr.Server has no cancel route, so it just spams 404s and can wedge the
1639
+ // client after enough rapid retunes (the other half of "text stops coming").
1640
  tuneGeneration += 1;
 
1641
  currentJob = null;
 
 
 
 
 
 
 
1642
  }
1643
 
1644
  async function tuneNow() {
1645
+ if (introActive || finaleStarted) return; // never interrupt the last message
1646
  const strength = signalStrength(frequency);
1647
  const locked = strength >= 0.95;
1648
  const stationFreq = stationFrequency(frequency);
 
2040
  const sf = stationFrequency(frequency);
2041
  const meta = stationMeta(sf);
2042
  const strength = signalStrength(frequency);
2043
+ const total = totalFragments();
2044
+ const got = collectedFragments.size;
2045
 
2046
  if (
2047
  meta?.fragment &&
2048
  sf !== null &&
2049
  !collectedFragments.has(sf.toFixed(1)) &&
2050
+ strength >= 0.45
2051
  ) {
2052
  if (downloadTarget !== sf) {
2053
  downloadTarget = sf;
2054
  downloadProgress = 0;
2055
  }
2056
+ downloadProgress = Math.min(1, downloadProgress + dt / 5); // ~5s to recover
2057
+ updateMissionHud(true, got, total, downloadProgress, stationDisplayName(meta));
2058
  if (hint) {
2059
  hint.textContent = t().downloading(
2060
  stationDisplayName(meta),
 
2065
  collectedFragments.add(sf.toFixed(1));
2066
  persistProgress();
2067
  playJingle(meta.voice);
2068
+ logStation(sf, stationDisplayName(meta), "fragment"); // recorded in the field log
2069
  downloadTarget = null;
2070
  downloadProgress = 0;
2071
+ const newGot = collectedFragments.size;
2072
+ if (hint) hint.textContent = t().received(stationDisplayName(meta), newGot, total);
2073
+ renderNotes(true);
2074
+ updateMissionHud(true, newGot, total, 0, null);
2075
+ if (total > 0 && newGot >= total) setTimeout(startFinale, 1600);
 
 
 
 
 
 
2076
  }
2077
+ } else {
2078
+ if (downloadTarget !== null) {
2079
+ // forgiving decay: a brief slip off the fragment doesn't wipe progress
2080
+ downloadProgress = Math.max(0, downloadProgress - dt * 0.12);
2081
+ if (downloadProgress === 0) downloadTarget = null;
2082
+ }
2083
+ updateMissionHud(true, got, total, downloadProgress, null);
2084
+ }
2085
+ }
2086
+
2087
+ // On-screen objective + progress for the fragment hunt (so the endgame is clear)
2088
+ function updateMissionHud(show, got = 0, total = 0, progress = 0, name = null) {
2089
+ if (!missionHud) return;
2090
+ if (!show || total <= 0) {
2091
+ missionHud.classList.add("hidden");
2092
+ return;
2093
+ }
2094
+ missionHud.classList.remove("hidden");
2095
+ if (missionHudText) {
2096
+ missionHudText.textContent = name
2097
+ ? `${t().bandLabel(got, total)} · ↓ ${name} ${Math.round(progress * 100)}%`
2098
+ : `${t().bandLabel(got, total)} · ${t().recoverHint}`;
2099
+ }
2100
+ if (missionHudFill) missionHudFill.style.width = `${Math.round(progress * 100)}%`;
2101
+ if (missionHudPips) {
2102
+ if (missionHudPips.children.length !== total) {
2103
+ missionHudPips.textContent = "";
2104
+ for (let i = 0; i < total; i++) missionHudPips.appendChild(document.createElement("span"));
2105
+ }
2106
+ [...missionHudPips.children].forEach((p, i) => p.classList.toggle("on", i < got));
2107
  }
2108
  }
2109
 
 
2318
  `<span class="nc-code">${escapeHtml(cipherCode)}</span>`;
2319
  notesCipherEl.appendChild(d);
2320
  }
2321
+ const fragTotal = totalFragments();
2322
+ if (fragTotal > 0) {
2323
+ const d = document.createElement("div");
2324
+ d.textContent = `${t().fragmentsLabel}: ${collectedFragments.size}/${fragTotal}`;
2325
+ notesCipherEl.appendChild(d);
2326
+ }
2327
  }
2328
  }
2329
  }