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>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 11:35:23 +03:00
1 parent bbbcc6a5d8
commit 12ec82c7e6
30 files changed
+700 -196

No files matched your search

+5 -32
View File
@@ -1,6 +1,7 @@
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> {
@@ -55,38 +56,10 @@ export async function launch(root: HTMLElement, versionId: number, onExit: () =>
Promise.all(game.scripts.map((s) => getJSON<ScriptModule>(blob(manifest, s)))),
loadTranslator(game, manifest, lang),
]);
if (game.runtime === "tomie/1") {
if (!game.tomie) throw new Error("Не найдены исходные настройки Tomie");
const { TomiePlayer } = await import("./tomie-player");
const scripts: import("./nani-runtime").NaniPackage["scripts"] = Object.create(null);
const resources: NonNullable<GameData["naniResources"]> = Object.create(null);
for (const module of modules) {
if (!module.nani) {
for (const node of module.labels.flatMap((label) => label.body)) {
const record = node.resource;
if (!record || typeof node.what !== "string" || typeof record.collection !== "string" || typeof record.key !== "string") continue;
const collection = resources[record.collection] ??= Object.create(null);
(collection[record.key] ??= []).push({ ...record, tid: node.tid, text: node.what });
}
continue;
}
const nodes = module.labels.flatMap((label) => label.body);
const commands = nodes.map((node) => {
const command = structuredClone(node.command);
if (node.textField && typeof node.what === "string") command.args[node.textField] = { template: node.what };
command.tid = node.tid ?? command.tid; return command;
});
const labels = module.nani.label_ids ? Object.fromEntries(Object.entries(module.nani.label_ids).map(([label, id]) => {
const index = id ? nodes.findIndex((node) => node.id === id) : nodes.length;
if (index < 0) throw new Error("Не найдена команда метки Naninovel: " + label);
return [label, index];
})) : module.nani.labels;
scripts[module.nani.name] = { commands, labels, label_depths: module.nani.label_depths };
}
if (url.get("at")) throw new Error("Предпросмотр Tomie с отдельной строки ещё не поддерживается. Откройте игру с начала.");
const player = new TomiePlayer(root, { ...game.tomie, scripts, resources }, manifest,
saveApi<import("./tomie-player").TomieSave>(versionId), tr, onExit);
try { player.start(); return player; } catch (error) { player.destroy(); throw error; }
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,