feat: add local image enhancement and lossless mod assets
This commit is contained in:
1 parent
cc71050362
commit
f7f7f5b1b3
29 files changed
+1495
-10
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
import { AudioSystem } from "./audio";
|
||||
import { VolumeStore } from "./volume-store";
|
||||
import { LiveImages } from "../imager/live";
|
||||
import { BlockingCall, JumpSignal, PyDict, Python, decodeValue, encodeValue, pyStr, truthy, userBuiltins, type Scope } from "./python";
|
||||
import { Renderer, type ResolvedImage } from "./renderer";
|
||||
import { fastPoint, interpolate, parseText, plainText, renderText, waitPoints } from "./text";
|
||||
@@ -24,6 +25,8 @@ interface LabelRef {
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
private liveImages?: LiveImages;
|
||||
private imageScales = new Map<string, number>();
|
||||
private labels = new Map<string, LabelRef>();
|
||||
private anchors = new Map<string, { label: string; path: number[]; params?: [string, Expr | null][] | null }>();
|
||||
private imagesByTag = new Map<string, { attrs: string[]; key: string }[]>();
|
||||
@@ -136,6 +139,7 @@ export class Engine {
|
||||
root.append(this.container);
|
||||
this.renderer = new Renderer(this.container, w, h, {
|
||||
url: (f) => this.fileUrl(f),
|
||||
assetScale: (url) => this.imageScales.get(url) ?? 1,
|
||||
resolveRef: (name) => this.resolveImage(name.split(" "))?.def ?? null,
|
||||
evalCond: (c) => truthy(this.safeEval(c)),
|
||||
transform: (name) => game.transforms[name] ?? null,
|
||||
@@ -153,6 +157,7 @@ export class Engine {
|
||||
});
|
||||
this.audio = new AudioSystem(this.settings, (f) => this.fileUrl(f), game.channels ?? {});
|
||||
this.buildUI();
|
||||
if (manifest.versionId !== undefined) this.liveImages = new LiveImages(manifest.versionId, (message) => this.notify(`Улучшение графики: ${message}`));
|
||||
this.resizeObserver = new ResizeObserver(() => this.renderer.fit(this.container));
|
||||
this.resizeObserver.observe(this.container);
|
||||
this.renderer.fit(this.container);
|
||||
@@ -161,6 +166,7 @@ export class Engine {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.liveImages?.destroy();
|
||||
this.screens.destroy();
|
||||
this.gen++;
|
||||
this.wakeAll();
|
||||
@@ -184,7 +190,13 @@ export class Engine {
|
||||
}
|
||||
}
|
||||
const entry = logical ? this.manifest.files[logical] : undefined;
|
||||
return entry ? `/api/versions/${this.manifest.versionId}/files/${entry.hash}` : null;
|
||||
if (!entry || !logical) return null;
|
||||
const source = `/api/versions/${this.manifest.versionId}/files/${entry.hash}`;
|
||||
const improved = this.liveImages?.url(logical, entry.hash, source);
|
||||
const url = improved || source;
|
||||
const baseScale = this.game.assetScale?.[logical] ?? 1;
|
||||
this.imageScales.set(url, baseScale * (improved ? this.liveImages!.scale(improved) : 1));
|
||||
return url;
|
||||
}
|
||||
|
||||
private resolveImage(name: string[]): ResolvedImage | null {
|
||||
@@ -698,10 +710,7 @@ export class Engine {
|
||||
if (resolved) return this.renderer.displayable(resolved.def);
|
||||
const url = this.fileUrl(value);
|
||||
if (!url) return null;
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.draggable = false;
|
||||
return img;
|
||||
return this.renderer.displayable({ file: value });
|
||||
}
|
||||
|
||||
private screenFont(file: string): string | null {
|
||||
@@ -2008,6 +2017,21 @@ export class Engine {
|
||||
row(reading, "Скорость текста", 0, 150, 5, s.cps, (v) => (v === 0 ? "Мгновенно" : `${v} симв/с`), (v) => (s.cps = v), "reading");
|
||||
row(reading, "Задержка авто", 0.3, 4, 0.1, s.autoDelay, (v) => `${v.toFixed(1)} с`, (v) => (s.autoDelay = v), "reading");
|
||||
const display = section("Игра", "Изменения сохраняются автоматически в этом браузере.");
|
||||
const graphics = section("Графика", "Локальный генератор настраивается в меню профиля → Улучшение графики. Результат подхватывается при следующем показе картинки.");
|
||||
const imageToggle = (label: string, checked: boolean, apply: (value: boolean) => void) => {
|
||||
const row = el("label", "pg-check"); const box = el("input");
|
||||
Object.assign(box, { type: "checkbox", checked });
|
||||
box.onchange = () => apply(box.checked); row.append(box, el("span", "", label)); graphics.append(row);
|
||||
};
|
||||
imageToggle("Показывать улучшенные изображения", this.liveImages?.enabled ?? false, (value) => {
|
||||
if (!this.liveImages) return;
|
||||
this.liveImages.enabled = value;
|
||||
this.liveImages.save();
|
||||
// Refresh shown scene for immediate original/result comparison.
|
||||
const shown = this.renderer.snapshot();
|
||||
for (const image of shown) { const resolved = this.resolveImage(image.name); if (resolved) this.renderer.show(resolved, image.tag, image.at, { zorder: image.zorder }); }
|
||||
});
|
||||
imageToggle("Улучшать по ходу игры", this.liveImages?.automatic ?? false, (value) => { if (this.liveImages) { this.liveImages.automatic = value; this.liveImages.save(); } });
|
||||
const langs = this.languages();
|
||||
if (langs.length > 1) {
|
||||
const r = el("label", "pg-setting");
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface ResolvedImage {
|
||||
|
||||
export interface RendererHost {
|
||||
url(file: string): string | null;
|
||||
assetScale?(url: string): number;
|
||||
resolveRef(name: string): ImageDef | null;
|
||||
evalCond(e: any): boolean;
|
||||
transform(name: string): Record<string, any> | null;
|
||||
@@ -204,12 +205,18 @@ export class Renderer {
|
||||
img.draggable = false;
|
||||
img.decoding = "async";
|
||||
const src = this.host.url(def.file);
|
||||
const scale = src ? this.host.assetScale?.(src) ?? 1 : 1;
|
||||
if (Number.isFinite(scale) && scale > 0 && scale !== 1) {
|
||||
img.addEventListener("load", () => {
|
||||
// HTML width supplies a natural-size fallback. Explicit CSS size/ATL still wins,
|
||||
// and the unspecified side continues to follow the picture's aspect ratio.
|
||||
if (!img.style.width && !img.style.height) img.width = Math.max(1, Math.round(img.naturalWidth / scale));
|
||||
}, { once: true });
|
||||
}
|
||||
const size = def.props?.size ?? def.size;
|
||||
if (size) { img.style.width = `${size[0]}px`; img.style.height = `${size[1]}px`; }
|
||||
if (src) img.src = src;
|
||||
else console.warn("[pgvn] нет файла", def.file);
|
||||
if (def.props?.size) {
|
||||
img.style.width = `${def.props.size[0]}px`;
|
||||
img.style.height = `${def.props.size[1]}px`;
|
||||
}
|
||||
return img;
|
||||
}
|
||||
return document.createElement("div");
|
||||
|
||||
@@ -100,6 +100,8 @@ export interface ScriptModule {
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
/** Natural image dimensions are divided by this factor after an asset replacement. */
|
||||
assetScale?: Record<string, number>;
|
||||
runtime?: string;
|
||||
tomie?: Omit<import("./tomie-player").TomieContent, "scripts" | "resources">;
|
||||
engine?: string;
|
||||
|
||||
Reference in new issue
Block a user