feat: ATL pictures/contains/function/time, colour matrices and more Python for Ren'Py

ATL
- Image blocks become timelines: picture steps with dissolve, parallel, contains,
  finite repeat, time, function f(trans, st, at), calls of other transforms with
  arguments, on hover/idle events for buttons. Animations stop when their element
  leaves the page
- Pictures defined by an expression (get_face_path("base")) are drawn at show time
- Purely graphical hints (shader, mesh, gl_*, u_*) are accepted and dropped

Colour
- matrixcolor and im.matrix.* as 5x5 matrices combined with `*`, applied through SVG
  filters; im.MatrixColor/Scale/Crop/Blur/Composite and Text/Image as values

Python
- match statement, re/copy/time/datetime/defaultdict/functools.partial, more
  str/dict methods, Character(...) created by code
- narrator("..."), Character calls, renpy.say and renpy.show/hide with expressions in
  Python blocks become script nodes
- Audit follows reachability: functions and classes nothing refers to (custom
  statement lexers, unused tools) no longer block an import

Docs: README and ARCHITECTURE now describe a multi-engine library (GameMaker and
QSP planned). Tests for the new runtime pieces.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 14:05:14 +03:00
1 parent 62ece25902
commit ea03cebf6a
21 files changed
+1264 -93

No files matched your search

+46 -1
View File
@@ -192,6 +192,8 @@ function percentFormat(fmt: string, args: any): string {
}
export function binop(op: string, a: any, b: any): any {
// objects (colour matrices) may define their own arithmetic
if (a && typeof a === "object" && typeof a.__binop__ === "function") return a.__binop__(op, b);
switch (op) {
case "+":
if (Array.isArray(a) && Array.isArray(b)) return [...a, ...b];
@@ -289,7 +291,13 @@ function methodOf(obj: any, name: string): any {
index: (x) => obj.findIndex((y) => pyEq(y, x)),
count: (x) => obj.filter((y) => pyEq(y, x)).length,
reverse: () => void obj.reverse(),
sort: () => void obj.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
sort: (...a: any[]) => {
const kw = a.length && a[a.length - 1]?.__kw ? a[a.length - 1].__kw : {};
const key = typeof kw.key === "function" ? kw.key : (x: any) => x;
const sorted = obj.map((x) => [key(x), x]).sort((p, q) => (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0)).map((p) => p[1]);
if (truthy(kw.reverse)) sorted.reverse();
obj.splice(0, obj.length, ...sorted);
},
clear: () => void obj.splice(0),
copy: () => [...obj],
add: (x) => void (contains(obj, x) || obj.push(x)), // sets are lists here
@@ -316,6 +324,10 @@ function methodOf(obj: any, name: string): any {
return obj.get(k);
},
update: (other: PyDict) => other.map.forEach((v, k) => obj.map.set(k, v)),
has_key: (k) => obj.has(k),
iteritems: () => [...obj.map.entries()],
iterkeys: () => [...obj.map.keys()],
itervalues: () => [...obj.map.values()],
clear: () => obj.map.clear(),
copy: () => new PyDict(obj.map.entries()),
};
@@ -341,6 +353,35 @@ function methodOf(obj: any, name: string): any {
},
count: (s) => obj.split(s).length - 1,
find: (s) => obj.indexOf(s),
zfill: (n: number) => obj.padStart(n, "0"),
ljust: (n: number, ch = " ") => obj.padEnd(n, ch),
rjust: (n: number, ch = " ") => obj.padStart(n, ch),
center: (n: number, ch = " ") => {
const total = Math.max(0, n - obj.length);
const left = Math.floor(total / 2);
return ch.repeat(left) + obj + ch.repeat(total - left);
},
partition: (sep: string) => {
const i = obj.indexOf(sep);
return i < 0 ? [obj, "", ""] : [obj.slice(0, i), sep, obj.slice(i + sep.length)];
},
rpartition: (sep: string) => {
const i = obj.lastIndexOf(sep);
return i < 0 ? ["", "", obj] : [obj.slice(0, i), sep, obj.slice(i + sep.length)];
},
rsplit: (sep?: string) => (sep ? obj.split(sep) : obj.trim().split(/\s+/)),
splitlines: () => obj.split(/\r?\n/),
isupper: () => obj !== obj.toLowerCase() && obj === obj.toUpperCase(),
islower: () => obj !== obj.toUpperCase() && obj === obj.toLowerCase(),
isspace: () => obj.length > 0 && /^\s+$/.test(obj),
isalnum: () => /^[\p{L}\p{N}]+$/u.test(obj),
swapcase: () => [...obj].map((c) => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())).join(""),
index: (x: string) => {
const i = obj.indexOf(x);
if (i < 0) throw new Error("substring not found");
return i;
},
rfind: (x: string) => obj.lastIndexOf(x),
isdigit: () => /^\d+$/.test(obj),
isalpha: () => /^\p{L}+$/u.test(obj),
};
@@ -412,12 +453,16 @@ export class Python {
const m = methodOf(o, e.a);
if (m) return m;
const v = o instanceof PyDict ? o.get(e.a) : o[e.a];
// methods of host objects (datetime, regex matches) need their object
if (typeof v === "function" && !(o instanceof PyDict) && !v.__py && Object.getPrototypeOf(o) !== Object.prototype)
return v.bind(o);
return v === undefined ? null : v;
}
case "sub": {
const o = this.eval(e.o);
const i = this.eval(e.i);
if (o instanceof PyDict) {
if (!o.has(i) && typeof (o as any).missing === "function") return (o as any).missing(i); // defaultdict
if (this.strict && !o.has(i)) this.raise("KeyError", pyStr(i));
return o.has(i) ? o.get(i) : null;
}