feat: player runs the game's own text tags, layers and camera, real preferences and Color
Layers (config.layers, onlayer, add_layer, show_layer_at, camera with perspective), the
game's config.custom_text_tags called like Ren'Py does (the built-in {bt}/{fi}/... copies
are gone), built-in {font} {alpha} {k} {cps} {a} {rb}/{rt} {space} {alt} {image}, FontGroup,
mouse displayable, renpy.fetch, preferences backed by the player's settings, Color class,
Render.subsurface/zoom, `default obj.attr` on provided objects, and a repaint loop that
survives text being laid out again.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5da7ab1a90
commit
a7034b3563
13 files changed
+1563
-710
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 { cssColor, makeColor } from "./colors";
|
||||
import { ColorMatrix, colorMatrices, legacyMatrices, matrixFilter } from "./colormatrix";
|
||||
import type { AudioSystem } from "./audio";
|
||||
import { isNativeAction } from "./musicroom";
|
||||
@@ -43,6 +44,8 @@ export interface ScreenHost {
|
||||
quit(): void;
|
||||
mainMenu(): void;
|
||||
openMenu(): void;
|
||||
/** The container for screens shown on a layer of the game (config.layers); null for the interface layer. */
|
||||
screensOn?(layer: string): HTMLElement | null;
|
||||
/** The Rollback action: back to the previous interaction. */
|
||||
rollback?(): void;
|
||||
openUrl(url: string): void;
|
||||
@@ -107,7 +110,18 @@ const legacyMatrix = (...given: any[]) => {
|
||||
};
|
||||
|
||||
export class Displayable {
|
||||
/** Counts the changes made after it was built (`set_text`), so a picture drawn from it is drawn again. */
|
||||
rev = 0;
|
||||
constructor(public kind: string, public args: any[], public kw: Props) {}
|
||||
|
||||
/** `Text.set_text`: the displayable shows other text from now on. */
|
||||
set_text(text: any) {
|
||||
if (this.kind === "text") {
|
||||
this.args[0] = text;
|
||||
this.rev++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class BarValue {
|
||||
@@ -118,6 +132,8 @@ export class BarValue {
|
||||
|
||||
interface Instance {
|
||||
name: string;
|
||||
/** The layer it is shown on: "screens" unless the script said otherwise. */
|
||||
layer: string;
|
||||
def: ScreenDef;
|
||||
args: any[];
|
||||
kw: Props;
|
||||
@@ -250,25 +266,33 @@ export class ScreenManager {
|
||||
const previous = this.suspended;
|
||||
this.suspended = true;
|
||||
this.layer.style.visibility = "hidden";
|
||||
return () => { this.suspended = previous; this.layer.style.visibility = previous ? "hidden" : ""; };
|
||||
for (const inst of this.shown.values()) inst.el.style.visibility = "hidden";
|
||||
return () => {
|
||||
this.suspended = previous;
|
||||
this.layer.style.visibility = previous ? "hidden" : "";
|
||||
for (const inst of this.shown.values()) inst.el.style.visibility = previous ? "hidden" : "";
|
||||
};
|
||||
}
|
||||
|
||||
// --- lifecycle -------------------------------------------------------------------
|
||||
|
||||
show(name: string, args: any[] = [], kw: Props = {}, opts: { call?: boolean; onReturn?: (v: any) => void } = {}) {
|
||||
show(name: string, args: any[] = [], kw: Props = {}, opts: { call?: boolean; onReturn?: (v: any) => void; layer?: string } = {}) {
|
||||
const def = this.screens[name];
|
||||
if (!def) throw new Error(`Не найден screen ${name}`);
|
||||
const previous = this.shown.get(name);
|
||||
if (previous) this.dispose(previous, false);
|
||||
const own = def.props.layer;
|
||||
const layer = opts.layer ?? previous?.layer ?? (own?.k === "const" ? pyStr(own.v) : "screens");
|
||||
const inst: Instance = {
|
||||
name, def, args, kw, frame: new Map(), vars: previous?.vars ?? new Map(), order: previous?.order ?? ++this.counter,
|
||||
name, layer, def, args, kw, frame: new Map(), vars: previous?.vars ?? new Map(), order: previous?.order ?? ++this.counter,
|
||||
onReturn: opts.onReturn, call: !!opts.call, el: document.createElement("div"), timers: [], keys: [], runners: [],
|
||||
clocks: new Map(), clocksSeen: new Set(), finished: previous?.finished ?? new Map(),
|
||||
shownEvents: previous?.shownEvents ?? new Set(), hideActions: [],
|
||||
};
|
||||
inst.el.className = "pg-sinst";
|
||||
this.shown.set(name, inst);
|
||||
this.layer.append(inst.el);
|
||||
(this.host.screensOn?.(layer) ?? this.layer).append(inst.el);
|
||||
if (this.suspended) inst.el.style.visibility = "hidden";
|
||||
this.bindParameters(inst);
|
||||
if (!previous) this.runInside(inst, () => {
|
||||
for (const [variable, expr] of def.defaults) inst.vars.set(variable, this.host.py.eval(expr));
|
||||
@@ -311,14 +335,14 @@ export class ScreenManager {
|
||||
this.layer.remove();
|
||||
}
|
||||
|
||||
snapshot(): { name: string; args: any[]; kw: Props }[] {
|
||||
snapshot(): { name: string; args: any[]; kw: Props; layer?: string }[] {
|
||||
return [...this.shown.values()].filter((i) => !i.call).sort((a, b) => a.order - b.order)
|
||||
.map((i) => ({ name: i.name, args: i.args, kw: i.kw }));
|
||||
.map((i) => ({ name: i.name, args: i.args, kw: i.kw, ...(i.layer === "screens" ? {} : { layer: i.layer }) }));
|
||||
}
|
||||
|
||||
restore(list: { name: string; args: any[]; kw: Props }[]) {
|
||||
restore(list: { name: string; args: any[]; kw: Props; layer?: string }[]) {
|
||||
this.clear();
|
||||
for (const item of list) if (this.screens[item.name]) this.show(item.name, item.args, item.kw);
|
||||
for (const item of list) if (this.screens[item.name]) this.show(item.name, item.args, item.kw, { layer: item.layer });
|
||||
}
|
||||
|
||||
private dispose(inst: Instance, forget: boolean) {
|
||||
@@ -634,7 +658,10 @@ export class ScreenManager {
|
||||
state === "selected_hover" ? ["selected_hover", "selected", "hover", ""] : ["selected_insensitive", "insensitive", ""];
|
||||
for (const prefix of order) {
|
||||
const key = prefix ? `${prefix}_${name}` : name;
|
||||
if (key in props) return this.ev(props[key]);
|
||||
if (key in props) {
|
||||
const value = this.ev(props[key]);
|
||||
return value && value.__renpyColor === true ? value.hexcode : value; // a Color is a CSS colour once it is drawn
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -955,6 +982,7 @@ export class ScreenManager {
|
||||
if (!pictures.length) return;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "pg-gallery";
|
||||
overlay.style.pointerEvents = "auto";
|
||||
Object.assign(overlay.style, { position: "absolute", inset: "0", zIndex: "9999", background: "black", display: "grid" });
|
||||
const image = document.createElement("div");
|
||||
Object.assign(image.style, { display: "grid", gridArea: "1 / 1", placeItems: "center" });
|
||||
@@ -1056,6 +1084,11 @@ export class ScreenManager {
|
||||
}
|
||||
|
||||
/** An element for an image name/path or for a Crop/Solid/Frame/Transform built by game code. */
|
||||
/** The element a displayable value stands for (the player's mouse picture). */
|
||||
element(value: any): HTMLElement | null {
|
||||
return this.displayable(value);
|
||||
}
|
||||
|
||||
private displayable(value: any, depth = 0): HTMLElement | null {
|
||||
if (depth > 8 || value === null || value === undefined) return null;
|
||||
if (isDisplayableInstance(value)) return this.renderHost.host(value);
|
||||
@@ -1097,7 +1130,8 @@ export class ScreenManager {
|
||||
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;
|
||||
const textColor = cssColor(value.kw.color);
|
||||
if (textColor) box.style.color = textColor;
|
||||
return box;
|
||||
}
|
||||
case "image": return this.displayable(a, depth + 1);
|
||||
@@ -1233,7 +1267,7 @@ export class ScreenManager {
|
||||
return box;
|
||||
}
|
||||
case "transform": {
|
||||
const child = this.displayable(a, depth + 1);
|
||||
const child = this.displayable(a ?? value.kw.child, depth + 1);
|
||||
if (!child) return null;
|
||||
const p = value.kw;
|
||||
const parts: string[] = [];
|
||||
@@ -1455,8 +1489,9 @@ export class ScreenManager {
|
||||
const size = num(this.pick(props, "size", state));
|
||||
if (size !== null) el.style.fontSize = `${size}px`;
|
||||
const font = this.pick(props, "font", state);
|
||||
if (typeof font === "string") {
|
||||
const family = this.host.fontFamily(font);
|
||||
const fontName = typeof font === "string" ? font : typeof font?.toString === "function" && String(font).startsWith("@group:") ? String(font) : null;
|
||||
if (fontName) {
|
||||
const family = this.host.fontFamily(fontName);
|
||||
if (family) el.style.fontFamily = `"${family}"`;
|
||||
}
|
||||
if (truthy(this.pick(props, "bold", state))) el.style.fontWeight = "bold";
|
||||
@@ -2012,7 +2047,7 @@ export class ScreenManager {
|
||||
out.Null = shape("null");
|
||||
out.Transform = shape("transform");
|
||||
out.DynamicDisplayable = shape("dynamic");
|
||||
out.Color = (value: any) => (typeof value === "string" ? value : pyStr(value));
|
||||
out.Color = (...a: any[]) => makeColor(a);
|
||||
// Character(...) built by game code: kept as a record the say statement can read
|
||||
out.Character = (...args: any[]) => {
|
||||
const last = args[args.length - 1];
|
||||
@@ -2028,7 +2063,7 @@ export class ScreenManager {
|
||||
out.RoundedCorners = rounded;
|
||||
out.CurriedRoundedCorners = rounded;
|
||||
out.ExplodeText = (child: any) => child; // the letter-explosion effect is shown as the plain text
|
||||
out.MouseDisplayable = () => null; // a custom mouse picture: the browser keeps its own cursor
|
||||
out.MouseDisplayable = (child: any, xoffset = 0, yoffset = 0) => ({ __mouse: child, xoffset, yoffset });
|
||||
out.ConditionSwitch = shape("conditionswitch");
|
||||
out.DynamicImage = shape("dynamicimage"); // `from renpy.text.text import Text as RenpyText`
|
||||
out.MatrixColor = shape("matrixcolor");
|
||||
@@ -2105,11 +2140,17 @@ export class ScreenManager {
|
||||
const last = a[a.length - 1];
|
||||
const kw = last && typeof last === "object" && Object.hasOwn(last, "__kw") ? last.__kw : {};
|
||||
const args = last && typeof last === "object" && Object.hasOwn(last, "__kw") ? a.slice(1, -1) : a.slice(1);
|
||||
this.show(String(a[0]), args, kw);
|
||||
const { _layer, ...rest } = kw;
|
||||
this.show(String(a[0]), args, rest, { layer: _layer ? pyStr(_layer) : undefined });
|
||||
return null;
|
||||
},
|
||||
hide_screen: (name: string) => { this.hide(String(name)); return null; },
|
||||
get_screen: (name: string) => (this.shown.has(name) ? true : null),
|
||||
get_screen: (name: string, ...more: any[]) => {
|
||||
const last = more[more.length - 1];
|
||||
const layer = last && typeof last === "object" && Object.hasOwn(last, "__kw") ? last.__kw.layer : more[0];
|
||||
const found = this.shown.get(name);
|
||||
return found && (!layer || found.layer === pyStr(layer)) ? true : null;
|
||||
},
|
||||
restart_interaction: () => { this.refresh(); return null; },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user