feat: player runtime for the Phone library and That New Teacher
Named stores (PyNamespace: dotted access, functions that see their own store first, restored after a
save is loaded), real generators run step by step (next, send, yield as an expression), len/bool through
__len__/__bool__, os/calendar/itertools/pygame_sdl2 helpers, Skip and ToggleLocalVariable actions,
text tags {glitch} and {wave} and plain display of explode/gradient/ruby, SnowBlossom and anim.Blink,
RoundedCorners as a transform, transforms listed in `at`, ATL choice weights that are flags, and
renpy.image_size from the converter's picture sizes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
83cc7fdd5c
commit
11f87df712
15 files changed
+457
-24
No files matched your search
@@ -45,6 +45,8 @@ export interface ScreenHost {
|
||||
openUrl(url: string): void;
|
||||
quickSave(): void;
|
||||
quickLoad(): void;
|
||||
/** The Skip action: fast-forward through text already seen. */
|
||||
setSkipping?(on: boolean): void;
|
||||
/** Ask the player to confirm (Confirm action). */
|
||||
ask(text: string): Promise<boolean>;
|
||||
/** The name of the current save page (`FilePageNameInputValue`). */
|
||||
@@ -54,6 +56,8 @@ export interface ScreenHost {
|
||||
saveSlot(slot: string, confirm: boolean): void;
|
||||
loadSlot(slot: string): void;
|
||||
deleteSlot(slot: string, confirm: boolean): void;
|
||||
/** Natural size of a named picture (from the converter), or null. */
|
||||
imageSize?(value: any): [number, number] | null;
|
||||
/** Size of the game picture, for displayables that ask how much room they have. */
|
||||
resolution?: [number, number];
|
||||
/** The page of the save/load screen (`persistent._file_page`). */
|
||||
@@ -174,6 +178,7 @@ export class ScreenManager {
|
||||
py: this.host.py,
|
||||
element: (value) => this.displayable(value),
|
||||
area: () => this.host.resolution ?? [1920, 1080],
|
||||
imageSize: (value) => this.host.imageSize?.(value) ?? null,
|
||||
finish: (value) => this.interactionReturn?.(value),
|
||||
}));
|
||||
}
|
||||
@@ -336,6 +341,8 @@ export class ScreenManager {
|
||||
this.pending = requestAnimationFrame(() => {
|
||||
this.pending = 0;
|
||||
if (this.suspended) return;
|
||||
// A press may have started after this frame was queued by a timer or hover.
|
||||
if (this.pressed) { this.refreshAfterPress = true; return; }
|
||||
for (const inst of [...this.shown.values()]) if (this.shown.get(inst.name) === inst) this.render(inst);
|
||||
});
|
||||
}
|
||||
@@ -1147,6 +1154,25 @@ export class ScreenManager {
|
||||
}
|
||||
return child;
|
||||
}
|
||||
case "blink": { // anim.Blink(picture): fades in and out
|
||||
const picture = this.displayable(a, depth + 1);
|
||||
if (picture) picture.style.animation = "pg-blink 1.2s ease-in-out infinite";
|
||||
return picture;
|
||||
}
|
||||
case "snowblossom": { // falling pictures
|
||||
const field = document.createElement("div");
|
||||
field.className = "pg-snow";
|
||||
const count = Math.min(60, Number(value.kw.count ?? b ?? 10) || 10);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const piece = this.displayable(a, depth + 1);
|
||||
if (!piece) break;
|
||||
piece.style.left = `${(i * 37 + (i * i) % 23) % 100}%`;
|
||||
piece.style.animationDuration = `${6 + (i * 7) % 9}s`;
|
||||
piece.style.animationDelay = `${-((i * 13) % 11)}s`;
|
||||
field.append(piece);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
case "conditionswitch": { // ConditionSwitch("cond", picture, ...): the first condition that holds
|
||||
for (let i = 0; i + 1 < value.args.length; i += 2) {
|
||||
const holds = typeof value.args[i] === "string" ? truthy(this.host.py.eval(parseExpression(value.args[i]))) : truthy(value.args[i]);
|
||||
@@ -1546,11 +1572,16 @@ export class ScreenManager {
|
||||
const named = (value: any): AtSpec => {
|
||||
const name = value instanceof TransformRef ? value.name : typeof value === "string" ? value
|
||||
: value && typeof value.__transform === "string" ? value.__transform : "";
|
||||
if (value && value.__rounded !== undefined) return { props: { rounded: value.__rounded } };
|
||||
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 pick = (spec: AtSpec): AtSpec[] => {
|
||||
if (spec.choose) return pick(truthy(this.ev(spec.choose.c)) ? spec.choose.then : spec.choose.else);
|
||||
if (!spec.dyn) return [spec];
|
||||
const value = this.ev(spec.dyn);
|
||||
return (Array.isArray(value) ? value : [value]).filter((v) => v !== null && v !== undefined).map(named); // a list of transforms
|
||||
};
|
||||
const specs: AtSpec[] = Array.isArray((props as any).at) ? (props as any).at.flatMap(pick) : [];
|
||||
const live: Props = {};
|
||||
const apply = (p: Props) => {
|
||||
const zoom = (p.zoom ?? 1);
|
||||
@@ -1562,6 +1593,11 @@ 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 (p.rounded !== undefined) { // RoundedCorners(radius): one radius or four (the game's own shader)
|
||||
const radius = Array.isArray(p.rounded) ? p.rounded.map((r: number) => `${r}px`).join(" ") : `${p.rounded}px`;
|
||||
el.style.borderRadius = radius;
|
||||
el.style.overflow = "hidden";
|
||||
}
|
||||
applyFilters(el, p);
|
||||
};
|
||||
for (const spec of specs) {
|
||||
@@ -1744,6 +1780,13 @@ export class ScreenManager {
|
||||
case "InvertSelected": this.perform(a, owner); return;
|
||||
case "UploadSync":
|
||||
case "DownloadSync": this.host.notify("Синхронизация недоступна в браузере"); return;
|
||||
case "Skip": this.host.setSkipping?.(true); return;
|
||||
case "Rollback": return; // there is no rollback in the browser player
|
||||
case "ToggleLocalVariable": {
|
||||
const now = owner?.vars.get(a);
|
||||
owner?.vars.set(a, !truthy(now));
|
||||
return;
|
||||
}
|
||||
case "Confirm": {
|
||||
const yes = b, no = c;
|
||||
void this.host.ask(pyStr(a)).then((ok) => { this.run(ok ? yes : no, owner); });
|
||||
@@ -1844,7 +1887,7 @@ export class ScreenManager {
|
||||
"OpenURL", "SetLocalVariable", "QuickSave", "QuickLoad", "Replay", "EndReplay", "ToggleSetMembership",
|
||||
"ToggleFocus", "PauseAudio", "SetMixer", "FileAction", "FileSave", "FileLoad", "FileDelete", "FileTakeScreenshot",
|
||||
"FilePage", "FilePageNext", "FilePagePrevious", "CaptureFocus", "ClearFocus", "InvertSelected", "UploadSync",
|
||||
"DownloadSync", "Confirm"]) out[kind] = make(kind);
|
||||
"DownloadSync", "Confirm", "Skip", "Rollback", "ToggleLocalVariable"]) out[kind] = make(kind);
|
||||
// what an `input` edits: classes games can extend (`class Typing(VariableInputValue)` overrides set_text)
|
||||
const inputBase = new PyClass("InputValue", [OBJECT]);
|
||||
const native = (cls: PyClass, name: string, fn: (self: PyInstance, ...a: any[]) => any) => cls.attrs.set(name, nativeMethod(fn));
|
||||
@@ -1961,6 +2004,13 @@ export class ScreenManager {
|
||||
out.Text = shape("text");
|
||||
out.RenpyText = shape("text");
|
||||
out.Animation = shape("animation");
|
||||
out.SnowBlossom = shape("snowblossom");
|
||||
out.anim = { Blink: shape("blink") };
|
||||
const rounded = (...a: any[]) => ({ __rounded: (a[a.length - 1]?.__kw ?? {}).radius ?? a[1] ?? 0 });
|
||||
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.ConditionSwitch = shape("conditionswitch");
|
||||
out.DynamicImage = shape("dynamicimage"); // `from renpy.text.text import Text as RenpyText`
|
||||
out.MatrixColor = shape("matrixcolor");
|
||||
|
||||
Reference in new issue
Block a user