An overlay camera from #302 can stand elsewhere and zoom differently.
A HUD button only it shows lay somewhere else in the world than the
mouse of the main camera, so Is hovered never found it. Both nodes now
take a camera and see the mouse through that camera's picture.
- New input `camera` on Mouse position and Is hovered. Left open, both
read the main camera exactly as before.
- `pointerThrough` in core/properties/camera is the rule as a pure
function. It fits the camera's picture into the main one and centres
it, the same way overlayView draws it.
- The game view reports where the mouse lies on the main picture, as a
part of its width and height. The world turns that into a camera's
view, so a main camera on the move does not drag the mouse across
the HUD while the mouse stands still.
- `in view` is true only on the camera's own picture. A camera that is
not drawn (switched off, a second plain one, not a camera) never has
the mouse on it.
- On click and Mouse button stay with the main camera, as agreed in
the issue.
Also fixes a unit test that 1d0fa7b broke. play-settings expected 8
to be no game view scale, but the steps go up to 20 since then. It now
checks 20 as the top and 21 as no choice.
Tests:
- Unit: the rule lands on the same world point as overlayView plus
screenToWorld at four screen points for two cameras. One mouse gives
(100, 50) through the main camera and (1050, 25) through an overlay
at (1000, 0) with zoom 2.
- e2e 63c2: the real mouse 30 units right of the picture centre hovers
the overlay's square through it, through the main camera not.
- Counter-checks: Math.max instead of Math.min in the rule, and no
check for "drawn", each fail one unit test. The view reporting the
centre of the picture always fails 63c2 with x 2000 instead of 2015.
- Gate: 231 files, 3526 tests (+6). e2e 63: 12 checks (+1), sum 1730.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
420 lines
18 KiB
JavaScript
420 lines
18 KiB
JavaScript
/**
|
||
* Render layers and overlay cameras (#302). The main camera sees only `Default`, an overlay camera
|
||
* 2000 units to the right sees only `UI`. Each draws its own picture, the overlay's laid on top:
|
||
* a green sprite of the overlay with z -10 covers a red one of the main camera with z 1000, and
|
||
* what a camera does not see stays away – a yellow `UI` sprite in front of the main camera, a blue
|
||
* `Default` one in front of the overlay. Switched to no overlay, the second camera draws nothing.
|
||
* `Is hovered` and `Mouse position` find the mouse through the overlay camera too (#304).
|
||
* Then the main camera wears a shader over its picture (#299), in the Game tab and, switched on in
|
||
* the eye, in the scene view.
|
||
*
|
||
* Everything it adds carries an `rl63` id and goes again at the end.
|
||
*/
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
const OVERLAY = 'rl63-cam';
|
||
|
||
export async function renderLayers(t) {
|
||
const { check, json, evaluate, mutate, sleep, shot, showTab, play, stop, clickText, press } = t;
|
||
await showTab('scene');
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const main = scene.objects.find((o) => o.id === 'o3');
|
||
window.__rl63 = { layers: p.renderLayers, overrides: structuredClone(main.overrides) };
|
||
p.renderLayers = ['Default', 'UI'];
|
||
main.overrides = { ...main.overrides, sees: 0b01 };
|
||
${MAKE_OBJECT}
|
||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||
const square = (id, name, dx, z, size, color, layer, from) => {
|
||
const o = make(id, sprite.id, '', 0, name);
|
||
Object.assign(o.position, { x: from.x + dx, y: from.y, z });
|
||
o.overrides = { width: size, height: size, color, texture: '', shader: '', renderLayer: layer };
|
||
return o;
|
||
};
|
||
const overlay = structuredClone(main);
|
||
Object.assign(overlay, { id: '${OVERLAY}', name: 'Overlay 63', instanceEdits: {} });
|
||
overlay.position = { ...main.position, x: main.position.x + 2000 };
|
||
overlay.overrides = { ...main.overrides, overlay: true, sees: 0b10 };
|
||
scene.objects.push(
|
||
overlay,
|
||
square('rl63-a', 'A 63', 70, 1000, 60, 0xff0000, -1, main.position),
|
||
square('rl63-d', 'D 63', -70, 1000, 60, 0xffff00, 1, main.position),
|
||
square('rl63-b', 'B 63', 0, -10, 40, 0x00ff00, 1, overlay.position),
|
||
square('rl63-c', 'C 63', 70, 1000, 60, 0x0000ff, 0, overlay.position),
|
||
);
|
||
`);
|
||
await evaluate('__gm.state.select(null)');
|
||
await showTab('game');
|
||
await sleep(300);
|
||
|
||
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||
const is = (hex, [r, g, b]) => {
|
||
const [pr, pg, pb] = channels(hex);
|
||
return Math.abs(pr - r) < 30 && Math.abs(pg - g) < 30 && Math.abs(pb - b) < 30;
|
||
};
|
||
/** The three points of the picture the squares sit on: the centre and 70 units either side. */
|
||
const picture = async () => {
|
||
const { frame, scale } = await json('__gm.gameScale()');
|
||
const cx = Math.round(frame.x + frame.w / 2);
|
||
const cy = Math.round(frame.y + frame.h / 2);
|
||
const at = (dx) => json(`__gm.pixelAt(${Math.round(cx + dx * scale)}, ${cy})`);
|
||
return {
|
||
centre: await at(0),
|
||
right: await at(70),
|
||
left: await at(-70),
|
||
main: await json('__gm.gameDrawOrder()'),
|
||
overlays: await json('__gm.gameOverlays()'),
|
||
};
|
||
};
|
||
const GREEN = [0, 255, 0];
|
||
const RED = [255, 0, 0];
|
||
const YELLOW = [255, 255, 0];
|
||
const stacked = (seen) =>
|
||
seen.overlays.length === 1 &&
|
||
seen.overlays[0].camera === OVERLAY &&
|
||
seen.overlays[0].drawOrder.includes('rl63-b') &&
|
||
!seen.overlays[0].drawOrder.includes('rl63-c') &&
|
||
seen.main.includes('rl63-a') &&
|
||
!seen.main.includes('rl63-d') &&
|
||
!seen.main.includes('rl63-b') &&
|
||
is(seen.centre, GREEN) &&
|
||
is(seen.right, RED) &&
|
||
!is(seen.left, YELLOW);
|
||
|
||
// 63a. The preview of the Game tab: each camera draws what it sees, the overlay on top.
|
||
const preview = await picture();
|
||
await shot('63-overlay-preview');
|
||
check(
|
||
'each camera draws only its layers, and the overlay lies on top whatever the z',
|
||
stacked(preview),
|
||
JSON.stringify(preview),
|
||
);
|
||
|
||
// 63b. Without Overlay the second camera is a plain one after the first: it draws nothing.
|
||
await mutate(`
|
||
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
|
||
cam.overrides = { ...cam.overrides, overlay: false };
|
||
`);
|
||
await sleep(200);
|
||
const plain = await picture();
|
||
check(
|
||
'two plain cameras switched on draw through the first one only',
|
||
plain.overlays.length === 0 && !is(plain.centre, GREEN) && is(plain.right, RED),
|
||
JSON.stringify(plain),
|
||
);
|
||
await mutate(`
|
||
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
|
||
cam.overrides = { ...cam.overrides, overlay: true };
|
||
`);
|
||
|
||
// 63c. The running game stacks them the same way.
|
||
await play();
|
||
await sleep(400);
|
||
const running = await picture();
|
||
check(
|
||
'the running game lays the overlay camera on top the same way',
|
||
stacked(running),
|
||
JSON.stringify(running),
|
||
);
|
||
await stop();
|
||
|
||
// 63c2. (#304) The mouse through the overlay camera, zoomed 2x: 30 units right of the centre of
|
||
// the picture it lies 15 units right of the overlay's centre, on the 40 wide square B. Through
|
||
// the main camera nothing is there. Logged each tick: hovered through it, without it, its x.
|
||
const node = (id, type, x, params = {}) => ({ id, type, x, y: 0, params });
|
||
const edge = (id, from, fromPort, to, toPort) => ({
|
||
id,
|
||
from: { node: from, port: fromPort },
|
||
to: { node: to, port: toPort },
|
||
});
|
||
const hoverGraph = {
|
||
nodes: [
|
||
node('rl63-tick', 'OnUpdate', 0),
|
||
node('rl63-b-find', 'ObjectByName', 0, { name: 'B 63' }),
|
||
node('rl63-cam-find', 'ObjectByName', 0, { name: 'Overlay 63' }),
|
||
node('rl63-through', 'IsHovered', 200),
|
||
node('rl63-plain', 'IsHovered', 200),
|
||
node('rl63-mouse', 'MousePosition', 200),
|
||
node('rl63-log1', 'Log', 400),
|
||
node('rl63-log2', 'Log', 600),
|
||
node('rl63-log3', 'Log', 800),
|
||
],
|
||
edges: [
|
||
edge('rl63-e1', 'rl63-tick', 'exec', 'rl63-log1', 'exec'),
|
||
edge('rl63-e2', 'rl63-log1', 'exec', 'rl63-log2', 'exec'),
|
||
edge('rl63-e3', 'rl63-log2', 'exec', 'rl63-log3', 'exec'),
|
||
edge('rl63-e4', 'rl63-b-find', 'object', 'rl63-through', 'object'),
|
||
edge('rl63-e5', 'rl63-cam-find', 'object', 'rl63-through', 'camera'),
|
||
edge('rl63-e6', 'rl63-b-find', 'object', 'rl63-plain', 'object'),
|
||
edge('rl63-e7', 'rl63-cam-find', 'object', 'rl63-mouse', 'camera'),
|
||
edge('rl63-e8', 'rl63-through', 'result', 'rl63-log1', 'message'),
|
||
edge('rl63-e9', 'rl63-plain', 'result', 'rl63-log2', 'message'),
|
||
edge('rl63-e10', 'rl63-mouse', 'x', 'rl63-log3', 'message'),
|
||
],
|
||
};
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
p.assets.push({ id: 'rl63-hover', type: 'behavior', name: 'Hover 63', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(hoverGraph)} });
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('rl63-logic', 'rl63-hover', '', 0, 'Hover 63'));
|
||
const cam = scene.objects.find((o) => o.id === '${OVERLAY}');
|
||
window.__rl63.overlay = structuredClone(cam.overrides);
|
||
cam.overrides = { ...cam.overrides, zoom: 2 };
|
||
`);
|
||
await play();
|
||
await sleep(300);
|
||
const game = await json('__gm.gameScale()');
|
||
const centre = { x: game.frame.x + game.frame.w / 2, y: game.frame.y + game.frame.h / 2 };
|
||
await t.mouse('mouseMoved', centre.x + 30 * game.scale, centre.y);
|
||
await sleep(300);
|
||
const ran = await json('__gm.world()');
|
||
const hovered = ran?.log.slice(-3) ?? [];
|
||
const overlayX = ran?.entities.find((e) => e.objectId === OVERLAY)?.worldX;
|
||
await stop();
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'rl63-logic');
|
||
p.assets = p.assets.filter((a) => a.id !== 'rl63-hover');
|
||
scene.objects.find((o) => o.id === '${OVERLAY}').overrides = window.__rl63.overlay;
|
||
`);
|
||
check(
|
||
'Is hovered through the overlay camera finds what only it shows, through the main one not',
|
||
hovered[0] === 'true' && hovered[1] === 'false' && Math.abs(Number(hovered[2]) - (overlayX + 15)) < 1,
|
||
JSON.stringify({ hovered, overlayX }),
|
||
);
|
||
|
||
// 63d. The project settings list the render layers, apart from the collision layers.
|
||
await t.clickToolbar('menu');
|
||
await clickText('Project settings…');
|
||
const opened = await json('__gm.renderLayers()');
|
||
await clickText('Add render layer');
|
||
const added = await json('__gm.renderLayers()');
|
||
const collision = await json('__gm.collisionLayers()');
|
||
await press('Escape');
|
||
await sleep(150);
|
||
check(
|
||
'the project settings list the render layers and add one at the end',
|
||
JSON.stringify(opened.dialog) === JSON.stringify(['Default', 'UI']) &&
|
||
JSON.stringify(added.dialog) === JSON.stringify(['Default', 'UI', 'Layer 2']) &&
|
||
!collision.dialog.includes('UI'),
|
||
JSON.stringify({ opened, added, collision }),
|
||
);
|
||
|
||
// 63e. (#299) The main camera wears a shader over its whole picture. One that changes nothing
|
||
// draws the picture byte for byte as before, the right way up: a blue square 100 units above
|
||
// the centre stays above it. At 100 % so the picture's pixels sit on the screen's.
|
||
await showTab('game');
|
||
const scaleBefore = await json('__gm.state.gameScale');
|
||
await mutate(`
|
||
${MAKE_OBJECT}
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const main = scene.objects.find((o) => o.id === 'o3');
|
||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||
const o = make('rl63-e', sprite.id, '', 0, 'E 63');
|
||
Object.assign(o.position, { x: main.position.x, y: main.position.y - 100, z: 0 });
|
||
o.overrides = { width: 30, height: 30, color: 0x3366ff, texture: '', shader: '', renderLayer: 0 };
|
||
scene.objects.push(o);
|
||
__gm.state.setGameScale(1);
|
||
`);
|
||
const shaderOf = (id, nodes, edges) => ({
|
||
id,
|
||
type: 'shader',
|
||
name: id,
|
||
props: {},
|
||
graph: { nodes, edges },
|
||
});
|
||
const link = (from, out, to, port) => ({ from: { node: from, port: out }, to: { node: to, port } });
|
||
const PASS = shaderOf('rl63-pass', [{ id: 'out', type: 'ShaderOutput', x: 0, y: 0, params: {} }], []);
|
||
const FLAT_RED = shaderOf(
|
||
'rl63-red',
|
||
[
|
||
{ id: 'red', type: 'ShaderColor', x: 0, y: 0, params: { value: 0xff0000 } },
|
||
{ id: 'one', type: 'ShaderNumber', x: 0, y: 80, params: { value: 1 } },
|
||
{ id: 'out', type: 'ShaderOutput', x: 200, y: 0, params: {} },
|
||
],
|
||
[link('red', 'color', 'out', 'color'), link('one', 'value', 'out', 'alpha')],
|
||
);
|
||
// Red is the width, green the height, both in thousandths: 640 × 360 comes out as 163 / 92.
|
||
const SIZE = shaderOf(
|
||
'rl63-size',
|
||
[
|
||
{ id: 'size', type: 'ShaderTextureSize', x: 0, y: 0, params: {} },
|
||
{ id: 'red', type: 'ShaderColor', x: 0, y: 80, params: { value: 0xff0000 } },
|
||
{ id: 'green', type: 'ShaderColor', x: 0, y: 160, params: { value: 0x00ff00 } },
|
||
{ id: 'milli', type: 'ShaderNumber', x: 0, y: 240, params: { value: 0.001 } },
|
||
{ id: 'one', type: 'ShaderNumber', x: 0, y: 320, params: { value: 1 } },
|
||
{ id: 'w', type: 'ShaderMultiply', x: 200, y: 0, params: {} },
|
||
{ id: 'h', type: 'ShaderMultiply', x: 200, y: 160, params: {} },
|
||
{ id: 'sum', type: 'ShaderAdd', x: 400, y: 0, params: {} },
|
||
{ id: 'scaled', type: 'ShaderMultiply', x: 600, y: 0, params: {} },
|
||
{ id: 'out', type: 'ShaderOutput', x: 800, y: 0, params: {} },
|
||
],
|
||
[
|
||
link('red', 'color', 'w', 'a'),
|
||
link('size', 'width', 'w', 'b'),
|
||
link('green', 'color', 'h', 'a'),
|
||
link('size', 'height', 'h', 'b'),
|
||
link('w', 'result', 'sum', 'a'),
|
||
link('h', 'result', 'sum', 'b'),
|
||
link('sum', 'result', 'scaled', 'a'),
|
||
link('milli', 'value', 'scaled', 'b'),
|
||
link('scaled', 'result', 'out', 'color'),
|
||
link('one', 'value', 'out', 'alpha'),
|
||
],
|
||
);
|
||
/** Puts `shader` on the main camera (null takes it off), with `resolution` as its row. */
|
||
const wear = (shader, resolution = 'screen') =>
|
||
mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('rl63-'));
|
||
const shader = ${JSON.stringify(shader)};
|
||
if (shader) p.assets.push(shader);
|
||
const main = __gm.state.activeScene.objects.find((o) => o.id === 'o3');
|
||
main.overrides = { ...main.overrides, shader: shader ? shader.id : '', shaderResolution: '${resolution}' };
|
||
`);
|
||
/** Two rows of the picture, through the centre and through the blue square, and the square's spot. */
|
||
const rows = async () => {
|
||
const { frame, scale } = await json('__gm.gameScale()');
|
||
const cx = Math.round(frame.x + frame.w / 2);
|
||
const cy = Math.round(frame.y + frame.h / 2);
|
||
const row = (dy) =>
|
||
json(`__gm.pixelRow(${Math.round(frame.x)}, ${Math.round(cy + dy * scale)}, ${Math.round(frame.w)})`);
|
||
return {
|
||
centre: await row(0),
|
||
above: await row(-100),
|
||
blueAbove: await json(`__gm.pixelAt(${cx}, ${Math.round(cy - 100 * scale)})`),
|
||
blueBelow: await json(`__gm.pixelAt(${cx}, ${Math.round(cy + 100 * scale)})`),
|
||
pixels: await json('__gm.cameraShaderPixels()'),
|
||
};
|
||
};
|
||
const same = (a, b) => JSON.stringify([a.centre, a.above]) === JSON.stringify([b.centre, b.above]);
|
||
const unshaded = await rows();
|
||
await wear(PASS);
|
||
const handedThrough = await rows();
|
||
const differing = handedThrough.above.filter((hex, i) => hex !== unshaded.above[i]).length;
|
||
check(
|
||
'a camera shader that changes nothing draws the game picture byte for byte, the right way up',
|
||
unshaded.pixels === null &&
|
||
handedThrough.pixels !== null &&
|
||
is(unshaded.blueAbove, [0x33, 0x66, 0xff]) &&
|
||
!is(unshaded.blueBelow, [0x33, 0x66, 0xff]) &&
|
||
same(handedThrough, unshaded),
|
||
JSON.stringify({ differing, pixels: handedThrough.pixels, blue: handedThrough.blueAbove }),
|
||
);
|
||
|
||
// 63f. A flat red fills the main camera's whole picture, corners included; the overlay camera's
|
||
// UI square lies on top of it in its own green (the proof moved here from #302).
|
||
await wear(FLAT_RED);
|
||
const { frame } = await json('__gm.gameScale()');
|
||
const corners = await json(
|
||
`__gm.pixels(${JSON.stringify([
|
||
{ x: frame.x + 2, y: frame.y + 2 },
|
||
{ x: frame.x + frame.w - 3, y: frame.y + 2 },
|
||
{ x: frame.x + 2, y: frame.y + frame.h - 3 },
|
||
{ x: frame.x + frame.w - 3, y: frame.y + frame.h - 3 },
|
||
])})`,
|
||
);
|
||
const reddened = await picture();
|
||
await shot('63-camera-shader-red');
|
||
check(
|
||
'a flat red fills the whole game picture, and the overlay camera’s UI keeps its colour',
|
||
corners.every((hex) => is(hex, RED)) && is(reddened.left, RED) && is(reddened.centre, GREEN),
|
||
JSON.stringify({ corners, reddened }),
|
||
);
|
||
|
||
// 63g. `Texture size` is the camera's raster in either resolution – a pixel counts as a camera
|
||
// pixel (the user's choice, #299) – while Screen pixels draws the picture with as many pixels
|
||
// as it covers on screen. Fitted, so that is not 640 by chance.
|
||
await mutate(`__gm.state.setGameScale('fit')`);
|
||
const measured = {};
|
||
for (const resolution of ['screen', 'camera']) {
|
||
await wear(SIZE, resolution);
|
||
measured[resolution] = {
|
||
colour: (await picture()).left,
|
||
pixels: await json('__gm.cameraShaderPixels()'),
|
||
};
|
||
}
|
||
const fitted = await json('__gm.gameScale()');
|
||
const ratio = (await json('__gm.pixelGrid()')).resolution;
|
||
// Tight: counted in screen pixels the fitted 670 would come out as 171 / 96, close to the real thing.
|
||
const exactly = (hex, rgb) => channels(hex).every((v, i) => Math.abs(v - rgb[i]) <= 2);
|
||
check(
|
||
'Texture size is View width × View height in both resolutions, and only Camera pixels draws 640 × 360',
|
||
['screen', 'camera'].every((r) => exactly(measured[r].colour, [163, 92, 0])) &&
|
||
measured.camera.pixels?.w === 640 &&
|
||
measured.camera.pixels?.h === 360 &&
|
||
measured.screen.pixels?.w === Math.round(fitted.frame.w * ratio) &&
|
||
measured.screen.pixels?.w !== 640,
|
||
JSON.stringify({ measured, frame: fitted.frame, ratio }),
|
||
);
|
||
|
||
// 63h. Taken off again, the camera draws as before and lays no picture over its view.
|
||
await mutate(`__gm.state.setGameScale(1)`);
|
||
await wear(null);
|
||
const back = await rows();
|
||
check(
|
||
'a camera without a shader draws as it did before',
|
||
back.pixels === null && same(back, unshaded),
|
||
JSON.stringify({ pixels: back.pixels }),
|
||
);
|
||
|
||
// 63i. The scene view lays the shader over the main camera's frame only while the eye's Shader
|
||
// switch is on – a real click on it – and never past the frame.
|
||
await wear(FLAT_RED);
|
||
await showTab('scene');
|
||
await evaluate(`(() => {
|
||
const main = __gm.state.activeScene.objects.find((o) => o.id === 'o3');
|
||
__gm.lookAt(main.position.x, main.position.y);
|
||
})()`);
|
||
await sleep(150);
|
||
const sceneSpots = async () => {
|
||
const r = await json('__gm.objectRect("o3")');
|
||
const [inside, outside] = await json(
|
||
`__gm.pixels(${JSON.stringify([
|
||
{ x: Math.round(r.x - r.w * 0.3), y: Math.round(r.y - r.h * 0.3) },
|
||
{ x: Math.round(r.x - r.w * 0.7), y: Math.round(r.y - r.h * 0.3) },
|
||
])})`,
|
||
);
|
||
return { inside, outside, pixels: await json(`__gm.cameraShaderPixels('scene')`) };
|
||
};
|
||
const switchedOff = await sceneSpots();
|
||
const eye = await json(`__gm.sceneHeaderAt('visibility')`);
|
||
await t.click(eye.x, eye.y);
|
||
const shaderSwitch = (await json('__gm.visibilityPopup()')).find(
|
||
(e) => e.type === 'camera' && e.part === 'shader',
|
||
);
|
||
await t.click(shaderSwitch.x, shaderSwitch.y);
|
||
await press('Escape');
|
||
await sleep(150);
|
||
const switchedOn = await sceneSpots();
|
||
await shot('63-camera-shader-scene');
|
||
check(
|
||
'the scene view shows the camera shader over the frame only while the eye switch is on',
|
||
shaderSwitch.on === false &&
|
||
switchedOff.pixels === null &&
|
||
!is(switchedOff.inside, RED) &&
|
||
switchedOn.pixels !== null &&
|
||
is(switchedOn.inside, RED) &&
|
||
!is(switchedOn.outside, RED),
|
||
JSON.stringify({ switchedOff, switchedOn, shaderSwitch }),
|
||
);
|
||
|
||
// Clean up.
|
||
await showTab('scene');
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('rl63-'));
|
||
scene.objects.find((o) => o.id === 'o3').overrides = window.__rl63.overrides;
|
||
p.renderLayers = window.__rl63.layers;
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('rl63-'));
|
||
__gm.state.setSceneVisibility('camera', 'shader', false);
|
||
__gm.state.setGameScale(${JSON.stringify(scaleBefore)});
|
||
`);
|
||
}
|