Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/main.ts
import './napplet-settings.js';
import { STORAGE_DOMAIN, storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import {
  canDownloadPack,
  canLoadPackLink,
  canSideLoadPack,
  currentPack,
  currentLevelOverlay,
  currentPackBuffer,
  fetchOriginalPack,
  fetchPackLink,
  fetchRegisteredLevel,
  installedPack,
  installLevelOverlay,
  installPack,
  isGlb,
  loadBuildPack,
  PACK_EMBEDDED,
  type PackMeta,
  pickPackFile,
  type RegisteredLevel,
  registeredLevels,
} from './game/assets.js';
import { App, type Progress } from './game/app.js';
import { intentToToken, keyToToken, KonamiCode } from './game/konami.js';
import { MODE_ORDER, type ModeName } from './game/lingo/gameplay.js';
import { focusTargets, padPress, stepFocus } from './game/ui/screens.js';
import { onPlayIntent } from './napplet-intent.js';
import { registerDrivingKeyActions } from './napplet-keys.js';
import './styles.css';

// App-owned colours: the game keeps its own palette in every host theme.
export const FOLLOW_HOST_THEME = false;

const PROGRESS_KEY = 'ssrc.unlocked.v1';

function requireElement<T extends HTMLElement>(selector: string): T {
  const element = document.querySelector<T>(selector);
  if (!element) throw new Error(`Missing required element: ${selector}`);
  return element;
}

/** Unlocks persist through the host's storage domain; without it they last for the session (as in the original). */
const progress: Progress | null = runtimeHasDomain(STORAGE_DOMAIN)
  ? {
      async load() {
        try {
          const raw = await storage.getItem(PROGRESS_KEY);
          const list = raw ? (JSON.parse(raw) as unknown) : [];
          return Array.isArray(list) ? MODE_ORDER.filter((m) => list.includes(m)) : [];
        } catch {
          return [];
        }
      },
      save(unlocked: ModeName[]) {
        void storage.setItem(PROGRESS_KEY, JSON.stringify(unlocked)).catch(() => {});
      },
    }
  : null;

const stage = requireElement<HTMLDivElement>('#stage');
const status = requireElement<HTMLOutputElement>('#status');
const badge = requireElement<HTMLSpanElement>('#assetBadge');
const panel = requireElement<HTMLElement>('#packPanel');
const panelTitle = requireElement<HTMLHeadingElement>('#packTitle');
const panelMessage = requireElement<HTMLParagraphElement>('#packMessage');
const linkForm = requireElement<HTMLDivElement>('#packLinkForm');
const linkInput = requireElement<HTMLInputElement>('#packLink');
const linkButton = requireElement<HTMLButtonElement>('#packLinkLoad');
const retryButton = requireElement<HTMLButtonElement>('#packRetry');
const fileButton = requireElement<HTMLButtonElement>('#packFile');
const cancelButton = requireElement<HTMLButtonElement>('#packCancel');
const levelsBox = requireElement<HTMLDivElement>('#packLevels');
const levelList = requireElement<HTMLDivElement>('#packLevelList');

const errorText = (error: unknown) => (error instanceof Error ? error.message : String(error));

// The pack panel: a full-frame card that gets an asset pack from the player,
// at startup (when the build's own pack is unavailable) or from "Asset pack…"
// on the title screen. One set of controls; `panelSession` says what the open
// panel does with them.
interface PanelSession {
  title: string;
  message: string;
  error: boolean;
  /** Called with the chosen pack's bytes. Throw to show the error in the panel. */
  take(buffer: ArrayBuffer): Promise<void> | void;
  retry?: () => void;
  cancel?: () => void;
  /** Offer the registered extra levels and the pack's own track. */
  levels?: (choice: RegisteredLevel | null) => void;
}
let panelSession: PanelSession | null = null;
const currentPackTitle = () => installedPack()?.title ?? 'Default';

/** The registered level being played: its id, null for the pack's own track, '' for any other level file. */
let playingLevel: string | null = null;

function renderLevels(session: PanelSession): void {
  const levels = session.levels ? registeredLevels() : [];
  levelsBox.hidden = levels.length === 0;
  levelList.replaceChildren();
  if (levelsBox.hidden) return;
  const choices: Array<RegisteredLevel | null> = [null, ...levels];
  for (const level of choices) {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'pack-button';
    button.textContent = level ? level.title : `${currentPackTitle()} track`;
    button.setAttribute('aria-pressed', String((level?.id ?? null) === playingLevel));
    button.addEventListener('click', () => session.levels?.(level));
    levelList.append(button);
  }
}

function showPanel(session: PanelSession): void {
  panelSession = session;
  panel.hidden = false;
  status.hidden = true;
  panelTitle.textContent = session.title;
  panelMessage.dataset.state = session.error ? 'error' : '';
  panelMessage.textContent = session.message;
  linkForm.hidden = !canLoadPackLink();
  fileButton.hidden = !canSideLoadPack();
  // Next to the link field the file picker is the secondary choice.
  fileButton.textContent = linkForm.hidden ? 'Load asset pack…' : 'Choose file…';
  fileButton.classList.toggle('primary', linkForm.hidden);
  retryButton.hidden = !session.retry;
  cancelButton.hidden = !session.cancel;
  renderLevels(session);
  setPanelBusy(false);
  (!linkForm.hidden ? linkInput : !fileButton.hidden ? fileButton : retryButton).focus();
}

function hidePanel(): void {
  panelSession = null;
  if (panel.contains(document.activeElement)) (document.activeElement as HTMLElement).blur();
  panel.hidden = true;
}

function setPanelBusy(busy: boolean): void {
  for (const control of [linkInput, linkButton, retryButton, fileButton, cancelButton, ...levelList.querySelectorAll('button')]) control.disabled = busy;
}

/** Get a pack's bytes from `load` and hand them to the open session; errors stay in the panel. */
async function panelLoad(load: () => Promise<ArrayBuffer | null>, busyText: string): Promise<void> {
  const session = panelSession;
  if (!session) return;
  setPanelBusy(true);
  panelMessage.dataset.state = '';
  panelMessage.textContent = busyText;
  try {
    const buffer = await load();
    if (buffer) await session.take(buffer);
    else if (panelSession === session) showPanel(session);
  } catch (error) {
    if (panelSession === session) showPanel({ ...session, message: errorText(error), error: true });
  }
}

/** Download a linked pack into the open panel's session. */
function loadLinkInPanel(text: string): void {
  linkInput.value = text;
  void panelLoad(() => fetchPackLink(text), 'Downloading the asset pack…');
}

// Not a <form>: the sandbox has no allow-forms, so a submit would be blocked.
const loadLink = () => loadLinkInPanel(linkInput.value);
linkButton.addEventListener('click', loadLink);
linkInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') loadLink();
});
fileButton.addEventListener('click', () => void panelLoad(pickPackFile, 'Reading the asset pack…'));
retryButton.addEventListener('click', () => panelSession?.retry?.());
cancelButton.addEventListener('click', () => panelSession?.cancel?.());
panel.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && panelSession?.cancel && !cancelButton.disabled) {
    e.stopPropagation();
    panelSession.cancel();
  }
});

