feat: drag boxes, transforms as values, Matrix() colours and offline renpy.fetch in the player
screens: `drag` with draggable True moves with the pointer (the place is kept while the screen is shown); `at icon["at"]` picks a transform that Python code stored in a dict (transform names now evaluate to a reference); PreviewSlowText and Move(...) exist as values. Python: Matrix([16 numbers]) for matrixcolor, renpy.has_live2d() is false, renpy.fetch fails like a missing network (games catch it), cache.get_total_size and the texture helpers are no-ops. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dcddd42aca
commit
353cd5e49e
4 files changed
+63
-4
No files matched your search
@@ -70,6 +70,13 @@ export function parseColor(value: any): [number, number, number, number] {
|
|||||||
|
|
||||||
export const colorMatrices = {
|
export const colorMatrices = {
|
||||||
IdentityMatrix: () => ColorMatrix.identity(),
|
IdentityMatrix: () => ColorMatrix.identity(),
|
||||||
|
/** `Matrix([16 numbers])`: Ren'Py's 4x4 acts on (r, g, b, a), as in its own BrightnessMatrix. */
|
||||||
|
Matrix: (values: any) => {
|
||||||
|
const flat = (Array.isArray(values) ? values : []).flat();
|
||||||
|
if (flat.length !== 16 || flat.some((v: any) => typeof v !== "number")) throw new Error("Matrix: нужны 16 чисел");
|
||||||
|
return new ColorMatrix([0, 1, 2, 3].map((i) => [flat[i * 4], flat[i * 4 + 1], flat[i * 4 + 2], flat[i * 4 + 3], 0] as Row)
|
||||||
|
.concat([[0, 0, 0, 0, 1] as Row]));
|
||||||
|
},
|
||||||
TintMatrix: (color: any) => {
|
TintMatrix: (color: any) => {
|
||||||
const [r, g, b, a] = parseColor(color);
|
const [r, g, b, a] = parseColor(color);
|
||||||
return diag(r, g, b, a);
|
return diag(r, g, b, a);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Renderer, type ResolvedImage } from "./renderer";
|
|||||||
import { fastPoint, interpolate, parseText, plainText, renderText, waitPoints } from "./text";
|
import { fastPoint, interpolate, parseText, plainText, renderText, waitPoints } from "./text";
|
||||||
import { languageName, rememberLanguage, type Translator } from "./i18n";
|
import { languageName, rememberLanguage, type Translator } from "./i18n";
|
||||||
import { naniText } from "./naninovel";
|
import { naniText } from "./naninovel";
|
||||||
import { Displayable, ScreenManager, type ScreenHost } from "./screens";
|
import { Displayable, ScreenManager, TransformRef, type ScreenHost } from "./screens";
|
||||||
import { musicRoomClass } from "./musicroom";
|
import { musicRoomClass } from "./musicroom";
|
||||||
import { pythonModules } from "./pymodules";
|
import { pythonModules } from "./pymodules";
|
||||||
import { parseExpression, parseStatements } from "./pyparse";
|
import { parseExpression, parseStatements } from "./pyparse";
|
||||||
@@ -125,8 +125,10 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
this.persistent = this.makePersistent();
|
this.persistent = this.makePersistent();
|
||||||
const scope: Scope = {
|
const scope: Scope = {
|
||||||
get: (n) => (Object.hasOwn(this.store, n) ? this.store[n] : this.builtins[n]),
|
// a transform named in Python code (`{"at": iconlori}`) is a value the screens can put in `at`
|
||||||
has: (n) => Object.hasOwn(this.store, n) || Object.hasOwn(this.builtins, n),
|
get: (n) => (Object.hasOwn(this.store, n) ? this.store[n]
|
||||||
|
: Object.hasOwn(this.builtins, n) ? this.builtins[n] : this.isTransform(n) ? new TransformRef(n) : this.builtins[n]),
|
||||||
|
has: (n) => Object.hasOwn(this.store, n) || Object.hasOwn(this.builtins, n) || this.isTransform(n),
|
||||||
set: (n, v) => void (this.store[n] = v),
|
set: (n, v) => void (this.store[n] = v),
|
||||||
del: (n) => void delete this.store[n],
|
del: (n) => void delete this.store[n],
|
||||||
};
|
};
|
||||||
@@ -181,6 +183,10 @@ export class Engine {
|
|||||||
if (box.width && box.height) this.mousePos = [Math.round((event.clientX - box.left) * w / box.width), Math.round((event.clientY - box.top) * h / box.height)];
|
if (box.width && box.height) this.mousePos = [Math.round((event.clientX - box.left) * w / box.width), Math.round((event.clientY - box.top) * h / box.height)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private isTransform(name: string): boolean {
|
||||||
|
return Boolean(this.game.transforms?.[name] || this.game.atl?.[name]);
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
window.removeEventListener("pointermove", this.onPointer);
|
window.removeEventListener("pointermove", this.onPointer);
|
||||||
this.liveImages?.destroy();
|
this.liveImages?.destroy();
|
||||||
@@ -450,6 +456,10 @@ export class Engine {
|
|||||||
has_screen: (name: any) => Boolean(this.game.screens?.[pyStr(name)]),
|
has_screen: (name: any) => Boolean(this.game.screens?.[pyStr(name)]),
|
||||||
list_files: () => Object.keys(this.game.files ?? {}),
|
list_files: () => Object.keys(this.game.files ?? {}),
|
||||||
get_mouse_pos: () => [...this.mousePos],
|
get_mouse_pos: () => [...this.mousePos],
|
||||||
|
has_live2d: () => false,
|
||||||
|
// there is no network access from the player: games that fetch news handle the failure themselves
|
||||||
|
fetch: () => { throw new Error("IOError: сеть недоступна"); },
|
||||||
|
display: { draw: { kill_textures: () => null }, render: { free_memory: () => null } },
|
||||||
jump_out_of_context: (label: string) => {
|
jump_out_of_context: (label: string) => {
|
||||||
throw new JumpSignal("jump", label);
|
throw new JumpSignal("jump", label);
|
||||||
},
|
},
|
||||||
@@ -660,6 +670,7 @@ export class Engine {
|
|||||||
tuple: (xs: any = []) => [...xs],
|
tuple: (xs: any = []) => [...xs],
|
||||||
set: (xs: any = []) => [...new Set(xs)],
|
set: (xs: any = []) => [...new Set(xs)],
|
||||||
frozenset: (xs: any = []) => [...new Set(xs)],
|
frozenset: (xs: any = []) => [...new Set(xs)],
|
||||||
|
cache: { get_total_size: () => 0, cache_limit: 1 },
|
||||||
dir: (o?: any) => (o === undefined ? Object.keys(this.store) : Object.keys(o ?? {})),
|
dir: (o?: any) => (o === undefined ? Object.keys(this.store) : Object.keys(o ?? {})),
|
||||||
dict: (...a: any[]) => {
|
dict: (...a: any[]) => {
|
||||||
const d = new PyDict(Array.isArray(kwless(a)[0]) ? kwless(a)[0] : []);
|
const d = new PyDict(Array.isArray(kwless(a)[0]) ? kwless(a)[0] : []);
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ type Props = Record<string, any>;
|
|||||||
|
|
||||||
class ActionAbort extends Error {}
|
class ActionAbort extends Error {}
|
||||||
|
|
||||||
|
/** A transform used as a value: `{"at": move_in}`; `at icon["at"]` then applies it. */
|
||||||
|
export class TransformRef {
|
||||||
|
constructor(public name: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
/** A screen action such as Jump("x"); run when the player activates a widget. */
|
/** A screen action such as Jump("x"); run when the player activates a widget. */
|
||||||
export class ScreenAction {
|
export class ScreenAction {
|
||||||
/** Actions are callable in Ren'Py: `MainMenu(confirm=False)()` performs the action at once. */
|
/** Actions are callable in Ren'Py: `MainMenu(confirm=False)()` performs the action at once. */
|
||||||
@@ -571,6 +576,7 @@ export class ScreenManager {
|
|||||||
private widget(node: any, ctx: Ctx): HTMLElement | null {
|
private widget(node: any, ctx: Ctx): HTMLElement | null {
|
||||||
switch (node.w) {
|
switch (node.w) {
|
||||||
case "fixed": return this.container(node, ctx, "div", "abs", ["fixed"], true);
|
case "fixed": return this.container(node, ctx, "div", "abs", ["fixed"], true);
|
||||||
|
case "drag": return this.dragBox(node, ctx);
|
||||||
case "frame":
|
case "frame":
|
||||||
case "window": return this.container(node, ctx, "div", "stack", [node.w, "frame"]);
|
case "window": return this.container(node, ctx, "div", "stack", [node.w, "frame"]);
|
||||||
case "vbox": return this.container(node, ctx, "div", "col", ["vbox"]);
|
case "vbox": return this.container(node, ctx, "div", "col", ["vbox"]);
|
||||||
@@ -594,6 +600,31 @@ export class ScreenManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A box the player can move with the pointer; where it was left is kept while the screen is shown. */
|
||||||
|
private dragBox(node: any, ctx: Ctx): HTMLElement {
|
||||||
|
const el = this.container(node, ctx, "div", "abs", ["fixed"]);
|
||||||
|
const props = this.resolve(node, ctx, ["fixed"]);
|
||||||
|
if (!truthy(this.pick(props, "draggable"))) return el;
|
||||||
|
const key = `__drag:${pyStr(this.pick(props, "drag_name") ?? "")}`;
|
||||||
|
const place = (ctx.inst.vars.get(key) ?? [0, 0]) as [number, number];
|
||||||
|
el.style.translate = `${place[0]}px ${place[1]}px`;
|
||||||
|
el.style.cursor = "grab";
|
||||||
|
el.onpointerdown = (down) => {
|
||||||
|
if ((down.target as HTMLElement).closest("button, [role=button], .pg-sbutton")) return;
|
||||||
|
down.stopPropagation();
|
||||||
|
el.setPointerCapture(down.pointerId);
|
||||||
|
const start = [...place] as [number, number];
|
||||||
|
el.onpointermove = (move) => {
|
||||||
|
place[0] = start[0] + (move.clientX - down.clientX);
|
||||||
|
place[1] = start[1] + (move.clientY - down.clientY);
|
||||||
|
el.style.translate = `${place[0]}px ${place[1]}px`;
|
||||||
|
ctx.inst.vars.set(key, [...place]);
|
||||||
|
};
|
||||||
|
el.onpointerup = () => { el.onpointermove = null; el.onpointerup = null; };
|
||||||
|
};
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
private container(node: any, ctx: Ctx, tag: string, layout: Ctx["layout"], kinds: string[], fill = false): HTMLElement {
|
private container(node: any, ctx: Ctx, tag: string, layout: Ctx["layout"], kinds: string[], fill = false): HTMLElement {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
const props = this.resolve(node, ctx, kinds);
|
const props = this.resolve(node, ctx, kinds);
|
||||||
@@ -1345,7 +1376,12 @@ export class ScreenManager {
|
|||||||
|
|
||||||
/** `at` transforms: static properties now, animated ones through the ATL runner. */
|
/** `at` transforms: static properties now, animated ones through the ATL runner. */
|
||||||
private transforms(el: HTMLElement, props: Record<string, Expr>, ctx: Ctx) {
|
private transforms(el: HTMLElement, props: Record<string, Expr>, ctx: Ctx) {
|
||||||
const pick = (spec: AtSpec): AtSpec => (spec.choose ? pick(truthy(this.ev(spec.choose.c)) ? spec.choose.then : spec.choose.else) : spec);
|
const named = (value: any): AtSpec => {
|
||||||
|
const name = value instanceof TransformRef ? value.name : typeof value === "string" ? value : "";
|
||||||
|
return name ? { name, atl: this.host.atl(name) ? name : undefined } : {};
|
||||||
|
};
|
||||||
|
const pick = (spec: AtSpec): AtSpec => (spec.choose ? pick(truthy(this.ev(spec.choose.c)) ? spec.choose.then : spec.choose.else)
|
||||||
|
: spec.dyn ? named(this.ev(spec.dyn)) : spec);
|
||||||
const specs: AtSpec[] = Array.isArray((props as any).at) ? (props as any).at.map(pick) : [];
|
const specs: AtSpec[] = Array.isArray((props as any).at) ? (props as any).at.map(pick) : [];
|
||||||
const live: Props = {};
|
const live: Props = {};
|
||||||
const apply = (p: Props) => {
|
const apply = (p: Props) => {
|
||||||
@@ -1714,6 +1750,9 @@ export class ScreenManager {
|
|||||||
out.Flatten = (child: any) => child; // flattening only changes how the browser composites, not what is drawn
|
out.Flatten = (child: any) => child; // flattening only changes how the browser composites, not what is drawn
|
||||||
out.AlphaMask = shape("alphamask");
|
out.AlphaMask = shape("alphamask");
|
||||||
out.TrackCursor = shape("trackcursor");
|
out.TrackCursor = shape("trackcursor");
|
||||||
|
out.PreviewSlowText = shape("text");
|
||||||
|
out.Move = (...a: any[]) => ({ __transition: "move", args: a }); // `default vpunch = Move(...)`: the effect itself is chosen by `with`
|
||||||
|
out.absolute = (x: any) => Number(x);
|
||||||
out.At = (child: any) => child;
|
out.At = (child: any) => child;
|
||||||
Object.assign(out, colorMatrices);
|
Object.assign(out, colorMatrices);
|
||||||
out.im = {
|
out.im = {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export interface Layer {
|
|||||||
export interface AtSpec {
|
export interface AtSpec {
|
||||||
/** `at (a if cond else b)`: the entry is chosen each time the screen is drawn. */
|
/** `at (a if cond else b)`: the entry is chosen each time the screen is drawn. */
|
||||||
choose?: { c: Expr; then: AtSpec; else: AtSpec };
|
choose?: { c: Expr; then: AtSpec; else: AtSpec };
|
||||||
|
/** `at icon["at"]`: the transform is a value computed when the screen is drawn. */
|
||||||
|
dyn?: Expr;
|
||||||
pos?: string;
|
pos?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
props?: Record<string, any>;
|
props?: Record<string, any>;
|
||||||
|
|||||||
Reference in new issue
Block a user