orbit/js/utils/Decode.js

87 lines
3.3 KiB
JavaScript

/**
* The "decode" scramble — the shared type-out effect of the console.
*
* The main menu uses it on the Galaxy Seed field (it "decodes" in as the
* panel assembles); the game uses it on the system dossier HUD — the
* system name and every data line below it type themselves in as the
* ship arrives. One effect, two scenes.
*
* Over `dur` ms the target string reveals left-to-right while the
* unrevealed slots hold random glyphs from the decode alphabet — the
* console pulling a signal out of static. With `reverse = true` the
* same reveal plays BACKWARDS (value → shrinking stable prefix → static
* → '') — the console dropping the signal, used to deconstruct the
* dossier when it folds away.
*
* Pure (no Phaser): build one per line with a start time, poll
* `display(time)` from the scene's `update()`, and drop it once
* `finished(time)`. The time base is the engine loop time the scene's
* `update(time, delta)` receives (=== `this.time.now` after that frame).
*
* const dec = new ScrambleDecode('Kepler-9', t0, DECODE_DURATION);
* // each frame: if (dec.started(time)) text.setText(dec.display(time));
* // done when: dec.finished(time)
*/
/** The alphabet unrevealed slots draw from (no vowels, no 0/1 — signal, not language). */
export const DECODE_CHARS = 'abcdefghjkmnpqrstuvwxyz23456789#%+*<>?';
/** The menu's seed-field window (ms) — the canonical decode timing. */
export const DECODE_DURATION = 620;
export class ScrambleDecode {
/**
* @param {string} value the final text
* @param {number} t0 absolute start time (scene time, in ms)
* @param {number} [dur] reveal window in ms
* @param {boolean} [reverse] false = construct ('' → static → value);
* true = deconstruct (value → static → ''), the same reveal in reverse
*/
constructor(value, t0, dur = DECODE_DURATION, reverse = false) {
this.value = String(value ?? '');
this.t0 = t0;
this.dur = dur > 0 ? dur : 1;
this.reverse = !!reverse;
}
started(time) {
return time >= this.t0;
}
finished(time) {
return (time - this.t0) / this.dur >= 1;
}
/**
* The display string at `time`: a stable prefix of the target with the
* remaining slots scrambled. Forward: the prefix grows. Reverse: it
* shrinks. Before the start: forward is '', reverse holds the value
* (the line is already showing it). After the finish: forward holds
* the value, reverse is '' (the reveal lands at ~87% of the window,
* then holds).
*/
display(time) {
if (!this.started(time)) return this.reverse ? this.value : '';
if (this.finished(time)) return this.reverse ? '' : this.value;
const u = (time - this.t0) / this.dur;
const progress = this.reverse
? Math.max(0, Math.min(1, (1 - u) * 1.15))
: Math.max(0, Math.min(1, u * 1.15));
const reveal = Math.floor(progress * this.value.length);
let out = '';
for (let i = 0; i < this.value.length; i++) {
out += i < reveal ? this.value[i] : DECODE_CHARS[(Math.random() * DECODE_CHARS.length) | 0];
}
return out;
}
}
/**
* Per-line window for multi-line decodes (the system dossier): ~70 ms per
* character — the menu's feel — floored so short lines don't flicker and
* capped so long ones don't drag.
*/
export function decodeDur(length = 0) {
return Math.max(420, Math.min(880, 320 + length * 46));
}