feat: render API shim - a game's renpy.Displayable subclasses are drawn and receive events
CI / frontend (push) Failing after 1m13s
CI / backend (push) Failing after 1m33s

Games write their own displayables with render(), renpy.Render, blit, fill, canvas() and renpy.redraw.
render.ts keeps a tree of placed pieces (child renders, ordinary displayables, fills and drawing
commands) and turns it into DOM elements that stay put between redraws; pointer events reach event()
as pygame-style objects, and a returned value ends the interaction. Shaders and GPU properties are
accepted and ignored. The converter whitelists the calls and methods and accepts `import pygame`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 15:20:35 +03:00
1 parent 6d78b8b847
commit 3f51f06fec
8 files changed
+354 -3

No files matched your search

+15
View File
@@ -8,6 +8,7 @@ import { isNativeAction } from "./musicroom";
import { galleryClass } from "./gallery";
import { parseExpression } from "./pyparse";
import { movieDisplayable } from "./movie";
import { RenderHost, isDisplayableInstance } from "./render";
import { strftime } from "./pymodules";
import { PyDict, PyInstance, callableOf, pyEq, pyStr, truthy, type Python } from "./python";
import type { AtSpec, Expr } from "./types";
@@ -49,6 +50,8 @@ export interface ScreenHost {
saveSlot(slot: string, confirm: boolean): void;
loadSlot(slot: string): void;
deleteSlot(slot: string, confirm: boolean): void;
/** Size of the game picture, for displayables that ask how much room they have. */
resolution?: [number, number];
/** The page of the save/load screen (`persistent._file_page`). */
filePage: string;
startReplay(label: string, scope: Record<string, any> | null): void;
@@ -147,6 +150,17 @@ export class ScreenManager {
window.addEventListener("pointercancel", this.onRelease, true);
}
/** Game displayables written against the render API (renpy.Displayable subclasses). */
private renderHostValue: RenderHost | null = null;
get renderHost(): RenderHost {
return (this.renderHostValue ??= new RenderHost({
py: this.host.py,
element: (value) => this.displayable(value),
area: () => this.host.resolution ?? [1920, 1080],
finish: (value) => this.interactionReturn?.(value),
}));
}
private pressed = false;
private refreshAfterPress = false;
private onPress = () => { this.pressed = true; };
@@ -912,6 +926,7 @@ export class ScreenManager {
/** An element for an image name/path or for a Crop/Solid/Frame/Transform built by game code. */
private displayable(value: any, depth = 0): HTMLElement | null {
if (depth > 8 || value === null || value === undefined) return null;
if (isDisplayableInstance(value)) return this.renderHost.host(value);
if (!(value instanceof Displayable)) return this.host.image(pyStr(value));
const [a, b] = value.args;
switch (value.kind) {