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 = {
|
||||
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) => {
|
||||
const [r, g, b, a] = parseColor(color);
|
||||
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 { languageName, rememberLanguage, type Translator } from "./i18n";
|
||||
import { naniText } from "./naninovel";
|
||||
import { Displayable, ScreenManager, type ScreenHost } from "./screens";
|
||||
import { Displayable, ScreenManager, TransformRef, type ScreenHost } from "./screens";
|
||||
import { musicRoomClass } from "./musicroom";
|
||||
import { pythonModules } from "./pymodules";
|
||||
import { parseExpression, parseStatements } from "./pyparse";
|
||||
@@ -125,8 +125,10 @@ export class Engine {
|
||||
}
|
||||
this.persistent = this.makePersistent();
|
||||
const scope: Scope = {
|
||||
get: (n) => (Object.hasOwn(this.store, n) ? this.store[n] : this.builtins[n]),
|
||||
has: (n) => Object.hasOwn(this.store, n) || Object.hasOwn(this.builtins, n),
|
||||
// a transform named in Python code (`{"at": iconlori}`) is a value the screens can put in `at`
|
||||
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),
|
||||
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)];
|
||||
};
|
||||
|
||||
private isTransform(name: string): boolean {
|
||||
return Boolean(this.game.transforms?.[name] || this.game.atl?.[name]);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener("pointermove", this.onPointer);
|
||||
this.liveImages?.destroy();
|
||||
@@ -450,6 +456,10 @@ export class Engine {
|
||||
has_screen: (name: any) => Boolean(this.game.screens?.[pyStr(name)]),
|
||||
list_files: () => Object.keys(this.game.files ?? {}),
|
||||
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) => {
|
||||
throw new JumpSignal("jump", label);
|
||||
},
|
||||
@@ -660,6 +670,7 @@ export class Engine {
|
||||
tuple: (xs: any = []) => [...xs],
|
||||
set: (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 ?? {})),
|
||||
dict: (...a: any[]) => {
|
||||
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 {}
|
||||
|
||||
/** 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. */
|
||||
export class ScreenAction {
|
||||
/** 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 {
|
||||
switch (node.w) {
|
||||
case "fixed": return this.container(node, ctx, "div", "abs", ["fixed"], true);
|
||||
case "drag": return this.dragBox(node, ctx);
|
||||
case "frame":
|
||||
case "window": return this.container(node, ctx, "div", "stack", [node.w, "frame"]);
|
||||
case "vbox": return this.container(node, ctx, "div", "col", ["vbox"]);
|
||||
@@ -594,6 +600,31 @@ export class ScreenManager {
|
||||
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 {
|
||||
const el = document.createElement(tag);
|
||||
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. */
|
||||
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 live: 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.AlphaMask = shape("alphamask");
|
||||
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;
|
||||
Object.assign(out, colorMatrices);
|
||||
out.im = {
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface Layer {
|
||||
export interface AtSpec {
|
||||
/** `at (a if cond else b)`: the entry is chosen each time the screen is drawn. */
|
||||
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;
|
||||
name?: string;
|
||||
props?: Record<string, any>;
|
||||
|
||||
Reference in new issue
Block a user