// ui/comms.js (Lane E) — the surgical team on the other end of the feed. // // GDD calls the tone "Star Fox arcade flow" and "playful-gross medical sci-fi" but the game // had nobody in it except you. This is the comms window: a voice that tutorialises L1, calls // your position so the tube stops being anonymous black, and carries the humour the docs // describe but never sited anywhere. // // No portrait art (D has a hero-mesh queue and doesn't need faces on it) — a signal panel // with a line-work waveform reads more "instrument feed" than a face would anyway. // // Clock: its own timer, NOT player:state. player.js:103 early-returns before the emit while // dead, so player:state stops exactly when the death line needs to show and dismiss. // Round-3 note filed to B in NOTES. const WHO = { voss: { name: 'voss', role: 'attending', color: '#39e6ff' }, // terse; the calm one park: { name: 'park', role: 'resident', color: '#7dffb0' }, // too excited, always adeyemi: { name: 'adeyemi', role: 'immunology', color: '#ffb13a' }, // apologetic — and amber, }; // the colour of the cells // attacking you. His fault. // Lines are grouped by trigger key. Delivery rotates through each group deterministically — // no RNG at all: this repo hashes and seeds everything, and round-robin has the nicer // property anyway that it cannot repeat a line back-to-back. const LINES = { start: [ ['voss', 'endo-1, you are in. mind the walls — that is a patient.'], ], checkpoint: [ ['voss', (n) => `mark. ${n}.`], ['park', (n) => `${n}! i have only ever seen that in the atlas.`], ['voss', (n) => `${n}. on schedule.`], ], warn_aortic_squeeze: [ ['voss', 'aortic arch — the wall squeezes on the beat. thread it.'], ], warn_reflux_surge: [ ['voss', 'reflux. it is coming up behind you. go. GO.'], ], // keys are the real HAZARDS ids (levels/enemies.js) — checked, not guessed. L2 authors // exactly three. The lines teach what C's registry note says the hazard IS; this is the // tutorialisation channel the GDD asks for and L1 has no other vehicle for. warn_ring_gate: [ ['voss', 'ring gate. it opens on a beat — match the beat, do not ram it.'], ['park', 'peristaltic ring! ease the throttle, arrive when it opens!'], ], warn: [ ['voss', (k) => `hazard ahead — ${String(k).replace(/_/g, ' ')}.`], ], hull_hit: [ ['park', 'that was hull! that was the actual ship!'], ['voss', 'you are bleeding structure. do not trade hits.'], ], coat_hit: [ ['adeyemi', 'coat is taking it. that is what it is for.'], ], coat_low: [ ['adeyemi', 'your coat is nearly gone. find mucin, please.'], ['voss', 'coat critical. next hit is hull.'], ], sample: [ ['park', 'specimen! oh, that is going straight in my thesis.'], ['park', 'got it! that is three years of somebody’s grant.'], ], surf: [ ['park', 'he is riding the peristalsis. he is RIDING it.'], ], combo: [ ['park', (n) => `${n} in a row! did everyone see that?`], ['voss', 'do not showboat in a live patient.'], ], death: [ ['voss', 'we lost the feed. re-acquiring.'], ['adeyemi', 'that was the immune system. i am sorry. they think you are a parasite.'], ], boss: [ ['voss', 'that is the pylorus. it does not open for you.'], ], complete: [ ['voss', 'clear. next segment.'], ], }; // seconds a line holds before it yields; higher pri interrupts a lower one mid-line const HOLD = 3.0; const PRI = { death: 9, boss: 8, warn: 7, complete: 6, coat_low: 5, hull_hit: 4, checkpoint: 3 }; // per-key silence so the crew doesn't chatter over a firefight const COOLDOWN = { coat_hit: 12, hull_hit: 9, combo: 8, surf: 20, checkpoint: 0, sample: 0 }; // Keys are `family` or `family_variant` (`warn` / `warn_reflux_surge`). Look up the exact key // first, then fall back to its family — otherwise every bespoke hazard line inherits the // default priority of 1 and the surge warning, the single most urgent line in the level, can // be talked over by a resident admiring a specimen. Exact keys still win: `hull_hit` is 4, not // whatever `hull` would be. const family = (key) => key.slice(0, key.indexOf('_') + 1 || key.length).replace(/_$/, ''); const priOf = (key) => PRI[key] ?? PRI[family(key)] ?? 1; const cdOf = (key) => COOLDOWN[key] ?? COOLDOWN[family(key)] ?? 0; const CSS = ` .comms { position:absolute; left:18px; bottom:70px; width:330px; font:11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing:.1em; text-transform:uppercase; border-left:1px solid currentColor; padding:6px 0 6px 9px; opacity:0; transition:opacity .18s ease-out; text-shadow:0 0 6px rgba(0,0,0,.95); } .comms.on { opacity:.95; } .comms .who { display:flex; align-items:center; gap:7px; font-size:9px; letter-spacing:.22em; } .comms .role { opacity:.45; } .comms .txt { color:#dff2ff; margin-top:3px; letter-spacing:.08em; opacity:.9; text-transform:none; font-size:12px; } /* the "carrier" — line-work waveform, CSS-animated so a talking head costs zero JS/frame */ .comms .wave { display:flex; align-items:flex-end; gap:2px; height:9px; } .comms .wave i { width:2px; height:100%; background:currentColor; transform-origin:bottom; animation:commsWave .52s ease-in-out infinite; } .comms .wave i:nth-child(2){ animation-delay:.09s } .comms .wave i:nth-child(3){ animation-delay:.18s } .comms .wave i:nth-child(4){ animation-delay:.27s } .comms .wave i:nth-child(5){ animation-delay:.36s } @keyframes commsWave { 0%,100%{ transform:scaleY(.25) } 50%{ transform:scaleY(1) } } @media (prefers-reduced-motion:reduce){ .comms .wave i{ animation:none; transform:scaleY(.6) } } `; export function createComms({ bus, flags = {}, mount = null } = {}) { const host = mount ?? document.getElementById('ui'); if (!host || flags.shots) return { dispose() {} }; // ?shots=1 = clean plate, same as the HUD const style = document.createElement('style'); style.textContent = CSS; document.head.appendChild(style); const root = document.createElement('div'); root.className = 'comms'; root.innerHTML = `