/**
 * Get an asset pack: the build's own (embedded, or downloaded from Blossom),
 * else whatever the player links or side-loads. Resolves once one is installed.
 */
function acquirePack(): Promise<PackMeta> {
  return new Promise((resolve) => {
    const offer = (message: string) => {
      const link = canLoadPackLink();
      const sideLoad = canSideLoadPack();
      const alternatives = [link && 'paste a Blossom link or hash of an asset pack', sideLoad && 'load an asset pack file (.ssrcpack)'].filter(Boolean);
      showPanel({
        title: 'Game data',
        message:
          !canDownloadPack() && !link && !sideLoad
            ? 'This host cannot download or open the game data. Open SuperSonic RC in a napplet host with resource or file access.'
            : message + (alternatives.length ? ` You can ${alternatives.join(' or ')} instead.` : ''),
        error: true,
        take: (buffer) => {
          const meta = installPack(buffer);
          hidePanel();
          resolve(meta);
        },
        retry: canDownloadPack() ? () => void tryBuild() : undefined,
      });
      // A play intent that arrived during startup brings its own pack.
      if (pendingPlay !== null) {
        const link = pendingPlay;
        pendingPlay = null;
        loadLinkInPanel(link);
      }
    };
    const tryBuild = async () => {
      panel.hidden = PACK_EMBEDDED;
      panelTitle.textContent = 'Game data';
      panelMessage.dataset.state = '';
      panelMessage.textContent = 'Downloading the game data…';
      linkForm.hidden = retryButton.hidden = fileButton.hidden = cancelButton.hidden = true;
      try {
        resolve(await loadBuildPack());
      } catch (error) {
        offer(errorText(error));
      }
    };
    void tryBuild();
  });
}

