import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; import { makeEngine, until } from "./helpers/renpy-engine.mjs"; const built = await build({ stdin: { contents: `export {parseStatements} from './pyparse'; export {galleryScreens} from './gallery-access';`, resolveDir: fileURLToPath(new URL("../src/player/", import.meta.url)), loader: "ts" }, bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" }); const { parseStatements, galleryScreens } = await import(`data:text/javascript;base64,${Buffer.from(built.outputFiles[0].text).toString("base64")}`); const runtime = () => ({ game: { resolution: [640, 480], entry: "start", ui: {}, files: {}, defaults: [], init: parseStatements('g = Gallery()\ng.button("Art")\ng.image("picture")\ng.condition("False")'), characters: {}, channels: {}, images: { picture: { color: "#fff" } }, transforms: {}, atl: {}, transitions: {}, screens: {}, styles: {}, audio: {} }, modules: [{ labels: [{ name: "start", body: [{ op: "pause", seconds: null }] }], anchors: {} }] }); test("Gallery conditions stay locked normally and are bypassed only for the opted-in version", () => { for (const enabled of [false, true]) { const { engine } = makeEngine(runtime(), { galleryUnlocked: enabled }); try { engine.newStore(); const action = engine.py.attrOf(engine.store.g, "Action")("Art"); assert.equal(action !== null, enabled); engine.openUnlockedGallery(); assert.equal(engine.gallerySession, enabled); } finally { engine.destroy(); } } }); test("gallery browsing does not write saves or persistent progress and restores the running story", async () => { const { engine, root } = makeEngine(runtime(), { galleryUnlocked: true }); let saves = 0; engine.saves.put = async () => { saves++; }; try { engine.newStore(); engine.persistent.example = ["story"]; engine.store.counter = 7; engine.history = [{ name: "Player", text: "Story history" }]; engine.seenImages.add("story"); engine.cur = { label: "start", path: [0] }; engine.running = true; engine.openUnlockedGallery(); assert.match(root.textContent, /Art/); engine.persistent.example.push("gallery"); engine.persistent.other = true; engine.seenImages.add("gallery"); await engine.save("auto"); assert.equal(saves, 0); engine.closeUnlockedGallery(); assert.deepEqual(engine.persistent.example, ["story"]); assert.equal(engine.persistent.other, null); assert.equal(engine.store.counter, 7); assert.deepEqual(engine.history, [{ name: "Player", text: "Story history" }]); assert.deepEqual([...engine.seenImages], ["story"]); assert.equal(engine.gallerySession, false); } finally { engine.destroy(); } }); test("MIST unlock modifies only gallery expressions and keeps page and story conditions intact", () => { const condition = { k: "attr", o: { k: "name", id: "scene" }, a: "watched" }; const page = { k: "cmp", l: { k: "name", id: "curpage" }, ops: ["=="], r: [{ k: "const", v: "Page 1" }] }; const game = { build: { name: "MIST" }, screens: { rewatchgirlselectscreen: {}, robgallery: { condition, page }, story: { condition } } }; const unlocked = galleryScreens(game, true); assert.deepEqual(unlocked.robgallery.condition, { k: "const", v: true }); assert.deepEqual(unlocked.robgallery.page, page); assert.equal(unlocked.story, game.screens.story); assert.deepEqual(game.screens.robgallery.condition, condition); assert.equal(galleryScreens(game, false), game.screens); }); test("MIST's actual gallery opens all character and scene buttons without completing quests", { skip: !process.env.PGAMES_MIST_RUNTIME, }, async () => { const original = JSON.parse(readFileSync(process.env.PGAMES_MIST_RUNTIME, "utf8")); const { engine, root } = makeEngine(original, { galleryUnlocked: true }); engine.fileUrl = (path) => `/mock/${path}`; engine.audio.play = () => {}; try { engine.openUnlockedGallery(); await until(() => root.querySelectorAll(".pg-simgbtn").length >= 5); assert.equal(engine.py.attrOf(engine.store.searchlodge07, "is_done"), false); const robin = root.querySelectorAll(".pg-simgbtn")[1]; robin.onclick({ stopPropagation() {} }); await until(() => engine.screens.shown.has("robgallery")); const images = [...engine.screens.shown.get("robgallery").el.querySelectorAll("img")].map((image) => image.src); assert.ok(images.some((path) => path.includes("g_r01"))); assert.equal(images.some((path) => path.includes("gallocked")), false); assert.equal(engine.py.attrOf(engine.store.rscene01, "watched"), false); for (const [name, pages] of [["robgallery", 3], ["miagallery", 2], ["lilgallery", 2], ["norgallery", 2], ["groupgallery", 1]]) { engine.screens.show(name); const instance = engine.screens.shown.get(name); for (let page = 1; page <= pages; page++) { instance.vars.set("curpage", `Page ${page}`); engine.screens.render(instance); const pictures = [...instance.el.querySelectorAll("img")].map((image) => image.src); assert.equal(pictures.some((path) => path.includes("gallocked")), false, `${name}, page ${page}`); } } engine.store.replaywatch = true; engine.jump("myroomcontrol"); assert.equal(engine.cur.label, "rewatchscene"); engine.screens.show("rewatchgirlselectscreen"); engine.store.replaywatch = false; engine.screens.shown.get("rewatchgirlselectscreen").el.querySelector(".pg-simgbtn").onclick({ stopPropagation() {} }); await new Promise((resolve) => setTimeout(resolve, 20)); assert.equal(engine.gallerySession, false); assert.equal(engine.running, false); assert.equal(engine.cur.label, ""); } finally { engine.destroy(); } });