feat: mousearea, datetime dates, dir(), renpy.image_size and style.rebuild for Ren'Py games
CI / frontend (push) Failing after 19s
CI / backend (push) Failing after 40s

Screens get an invisible mousearea that reports the pointer entering and leaving without taking
clicks. datetime.date/datetime/timedelta support construction and date arithmetic. dir() lists the
game's variables, renpy.style.rebuild() is a no-op, and renpy.image_size answers only for pictures
the browser already holds (otherwise an IOError the game can catch).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-26 03:02:13 +03:00
1 parent 6c10d06d90
commit 4dc9836a8b
7 files changed
+119 -4

No files matched your search

+52 -1
View File
@@ -123,7 +123,58 @@ function strftime(format: string, d: Date): string {
}
const now = () => new DateTime(new Date());
const DAY_MS = 86_400_000;
/** `datetime.timedelta`: kept in milliseconds. */
class TimeDelta {
constructor(public ms: number) {}
get days() { return Math.floor(this.ms / DAY_MS); }
get seconds() { return Math.floor((this.ms - this.days * DAY_MS) / 1000); }
total_seconds() { return this.ms / 1000; }
__binop__(op: string, other: any) {
if (op === "+" && other instanceof TimeDelta) return new TimeDelta(this.ms + other.ms);
if (op === "-" && other instanceof TimeDelta) return new TimeDelta(this.ms - other.ms);
if (op === "*" && typeof other === "number") return new TimeDelta(this.ms * other);
throw new TypeError(`timedelta ${op}`);
}
}
function timedelta(...a: any[]) {
const last = a[a.length - 1];
const kw = last && typeof last === "object" && last.__kw ? last.__kw : {};
const unit: Record<string, number> = { days: DAY_MS, seconds: 1000, minutes: 60_000, hours: 3_600_000,
weeks: 7 * DAY_MS, milliseconds: 1 };
let ms = typeof a[0] === "number" ? a[0] * DAY_MS : 0;
for (const [name, factor] of Object.entries(unit)) if (typeof kw[name] === "number") ms += kw[name] * factor;
return new TimeDelta(ms);
}
/** `date(y, m, d)` and `datetime(y, m, d, ...)`: a DateTime at a given moment, with date arithmetic. */
const fromParts = (...a: any[]) => {
const [y, m, d, h = 0, min = 0, s = 0] = a.filter((x) => typeof x === "number");
const date = new Date(y, m - 1, d, h, min, s);
if (date.getMonth() !== m - 1 || date.getDate() !== d) throw new Error("ValueError: day is out of range for month");
return new DateTime(date);
};
const datetimeClass = { now, today: now, utcnow: now };
const dateClass = Object.assign(fromParts, datetimeClass);
Object.assign(DateTime.prototype, {
__binop__(this: DateTime, op: string, other: any) {
if (other instanceof TimeDelta && (op === "+" || op === "-")) {
const moved = new Date(this.d.getTime());
moved.setTime(moved.getTime() + (op === "+" ? other.ms : -other.ms));
// whole days keep the wall-clock time across daylight-saving changes
if (other.ms % DAY_MS === 0) {
const shifted = new Date(this.d.getTime());
shifted.setDate(shifted.getDate() + (op === "+" ? 1 : -1) * (other.ms / DAY_MS));
return new DateTime(shifted);
}
return new DateTime(moved);
}
if (other instanceof DateTime && op === "-") return new TimeDelta(this.d.getTime() - other.d.getTime());
throw new TypeError(`datetime ${op}`);
},
});
/** Namespaces added to the sandbox's builtins. */
export function pythonModules(): Record<string, any> {
@@ -172,7 +223,7 @@ export function pythonModules(): Record<string, any> {
sleep: () => null,
strftime: (format: string, t?: any) => strftime(format, t instanceof DateTime ? t.d : new Date()),
},
datetime: Object.assign({ datetime: datetimeClass, date: datetimeClass }, datetimeClass),
datetime: Object.assign({ datetime: dateClass, date: dateClass, timedelta }, datetimeClass),
collections: {
defaultdict: (factory?: any) => new PyDefaultDict(typeof factory === "function" ? () => factory() : null),
OrderedDict: () => new PyDict(),