const app = new App(stage, progress);
// Controllers in the pack panel: move between its controls, A presses one
// (the link field only loads a link already pasted; a controller cannot type),
// B cancels like Escape. While the panel is up the screens behind it get nothing.
app.menuOverride = (intent) => {
  if (panel.hidden) return false;
  const session = panelSession;
  if (!session) return true;
  const targets = focusTargets(panel, 'button, input');
  const focused = targets.find((t) => t === document.activeElement);
  if (intent === 'back') session.cancel?.();
  else if (intent !== 'select' && intent !== 'start') stepFocus(targets, intent);
  else if (focused === linkInput) {
    if (linkInput.value.trim()) loadLink();
    else stepFocus(targets, 'down');
  } else if (focused) padPress(focused);
  else stepFocus(targets, 'down');
  return true;
};
const closeKeyActions = registerDrivingKeyActions((key) => app.triggerKeyAction(key));
window.addEventListener('pagehide', closeKeyActions, { once: true });
app.onStatus = (text) => {
  status.textContent = text;
  status.hidden = text === '';
  status.dataset.state = '';
};

/** A short status line that clears itself (pack switch results). */
let noticeTimer: ReturnType<typeof setTimeout> | undefined;
function notice(text: string, error = false): void {
  clearTimeout(noticeTimer);
  app.onStatus(text);
  status.dataset.state = error ? 'error' : '';
  noticeTimer = setTimeout(() => app.onStatus(''), error ? 8000 : 4000);
}

/**
 * Replace the running game's pack with `buffer` (an asset pack, or a level
 * .glb played with the current pack) and return to the title screen. Throws
 * synchronously, changing nothing, if it is neither; one that fails to build
 * is dropped and the previous pack or level reinstalled.
 */
function switchPack(buffer: ArrayBuffer, levelId = ''): Promise<void> {
  const previousPack = currentPackBuffer();
  const previousLevel = currentLevelOverlay();
  const previousId = playingLevel;
  const level = isGlb(buffer);
  const meta = level ? installLevelOverlay(buffer)! : installPack(buffer);
  playingLevel = level ? levelId : null;
  const restore = () => {
    if (!level && previousPack) installPack(previousPack);
    installLevelOverlay(previousLevel);
    playingLevel = previousId;
  };
  return rebuildWithPack(meta, level ? 'level' : 'asset pack', restore);
}

/** Back to the installed pack's own track (drop the level overlay). */
function switchToPackLevel(): Promise<void> {
  const previousLevel = currentLevelOverlay();
  const previousId = playingLevel;
  installLevelOverlay(null);
  playingLevel = null;
  return rebuildWithPack({ title: `${currentPackTitle()} track`, original: installedPack()?.original === true }, 'level', () => {
    installLevelOverlay(previousLevel);
    playingLevel = previousId;
  });
}

async function rebuildWithPack(meta: PackMeta, what: string, restore: () => void): Promise<void> {
  try {
    await app.reloadAssets();
  } catch (error) {
    restore();
    app.showTitle();
    notice(`Could not switch to that ${what}: ${errorText(error)}`, true);
    return;
  }
  badge.hidden = !meta.original;
  notice(`Loaded ${what}: ${meta.title}`);
}

/** The registered level whose download the open panel is taking (so the switch records it). */
let pickedLevel: RegisteredLevel | null = null;

// Switching packs: "Asset pack…" on the title screen opens the pack panel to
// load another .ssrcpack (a pasted Blossom link or hash through the host's
// resource domain, or a file through its file picker) in place of the current
// one. A file that is not a pack stays in the panel as an error; a pack that
// fails to build is dropped and the previous one reinstalled. The panel also
// lists the registered extra levels and the pack's own track.
if (canLoadPackLink() || canSideLoadPack()) {
  app.packSwitch = () =>
    new Promise<void>((done) => {
      const current = currentPack();
      showPanel({
        title: 'Asset pack',
        message: `${current ? `Playing with “${current.title}”. ` : ''}${registeredLevels().length ? 'Pick a level, or load' : 'Load'} another asset pack or level: ${
          canLoadPackLink() ? `paste its Blossom link or hash${canSideLoadPack() ? ', or choose a .ssrcpack or .glb file' : ''}` : 'choose a .ssrcpack or .glb file'
        }.`,
        error: false,
        take: async (buffer) => {
          // Not a pack: installPack throws here, and the panel shows the error.
          const switching = switchPack(buffer, pickedLevel?.id ?? '');
          pickedLevel = null;
          hidePanel();
          done();
          linkInput.value = '';
          await switching;
        },
        levels: (level) => {
          if (!level) {
            if (playingLevel === null) return;
            hidePanel();
            done();
            void switchToPackLevel();
            return;
          }
          if (level.id === playingLevel) return;
          pickedLevel = level;
          void panelLoad(() => fetchRegisteredLevel(level), `Downloading “${level.title}”…`).finally(() => {
            pickedLevel = null;
          });
        },
        cancel: () => {
          hidePanel();
          done();
        },
      });
    });
}

