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
@@ -2,6 +2,7 @@
|
||||
// whenever game state may have changed. Expressions run in the same Python sandbox as the script.
|
||||
|
||||
import { AtlRunner, bindArguments, type AtlBlock } from "./atl";
|
||||
import { ColorMatrix, colorMatrices, legacyMatrices, matrixFilter } from "./colormatrix";
|
||||
import { PyDict, pyStr, truthy, type Python } from "./python";
|
||||
import type { AtSpec, Expr } from "./types";
|
||||
|
||||
@@ -35,6 +36,8 @@ export interface ScreenHost {
|
||||
mainMenu(): void;
|
||||
openMenu(): void;
|
||||
openUrl(url: string): void;
|
||||
quickSave(): void;
|
||||
quickLoad(): void;
|
||||
setLanguage(code: string | null): void;
|
||||
transform(name: string): Record<string, any> | null;
|
||||
atl(name: string): AtlBlock | null;
|
||||
@@ -660,6 +663,68 @@ export class ScreenManager {
|
||||
const result = typeof a === "function" ? a(0, 0) : null;
|
||||
return this.displayable(Array.isArray(result) ? result[0] : result, depth + 1);
|
||||
}
|
||||
case "text": {
|
||||
const box = document.createElement("div");
|
||||
const size = num(value.kw.size) ?? this.host.baseSize;
|
||||
this.host.text(box, pyStr(a), size);
|
||||
if (typeof value.kw.color === "string") box.style.color = value.kw.color;
|
||||
return box;
|
||||
}
|
||||
case "image": return this.displayable(a, depth + 1);
|
||||
case "matrixcolor": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (child && b instanceof ColorMatrix) child.style.filter = matrixFilter(b);
|
||||
return child;
|
||||
}
|
||||
case "scale": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (child && num(value.args[1]) !== null) {
|
||||
child.style.width = `${value.args[1]}px`;
|
||||
child.style.height = `${value.args[2]}px`;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
case "factorscale": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (child && num(b) !== null) {
|
||||
child.style.transformOrigin = "0 0";
|
||||
child.style.transform = `scale(${b}, ${value.args[2] ?? b})`;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
case "flip": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (child) child.style.transform = `scale(${truthy(value.kw.horizontal) ? -1 : 1}, ${truthy(value.kw.vertical) ? -1 : 1})`;
|
||||
return child;
|
||||
}
|
||||
case "filter": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (child) {
|
||||
const how = String(value.kw.how);
|
||||
if (how === "alpha") child.style.opacity = String(b ?? 1);
|
||||
else child.style.filter = how === "blur" ? `blur(${b ?? 0}px)` : `${how}(1)`;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
case "composite": {
|
||||
const box = document.createElement("div");
|
||||
const size = a;
|
||||
box.style.position = "relative";
|
||||
if (Array.isArray(size)) {
|
||||
box.style.width = `${size[0]}px`;
|
||||
box.style.height = `${size[1]}px`;
|
||||
}
|
||||
for (let i = 1; i + 1 < value.args.length; i += 2) {
|
||||
const child = this.displayable(value.args[i + 1], depth + 1);
|
||||
const at = value.args[i];
|
||||
if (!child || !Array.isArray(at)) continue;
|
||||
child.style.position = "absolute";
|
||||
child.style.left = `${at[0]}px`;
|
||||
child.style.top = `${at[1]}px`;
|
||||
box.append(child);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
case "transform": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
if (!child) return null;
|
||||
@@ -763,6 +828,10 @@ export class ScreenManager {
|
||||
const paint = () => {
|
||||
const state = !sensitive ? (selected ? "selected_insensitive" : "insensitive")
|
||||
: selected ? (hover ? "selected_hover" : "selected_idle") : hover ? "hover" : "idle";
|
||||
if (el.dataset.state && el.dataset.state !== state) {
|
||||
// `on hover:` / `on idle:` handlers of the button's transforms
|
||||
for (const entry of (el as any).__atl ?? []) entry.runner.event(entry.block, state);
|
||||
}
|
||||
el.dataset.state = state;
|
||||
if (images) {
|
||||
const file = images[state];
|
||||
@@ -867,6 +936,7 @@ export class ScreenManager {
|
||||
const style = el.style;
|
||||
const alpha = num(g("alpha"));
|
||||
if (alpha !== null) style.opacity = String(alpha);
|
||||
applyFilters(el, { blur: g("blur"), matrixcolor: g("matrixcolor") });
|
||||
// size
|
||||
let [width, height] = [g("xsize") ?? g("width"), g("ysize") ?? g("height")];
|
||||
const both = g("xysize");
|
||||
@@ -979,7 +1049,7 @@ export class ScreenManager {
|
||||
if (p.rotate) parts.push(`rotate(${p.rotate}deg)`);
|
||||
el.style.transform = parts.join(" ");
|
||||
if (p.alpha !== undefined) el.style.opacity = String(p.alpha);
|
||||
if (typeof p.blur === "number") el.style.filter = p.blur > 0 ? `blur(${p.blur}px)` : "";
|
||||
applyFilters(el, p);
|
||||
};
|
||||
for (const spec of specs) {
|
||||
Object.assign(live, spec.props ?? {}, spec.name ? this.host.transform(spec.name) ?? {} : {});
|
||||
@@ -1000,6 +1070,8 @@ export class ScreenManager {
|
||||
apply: (p) => apply(p),
|
||||
eval: (expr, frame) => this.evalWith(expr, frame),
|
||||
}, live, bindArguments(block, spec, (e) => this.ev(e)));
|
||||
runner.watch = el;
|
||||
((el as any).__atl ??= []).push({ runner, block });
|
||||
ctx.inst.runners.push(runner);
|
||||
const inst = ctx.inst;
|
||||
void runner.start(block).then((completed) => {
|
||||
@@ -1095,7 +1167,10 @@ export class ScreenManager {
|
||||
this.assign(a, truthy(now) && now !== off ? off : on, owner);
|
||||
return;
|
||||
}
|
||||
case "SetLocalVariable":
|
||||
case "SetScreenVariable": owner?.vars.set(a, b); return;
|
||||
case "QuickSave": this.host.quickSave(); return;
|
||||
case "QuickLoad": this.host.quickLoad(); throw new ActionAbort();
|
||||
case "ToggleScreenVariable": {
|
||||
const now = owner?.vars.get(a);
|
||||
const on = action.args.length > 1 ? b : true;
|
||||
@@ -1148,7 +1223,7 @@ export class ScreenManager {
|
||||
for (const kind of ["Jump", "Call", "Return", "SetVariable", "ToggleVariable", "SetDict", "SetField", "ToggleField",
|
||||
"SetScreenVariable", "ToggleScreenVariable", "Show", "Hide", "ShowTransient", "Function", "NullAction", "Play",
|
||||
"Queue", "Stop", "Notify", "Quit", "MainMenu", "SelectedIf", "SensitiveIf", "ToggleScreen", "Language", "ShowMenu",
|
||||
"OpenURL"]) out[kind] = make(kind);
|
||||
"OpenURL", "SetLocalVariable", "QuickSave", "QuickLoad"]) out[kind] = make(kind);
|
||||
const shape = (kind: string) => (...args: any[]) => {
|
||||
const last = args[args.length - 1];
|
||||
const hasKw = last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw");
|
||||
@@ -1162,6 +1237,27 @@ export class ScreenManager {
|
||||
out.Transform = shape("transform");
|
||||
out.DynamicDisplayable = shape("dynamic");
|
||||
out.Color = (value: any) => (typeof value === "string" ? value : pyStr(value));
|
||||
// Character(...) built by game code: kept as a record the say statement can read
|
||||
out.Character = (...args: any[]) => {
|
||||
const last = args[args.length - 1];
|
||||
const kw = last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw") ? last.__kw : {};
|
||||
return { __character: true, name: args.length && typeof args[0] === "string" ? args[0] : null, color: kw.color ?? null, kw };
|
||||
};
|
||||
out.Text = shape("text");
|
||||
out.Image = shape("image");
|
||||
out.Fixed = shape("composite");
|
||||
out.AlphaMask = (base: any) => base;
|
||||
out.At = (child: any) => child;
|
||||
Object.assign(out, colorMatrices);
|
||||
out.im = {
|
||||
MatrixColor: shape("matrixcolor"), Scale: shape("scale"), FactorScale: shape("factorscale"), Flip: shape("flip"),
|
||||
Crop: shape("crop"), Composite: shape("composite"),
|
||||
Grayscale: (child: any) => new Displayable("filter", [child], { how: "grayscale" }),
|
||||
Sepia: (child: any) => new Displayable("filter", [child], { how: "sepia" }),
|
||||
Alpha: (child: any, amount: number) => new Displayable("filter", [child, amount], { how: "alpha" }),
|
||||
Blur: (child: any, radius: number) => new Displayable("filter", [child, radius], { how: "blur" }),
|
||||
matrix: legacyMatrices,
|
||||
};
|
||||
out.If = (condition: any, yes: any = null, no: any = null) => (truthy(condition) ? yes : no);
|
||||
out.GetTooltip = () => this.tooltip;
|
||||
out.StaticValue = (value: number, range = 1) => new BarValue(() => value, () => range);
|
||||
@@ -1191,6 +1287,15 @@ export class ScreenManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** blur and matrixcolor of a widget or transform, combined into one CSS filter. */
|
||||
function applyFilters(el: HTMLElement, p: Props) {
|
||||
const filters: string[] = [];
|
||||
if (num(p.blur) !== null && p.blur > 0) filters.push(`blur(${p.blur}px)`);
|
||||
if (p.matrixcolor instanceof ColorMatrix) filters.push(matrixFilter(p.matrixcolor));
|
||||
if (filters.length) el.style.filter = filters.join(" ");
|
||||
else if (p.blur !== undefined || p.matrixcolor !== undefined) el.style.filter = "";
|
||||
}
|
||||
|
||||
function lengthOf(v: any): string {
|
||||
return typeof v === "number" ? PX_UNIT(v) : "0px";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user