feat: render API shim - a game's renpy.Displayable subclasses are drawn and receive events
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:
1 parent
6d78b8b847
commit
3f51f06fec
8 files changed
+354
-3
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { makeEngine } from "./helpers/renpy-engine.mjs";
|
||||
@@ -167,3 +168,18 @@ test("hashlib, re.fullmatch, type/id/vars, dict spread and small renpy helpers",
|
||||
assert.equal(typeof b.LiveComposite, "function");
|
||||
} finally { engine.destroy(); }
|
||||
});
|
||||
|
||||
test("a game's renpy.Displayable subclass renders a tree of pieces through renpy.Render", () => {
|
||||
const { engine } = makeEngine(runtime());
|
||||
try {
|
||||
engine.newStore();
|
||||
engine.py.exec(JSON.parse(readFileSync(new URL("./fixtures/displayable.ir.json", import.meta.url), "utf8")));
|
||||
const meter = engine.store.meter;
|
||||
const tree = engine.screens.renderHost.renderOf(meter, 200, 100, 1.5, 1.5);
|
||||
assert.deepEqual(tree.get_size(), [200, 20]);
|
||||
assert.equal(tree.pieces.length, 3); // a fill, drawing commands and the picture
|
||||
assert.equal(tree.pieces[0].fill, "#fff");
|
||||
assert.equal(tree.pieces[1].commands[0].kind, "circle");
|
||||
assert.deepEqual([tree.pieces[2].x, tree.pieces[2].y], [3, 4]);
|
||||
} finally { engine.destroy(); }
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
[{"op":"class","name":"Meter","bases":[{"k":"attr","o":{"k":"name","id":"renpy"},"a":"Displayable"}],"body":[{"op":"def","name":"__init__","params":[{"n":"self","d":null,"kind":"p"},{"n":"value","d":null,"kind":"p"},{"n":"kwargs","d":null,"kind":"**"}],"body":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"call","f":{"k":"name","id":"super"},"a":[{"k":"name","id":"Meter"},{"k":"name","id":"self"}],"kw":{}},"a":"__init__"},"a":[],"kw":{},"ks":[{"k":"name","id":"kwargs"}]}},{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"value"},"v":{"k":"name","id":"value"}},{"op":"set","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"moves"},"v":{"k":"const","v":0}}],"decos":[]},{"op":"def","name":"render","params":[{"n":"self","d":null,"kind":"p"},{"n":"width","d":null,"kind":"p"},{"n":"height","d":null,"kind":"p"},{"n":"st","d":null,"kind":"p"},{"n":"at","d":null,"kind":"p"}],"body":[{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"renpy"},"a":"redraw"},"a":[{"k":"name","id":"self"},{"k":"const","v":0.1}],"kw":{}}},{"op":"set","t":{"k":"name","id":"rv"},"v":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"renpy"},"a":"Render"},"a":[{"k":"name","id":"width"},{"k":"const","v":20}],"kw":{}}},{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"rv"},"a":"fill"},"a":[{"k":"const","v":"#fff"},{"k":"tuple","v":[{"k":"const","v":0},{"k":"const","v":0},{"k":"attr","o":{"k":"name","id":"self"},"a":"value"},{"k":"const","v":20}]}],"kw":{}}},{"op":"set","t":{"k":"name","id":"canvas"},"v":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"rv"},"a":"canvas"},"a":[],"kw":{}}},{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"canvas"},"a":"circle"},"a":[{"k":"const","v":"#f00"},{"k":"tuple","v":[{"k":"const","v":5},{"k":"const","v":5}]},{"k":"const","v":4}],"kw":{}}},{"op":"eval","e":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"rv"},"a":"blit"},"a":[{"k":"call","f":{"k":"attr","o":{"k":"name","id":"renpy"},"a":"render"},"a":[{"k":"const","v":"pic.png"},{"k":"name","id":"width"},{"k":"name","id":"height"},{"k":"name","id":"st"},{"k":"name","id":"at"}],"kw":{}},{"k":"tuple","v":[{"k":"const","v":3},{"k":"const","v":4}]}],"kw":{}}},{"op":"return","v":{"k":"name","id":"rv"}}],"decos":[]},{"op":"def","name":"event","params":[{"n":"self","d":null,"kind":"p"},{"n":"ev","d":null,"kind":"p"},{"n":"x","d":null,"kind":"p"},{"n":"y","d":null,"kind":"p"},{"n":"st","d":null,"kind":"p"}],"body":[{"op":"if","c":{"k":"cmp","l":{"k":"attr","o":{"k":"name","id":"ev"},"a":"type"},"ops":["=="],"r":[{"k":"attr","o":{"k":"name","id":"pygame"},"a":"MOUSEMOTION"}]},"then":[{"op":"aug","t":{"k":"attr","o":{"k":"name","id":"self"},"a":"moves"},"o":"+","v":{"k":"const","v":1}}],"else":[]},{"op":"raise","v":{"k":"call","f":{"k":"attr","o":{"k":"name","id":"renpy"},"a":"IgnoreEvent"},"a":[],"kw":{}}}],"decos":[]}]},{"op":"set","t":{"k":"name","id":"meter"},"v":{"k":"call","f":{"k":"name","id":"Meter"},"a":[{"k":"const","v":60}],"kw":{}}}]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
class Meter(renpy.Displayable):
|
||||
def __init__(self, value, **kwargs):
|
||||
super(Meter, self).__init__(**kwargs)
|
||||
self.value = value
|
||||
self.moves = 0
|
||||
|
||||
def render(self, width, height, st, at):
|
||||
renpy.redraw(self, 0.1)
|
||||
rv = renpy.Render(width, 20)
|
||||
rv.fill("#fff", (0, 0, self.value, 20))
|
||||
canvas = rv.canvas()
|
||||
canvas.circle("#f00", (5, 5), 4)
|
||||
rv.blit(renpy.render("pic.png", width, height, st, at), (3, 4))
|
||||
return rv
|
||||
|
||||
def event(self, ev, x, y, st):
|
||||
if ev.type == pygame.MOUSEMOTION:
|
||||
self.moves += 1
|
||||
raise renpy.IgnoreEvent()
|
||||
|
||||
|
||||
meter = Meter(60)
|
||||
Reference in new issue
Block a user