Files
pgames-manager/frontend/src/player/index.ts
T
mrleo1nidandClaude Sonnet 5.5 12ec82c7e6 refactor: move game-specific import logic into profiles; unblock Tomie import
- Add app/games: GameProfile base, registry, ReaderHooks and renpy/naninovel/
  tomie profiles. pipeline, preflight, detect and mods dispatch via the registry
- Move tomie_* converter modules to app/games/tomie and split out reader hooks,
  import, mod validation and known defects
- Tolerate the 31 authoring defects of the pinned Tomie build as report
  warnings (exact list, per-script counts); any new defect still blocks import
- Frontend: runtime registry (runtimes.ts) with lazy Tomie launcher
- Add profile/registry tests and docs/GAME_PROFILES.md; update IMPORT.md

Full media export of the original Tomie build is not yet verified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:35:23 +03:00

72 lines
2.9 KiB
TypeScript

import "./player.css";
import { Engine, type SaveApi } from "./engine";
import { loadTranslator, preferredLanguage } from "./i18n";
import { RUNTIMES } from "./runtimes";
import type { GameData, Manifest, SaveData, ScriptModule } from "./types";
async function getJSON<T>(url: string): Promise<T> {
const r = await fetch(url, { credentials: "same-origin" });
if (!r.ok) throw new Error(`${r.status} ${url}`);
return r.json();
}
function blob(manifest: Manifest, path: string) {
const f = manifest.files[path];
if (!f) throw new Error(`в версии нет файла ${path}`);
return `/api/versions/${manifest.versionId}/files/${f.hash}`;
}
function saveApi<T = SaveData>(versionId: number): SaveApi<T> {
const base = `/api/versions/${versionId}/saves`;
return {
list: () => getJSON(base),
async get(slot) {
const r = await fetch(`${base}/${slot}`);
if (r.status === 404) return null;
if (!r.ok) throw new Error(`${r.status}`);
return (await r.json()).data as T;
},
async put(slot, title, data) {
const r = await fetch(`${base}/${slot}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, data }),
});
if (!r.ok) throw new Error(`${r.status}`);
},
async remove(slot) {
await fetch(`${base}/${slot}`, { method: "DELETE" });
},
};
}
export interface PlayerHandle { destroy(): void }
export async function launch(root: HTMLElement, versionId: number, onExit: () => void,
onProgress: (text: string) => void): Promise<PlayerHandle> {
onProgress("Загрузка манифеста");
const manifest = await getJSON<Manifest>(`/api/versions/${versionId}/manifest`);
const game = await getJSON<GameData>(blob(manifest, "game.json"));
onProgress(`Загрузка скриптов (${game.scripts.length})`);
// ?lang= and ?at= come from the translation editor ("play from here")
const url = new URLSearchParams(location.search);
const asked = url.get("lang");
const lang = asked && (asked === game.sourceLang || game.locales?.[asked]) ? asked : preferredLanguage(game);
const [modules, tr] = await Promise.all([
Promise.all(game.scripts.map((s) => getJSON<ScriptModule>(blob(manifest, s)))),
loadTranslator(game, manifest, lang),
]);
if (game.runtime) {
const load = RUNTIMES[game.runtime];
if (!load) throw new Error(`Неизвестная среда исполнения игры: ${game.runtime}`);
return (await load())({ root, game, manifest, modules, url, tr, onExit, versionId, saveApi });
}
const engine = new Engine(root, game, manifest, modules, saveApi(versionId),
`pgvn:persistent:${versionId}`, onExit, tr,
(code) => loadTranslator(game, manifest, code));
const at = url.get("at");
if (at) engine.startAt(at);
else engine.start();
return engine;
}