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:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-27 04:39:48 +03:00
1 parent 5da7ab1a90
commit a7034b3563
13 files changed
+1563 -710

No files matched your search

+80
View File
@@ -193,3 +193,83 @@ test("starred items spread into tuples and lists", () => {
assert.deepEqual(spread, [1, 2, 3, 4]);
} finally { engine.destroy(); }
});
test("pictures on a named layer stay on it, layers stack as config.layers says, and a save keeps both", () => {
const { engine } = makeEngine(runtime());
try {
engine.newStore();
engine.config.layers = ["bottom", "master", "screens", "top"];
engine.syncLayers();
const picture = engine.resolveImage(["background"]);
engine.renderer.show(picture, "bg", [], {});
engine.renderer.show(picture, "cloud", [], { layer: "top" });
engine.renderer.show(picture, "floor", [], { layer: "bottom" });
const depth = (name) => Number(engine.renderer.stage.querySelector(`[data-layer=${name}]`).style.zIndex);
assert.ok(depth("bottom") < depth("master") && depth("master") < 0 && depth("top") > 0);
assert.equal(engine.renderer.isShown("cloud"), false);
assert.equal(engine.renderer.isShown("cloud", "top"), true);
engine.renderer.layerAt("top", [{ props: { xoffset: 30 } }]);
const saved = structuredClone(engine.snapshotState());
assert.deepEqual(saved.scene.map((s) => [s.tag, s.layer ?? "master"]).sort(), [["bg", "master"], ["cloud", "top"], ["floor", "bottom"]]);
assert.deepEqual(saved.layers, { top: [{ props: { xoffset: 30 } }] });
engine.newStore();
engine.applySave(saved);
assert.equal(engine.renderer.isShown("cloud", "top"), true);
assert.equal(engine.renderer.isShown("floor", "bottom"), true);
engine.renderer.scene("top");
assert.equal(engine.renderer.isShown("cloud", "top"), false);
assert.equal(engine.renderer.isShown("bg"), true);
} finally { engine.destroy(); }
});
test("a layer added by the game is part of the save and comes back with it", () => {
const { engine } = makeEngine(runtime());
try {
engine.newStore();
engine.builtins.renpy.add_layer("hud", { __kw: { above: "master" } });
assert.deepEqual(engine.config.layers, ["master", "hud", "transient", "screens", "overlay"]);
const saved = structuredClone(engine.snapshotState());
engine.newStore();
assert.deepEqual(engine.config.layers, ["master", "transient", "screens", "overlay"]);
engine.applySave(saved);
assert.deepEqual(engine.config.layers, ["master", "hud", "transient", "screens", "overlay"]);
} finally { engine.destroy(); }
});
test("preferences are the player's settings", () => {
const { engine } = makeEngine(runtime());
try {
engine.newStore();
const prefs = engine.builtins.preferences;
assert.equal(prefs.text_cps, engine.settings.cps);
prefs.text_cps = 20;
assert.equal(engine.settings.cps, 20);
prefs.set_volume("music", 0.25);
assert.equal(prefs.get_volume("music"), 0.25);
prefs.set_mute("music", true);
assert.equal(prefs.get_volume("music"), 0);
assert.equal(prefs.get_mute("music"), true);
prefs.set_mute("music", false);
assert.equal(prefs.get_volume("music"), 0.25);
engine.py.assignPath("preferences.chaos_on", true); // `default preferences.chaos_on = True`
assert.equal(prefs.chaos_on, true);
assert.equal(engine.builtins.preferences, prefs);
} finally { engine.destroy(); }
});
test("Rollback goes back one interaction", async () => {
const { engine } = makeEngine(runtime());
try {
engine.newStore();
engine.running = true;
engine.cur = { label: "start", path: [0] };
engine.store.n = 1;
engine.interactionStart();
engine.store.n = 2;
engine.cur = { label: "start", path: [0] };
engine.interactionStart();
assert.equal(engine.rollbackLog.length, 2);
engine.rollback();
assert.equal(engine.store.n, 1);
} finally { engine.destroy(); }
});