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:
1 parent
62ece25902
commit
ea03cebf6a
21 files changed
+1264
-93
No files matched your search
@@ -0,0 +1,182 @@
|
||||
// The small parts of the Python standard library games reach for: re, copy, time, datetime,
|
||||
// collections.defaultdict and functools.partial. Everything is pure computation.
|
||||
|
||||
import { PyDict, PyInstance, pyStr } from "./python";
|
||||
|
||||
/** dict that creates missing entries from a factory (collections.defaultdict). */
|
||||
export class PyDefaultDict extends PyDict {
|
||||
constructor(public factory: (() => any) | null, entries?: Iterable<[any, any]>) {
|
||||
super(entries);
|
||||
}
|
||||
missing(k: any) {
|
||||
const value = this.factory ? this.factory() : null;
|
||||
this.set(k, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class ReMatch {
|
||||
constructor(private m: RegExpExecArray) {}
|
||||
group(...which: (number | string)[]) {
|
||||
const pick = (w: number | string) => {
|
||||
const v = typeof w === "string" ? this.m.groups?.[w] : this.m[w];
|
||||
return v ?? null;
|
||||
};
|
||||
if (!which.length) return this.m[0];
|
||||
return which.length === 1 ? pick(which[0]) : which.map(pick);
|
||||
}
|
||||
groups() {
|
||||
return this.m.slice(1).map((v) => v ?? null);
|
||||
}
|
||||
groupdict() {
|
||||
return new PyDict(Object.entries(this.m.groups ?? {}));
|
||||
}
|
||||
start() {
|
||||
return this.m.index;
|
||||
}
|
||||
end() {
|
||||
return this.m.index + this.m[0].length;
|
||||
}
|
||||
}
|
||||
|
||||
const FLAGS: Record<number, string> = { 2: "i", 8: "m", 16: "s" };
|
||||
|
||||
function compile(pattern: string, flags = 0, global = false): RegExp {
|
||||
const js = pattern.replace(/\(\?P<(\w+)>/g, "(?<$1>").replace(/\(\?P=(\w+)\)/g, "\\k<$1>").replace(/\\Z/g, "$");
|
||||
const letters = Object.entries(FLAGS).filter(([bit]) => flags & Number(bit)).map(([, f]) => f).join("");
|
||||
return new RegExp(js, letters + (global ? "g" : "") + "u");
|
||||
}
|
||||
|
||||
function expand(replacement: string): string {
|
||||
// \1 and \g<name> are the group references of Python templates
|
||||
return replacement.replace(/\\(\d+)/g, "$$$1").replace(/\\g<(\w+)>/g, "$$<$1>");
|
||||
}
|
||||
|
||||
function deepcopy(v: any, seen = new Map<any, any>()): any {
|
||||
if (v === null || typeof v !== "object") return v;
|
||||
if (seen.has(v)) return seen.get(v);
|
||||
if (Array.isArray(v)) {
|
||||
const out: any[] = [];
|
||||
seen.set(v, out);
|
||||
v.forEach((x) => out.push(deepcopy(x, seen)));
|
||||
return out;
|
||||
}
|
||||
if (v instanceof PyDefaultDict) {
|
||||
const out = new PyDefaultDict(v.factory);
|
||||
seen.set(v, out);
|
||||
v.map.forEach((x, k) => out.map.set(k, deepcopy(x, seen)));
|
||||
return out;
|
||||
}
|
||||
if (v instanceof PyDict) {
|
||||
const out = new PyDict();
|
||||
seen.set(v, out);
|
||||
v.map.forEach((x, k) => out.map.set(k, deepcopy(x, seen)));
|
||||
return out;
|
||||
}
|
||||
if (v instanceof PyInstance) {
|
||||
const attrs: Record<string, any> = Object.create(null);
|
||||
const out = new PyInstance(v.cls, attrs);
|
||||
seen.set(v, out);
|
||||
for (const [k, x] of Object.entries(v.attrs)) attrs[k] = deepcopy(x, seen);
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const pad = (n: number, width = 2) => String(n).padStart(width, "0");
|
||||
|
||||
class DateTime {
|
||||
constructor(public d: Date) {}
|
||||
get year() { return this.d.getFullYear(); }
|
||||
get month() { return this.d.getMonth() + 1; }
|
||||
get day() { return this.d.getDate(); }
|
||||
get hour() { return this.d.getHours(); }
|
||||
get minute() { return this.d.getMinutes(); }
|
||||
get second() { return this.d.getSeconds(); }
|
||||
weekday() { return (this.d.getDay() + 6) % 7; }
|
||||
strftime(format: string) { return strftime(format, this.d); }
|
||||
isoformat() { return `${this.year}-${pad(this.month)}-${pad(this.day)}T${pad(this.hour)}:${pad(this.minute)}:${pad(this.second)}`; }
|
||||
}
|
||||
|
||||
function strftime(format: string, d: Date): string {
|
||||
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
return format.replace(/%([a-zA-Z%])/g, (_m, c: string) => {
|
||||
switch (c) {
|
||||
case "Y": return String(d.getFullYear());
|
||||
case "y": return pad(d.getFullYear() % 100);
|
||||
case "m": return pad(d.getMonth() + 1);
|
||||
case "d": return pad(d.getDate());
|
||||
case "H": return pad(d.getHours());
|
||||
case "I": return pad(d.getHours() % 12 || 12);
|
||||
case "M": return pad(d.getMinutes());
|
||||
case "S": return pad(d.getSeconds());
|
||||
case "p": return d.getHours() < 12 ? "AM" : "PM";
|
||||
case "A": return days[d.getDay()];
|
||||
case "a": return days[d.getDay()].slice(0, 3);
|
||||
case "B": return months[d.getMonth()];
|
||||
case "b": return months[d.getMonth()].slice(0, 3);
|
||||
case "%": return "%";
|
||||
default: return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const now = () => new DateTime(new Date());
|
||||
const datetimeClass = { now, today: now, utcnow: now };
|
||||
|
||||
/** Namespaces added to the sandbox's builtins. */
|
||||
export function pythonModules(): Record<string, any> {
|
||||
const re = {
|
||||
I: 2, IGNORECASE: 2, M: 8, MULTILINE: 8, S: 16, DOTALL: 16,
|
||||
escape: (s: string) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
||||
compile: (pattern: string, flags = 0) => ({
|
||||
search: (s: string) => re.search(pattern, s, flags),
|
||||
match: (s: string) => re.match(pattern, s, flags),
|
||||
sub: (r: string, s: string) => re.sub(pattern, r, s, 0, flags),
|
||||
findall: (s: string) => re.findall(pattern, s, flags),
|
||||
split: (s: string) => re.split(pattern, s, 0, flags),
|
||||
}),
|
||||
search: (pattern: string, s: string, flags = 0) => {
|
||||
const m = compile(pattern, flags).exec(String(s));
|
||||
return m ? new ReMatch(m) : null;
|
||||
},
|
||||
match: (pattern: string, s: string, flags = 0) => {
|
||||
const m = compile(`^(?:${pattern})`, flags).exec(String(s));
|
||||
return m ? new ReMatch(m) : null;
|
||||
},
|
||||
findall: (pattern: string, s: string, flags = 0) =>
|
||||
[...String(s).matchAll(compile(pattern, flags, true))].map((m) => (m.length > 2 ? m.slice(1) : m[1] ?? m[0])),
|
||||
sub: (pattern: string, replacement: any, s: string, count = 0, flags = 0) => {
|
||||
let left = count || Infinity;
|
||||
const regex = compile(pattern, flags, true);
|
||||
return String(s).replace(regex, (...args: any[]) => {
|
||||
if (left-- <= 0) return args[0];
|
||||
if (typeof replacement === "function") return pyStr(replacement(new ReMatch(Object.assign(args.slice(0, -2).filter((x) => typeof x !== "object"), { index: args[args.length - 2], groups: undefined, input: s }) as any)));
|
||||
return String(replacement).replace(/\\(\d+)/g, (_m: string, n: string) => args[Number(n)] ?? "");
|
||||
});
|
||||
},
|
||||
split: (pattern: string, s: string, maxsplit = 0, flags = 0) => {
|
||||
const parts = String(s).split(compile(pattern, flags, true));
|
||||
return maxsplit ? [...parts.slice(0, maxsplit), parts.slice(maxsplit).join("")] : parts;
|
||||
},
|
||||
};
|
||||
void expand;
|
||||
const partial = (f: any, ...bound: any[]) => (...args: any[]) => f(...bound, ...args);
|
||||
const functionModules = { partial };
|
||||
return {
|
||||
re,
|
||||
copy: { copy: (v: any) => (Array.isArray(v) ? [...v] : v instanceof PyDict ? new PyDict(v.map.entries()) : v), deepcopy: (v: any) => deepcopy(v) },
|
||||
time: {
|
||||
time: () => Date.now() / 1000,
|
||||
sleep: () => null,
|
||||
strftime: (format: string, t?: any) => strftime(format, t instanceof DateTime ? t.d : new Date()),
|
||||
},
|
||||
datetime: Object.assign({ datetime: datetimeClass, date: datetimeClass }, datetimeClass),
|
||||
collections: {
|
||||
defaultdict: (factory?: any) => new PyDefaultDict(typeof factory === "function" ? () => factory() : null),
|
||||
OrderedDict: () => new PyDict(),
|
||||
},
|
||||
functools: functionModules,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user