Text fields (`input`) with InputValue classes games can extend, continue/break in screen loops, Confirm, CaptureFocus/ClearFocus/InvertSelected, generator functions and iter/next, starred tuple items, renpy.file and json over data files loaded before the game starts, renpy.Container and register_sl_displayable, DiamondBox layout, ConditionSwitch/DynamicImage/Animation, renpy.show(what=...), callable transforms (`store.move_in(0.3)`), call_in_new_context as a call, im.matrix/im.Image/ MatrixColor, colorsys, fnmatch, FontGroup, persistent._clear and small renpy helpers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
63 lines
3.1 KiB
TypeScript
63 lines
3.1 KiB
TypeScript
import "./player.css";
|
|
import { Engine, type SaveApi } from "./engine";
|
|
import { loadTranslator, preferredLanguage } from "./i18n";
|
|
import { RUNTIMES } from "./runtimes";
|
|
import { cloudActive, makeSaveApi } from "./saves";
|
|
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, cloud: boolean): SaveApi<T> {
|
|
return makeSaveApi<T>(versionId, cloud);
|
|
}
|
|
|
|
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),
|
|
]);
|
|
// files the game reads while it starts (`renpy.file(...)`) have to be here before its first line runs
|
|
if (game.dataFiles?.length) {
|
|
const texts = await Promise.all(game.dataFiles.map((path) =>
|
|
fetch(blob(manifest, `data/${path}`), { credentials: "same-origin" }).then((r) => (r.ok ? r.text() : ""))));
|
|
game.dataText = Object.fromEntries(game.dataFiles.map((path, i) => [path.toLowerCase(), texts[i]]));
|
|
}
|
|
const cloud = await cloudActive();
|
|
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: (id) => saveApi(id, cloud) });
|
|
}
|
|
// volume belongs to the game, not to one of its versions (forks and mods share it)
|
|
const gameId = await getJSON<{ game_id: number }>(`/api/versions/${versionId}`).then((v) => v.game_id, () => null);
|
|
const engine = new Engine(root, game, manifest, modules, saveApi(versionId, cloud),
|
|
`pgvn:persistent:${versionId}`, onExit, tr,
|
|
(code) => loadTranslator(game, manifest, code),
|
|
gameId === null ? `pgvn:audio:version:${versionId}` : `pgvn:audio:game:${gameId}`);
|
|
const at = url.get("at");
|
|
if (at) engine.startAt(at);
|
|
else engine.start();
|
|
return engine;
|
|
}
|