// NAP-INTENT play (see napplet-intent.ts): another napplet asks to play with
// the asset pack it links. An open pack panel takes the pack as if pasted;
// during startup the link waits for the game (or the startup panel, if the
// build's own pack fails); during a race it waits for the race to end.
let started = false;
let pendingPlay: string | null = null;
let playQueue = Promise.resolve();

function receivePlay(link: string): void {
  if (panelSession) return loadLinkInPanel(link);
  if (!started) {
    pendingPlay = link;
    return;
  }
  playQueue = playQueue.then(() => playLinkedPack(link));
}

async function playLinkedPack(link: string): Promise<void> {
  // The pack panel may have opened while an earlier request was handled.
  if (panelSession) return loadLinkInPanel(link);
  app.onStatus('Downloading the linked asset pack…');
  try {
    const buffer = await fetchPackLink(link);
    if (app.racing) {
      notice('Finish the race to play with the linked asset pack.');
      await app.whenIdle();
    }
    await switchPack(buffer);
  } catch (error) {
    notice(`Could not play that link: ${errorText(error)}`, true);
  }
}

const closePlayIntents = onPlayIntent(receivePlay, () => notice('A request to play had no usable asset pack or level link.', true));
window.addEventListener('pagehide', closePlayIntents, { once: true });

// The secret (↑ ↑ ↓ ↓ ← → ← → B A), enterable on any menu screen with the
// keyboard or a controller: swap in the original 2004 assets, downloaded by
// hash from the Blossom servers that carry them (fetchOriginalPack). Keyboard
// entry reads raw key codes — KeyA is "left" to the game — and the controller
// side consumes only the completing A, so it does not also press the focused
// menu button.
const konami = new KonamiCode();
let loadingOriginal = false;

/** Feed one code token; true when it completed the code. */
function feedKonami(token: ReturnType<typeof keyToToken>): boolean {
  return token !== undefined && konami.feed(token);
}

/** What completing the code does: load the original-assets pack, like a linked pack. */
async function loadOriginalPack(): Promise<void> {
  if (loadingOriginal) return;
  if (installedPack()?.original) {
    notice('Already playing the original assets.');
    return;
  }
  if (!canLoadPackLink()) {
    notice('This host cannot download the original assets.', true);
    return;
  }
  loadingOriginal = true;
  notice('Loading the original assets…');
  try {
    const buffer = await fetchOriginalPack();
    if (app.racing) {
      notice('Finish the race to play with the original assets.');
      await app.whenIdle();
    }
    await switchPack(buffer);
  } catch (error) {
    notice(`Could not load the original assets: ${errorText(error)}`, true);
  } finally {
    loadingOriginal = false;
  }
}

// Menus only: not during startup, a race or an open pack panel.
app.menuIntentFilter = (intent) => {
  if (!started || app.racing) return false;
  if (!feedKonami(intentToToken(intent))) return false;
  void loadOriginalPack();
  return true;
};

window.addEventListener('keydown', (e) => {
  if (e.repeat || !started || app.racing || !panel.hidden) return;
  // Typing in a text field (the pack link) is not code entry.
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
  if (feedKonami(keyToToken(e.code))) void loadOriginalPack();
});

acquirePack()
  .then((pack) => {
    panel.hidden = true;
    badge.hidden = !pack.original;
    return app.start();
  })
  .then(() => {
    started = true;
    if (pendingPlay !== null) {
      const link = pendingPlay;
      pendingPlay = null;
      receivePlay(link);
    }
  })
  .catch((error: unknown) => {
    status.hidden = false;
    status.textContent = errorText(error);
    status.dataset.state = 'error';
  });