Let Mouse position and Is hovered see through any camera (#304)
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>
This commit is contained in:
1 parent
1d0fa7b947
commit
70f3608733
15 files changed
+286
-17
No files matched your search
@@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1729 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1730 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -162,6 +162,11 @@ export interface NodeContext {
|
||||
wasKeyPressed(key: string): boolean;
|
||||
wasKeyReleased(key: string): boolean;
|
||||
pointer: PointerState;
|
||||
/**
|
||||
* The mouse as `camera` sees it (#304): world units through that camera, and whether the mouse is
|
||||
* on its picture. The main camera gives `pointer`; a camera not drawn is never under the mouse.
|
||||
*/
|
||||
pointerThrough(camera: EntityHandle): { x: number; y: number; inside: boolean };
|
||||
/**
|
||||
* Whether a world point lies on `target`: on its colliders when it has any, otherwise on its own
|
||||
* footprint or that of anything parented below it.
|
||||
|
||||
@@ -6,13 +6,15 @@ export const IsHovered: NodeDef = {
|
||||
type: 'IsHovered',
|
||||
label: 'Is hovered',
|
||||
category: 'data',
|
||||
description: "Whether the mouse is over an object (its colliders, else its own or its children's footprint).",
|
||||
inputs: [obj('object')],
|
||||
description:
|
||||
"Whether the mouse is over an object (its colliders, else its own or its children's footprint) – as the main camera sees it, or the camera given.",
|
||||
inputs: [obj('object'), obj('camera')],
|
||||
outputs: [bool('result')],
|
||||
params: [],
|
||||
run: (ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const { x, y, inside } = ctx.pointer;
|
||||
const camera = asEntity(inputs.camera);
|
||||
const { x, y, inside } = camera ? ctx.pointerThrough(camera) : ctx.pointer;
|
||||
return { outputs: { result: Boolean(target && inside && ctx.hitTest(target, x, y)) } };
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { bool, num } from '../shared';
|
||||
import { asEntity } from '../../values';
|
||||
import { bool, num, obj } from '../shared';
|
||||
|
||||
export const MousePosition: NodeDef = {
|
||||
type: 'MousePosition',
|
||||
label: 'Mouse position',
|
||||
category: 'data',
|
||||
description: 'Where the mouse is in the Game tab, in world units.',
|
||||
inputs: [],
|
||||
description:
|
||||
'Where the mouse is in the Game tab, in world units – through the main camera, or through the camera given.',
|
||||
inputs: [obj('camera')],
|
||||
outputs: [num('x'), num('y'), bool('inside', 'in view')],
|
||||
params: [],
|
||||
run: (ctx) => ({ outputs: { x: ctx.pointer.x, y: ctx.pointer.y, inside: ctx.pointer.inside } }),
|
||||
run: (ctx, inputs) => {
|
||||
const camera = asEntity(inputs.camera);
|
||||
const { x, y, inside } = camera ? ctx.pointerThrough(camera) : ctx.pointer;
|
||||
return { outputs: { x, y, inside } };
|
||||
},
|
||||
};
|
||||
@@ -25,6 +25,29 @@ export function cameraSize(props: PropertyBag): { w: number; h: number } {
|
||||
return { w: Number(props.width ?? DEFAULTS.width), h: Number(props.height ?? DEFAULTS.height) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the mouse is in the world as `camera` sees it, with that camera's picture fitted into the
|
||||
* main camera's and centred, the way #302 lays an overlay on. `onPicture` is the mouse on the main
|
||||
* picture, from its centre in parts of its width and height; `onMain` whether it is on it at all.
|
||||
* `inside` is then whether the mouse is on this camera's picture.
|
||||
*/
|
||||
export function pointerThrough(
|
||||
onPicture: { x: number; y: number },
|
||||
onMain: boolean,
|
||||
mainProps: PropertyBag,
|
||||
camera: { x: number; y: number; props: PropertyBag },
|
||||
): { x: number; y: number; inside: boolean } {
|
||||
const main = cameraFrame(mainProps);
|
||||
const frame = cameraFrame(camera.props);
|
||||
// Units of the main frame per unit of this camera's.
|
||||
const fit = Math.min(main.w / frame.w, main.h / frame.h);
|
||||
if (!(fit > 0) || !Number.isFinite(fit)) return { x: camera.x, y: camera.y, inside: false };
|
||||
const dx = onPicture.x * main.w;
|
||||
const dy = onPicture.y * main.h;
|
||||
const inside = onMain && Math.abs(dx) <= (frame.w * fit) / 2 && Math.abs(dy) <= (frame.h * fit) / 2;
|
||||
return { x: camera.x + dx / fit, y: camera.y + dy / fit, inside };
|
||||
}
|
||||
|
||||
export function rectAround(cx: number, cy: number, w: number, h: number): WorldRect {
|
||||
return { left: cx - w / 2, top: cy - h / 2, right: cx + w / 2, bottom: cy + h / 2 };
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export class GameViewport extends WorldViewport<EditorState> {
|
||||
this,
|
||||
() => this.pointerWorld(),
|
||||
() => this.currentView(),
|
||||
() => this.mainCamera(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export class GameScreen extends WorldViewport {
|
||||
this,
|
||||
() => this.running,
|
||||
() => this.currentView(),
|
||||
() => this.mainCamera(),
|
||||
);
|
||||
this.on('pointerdown', (e) => {
|
||||
if (e.button === 0 && !this.running && this.startable) this.onStart?.();
|
||||
|
||||
+33
-3
@@ -1,4 +1,10 @@
|
||||
import { ENTITY_HANDLE, isHandleEnabled, type PointerState, worldMatrixOf } from '../core/graph';
|
||||
import {
|
||||
ENTITY_HANDLE,
|
||||
isHandleEnabled,
|
||||
type PointerState,
|
||||
worldMatrixOf,
|
||||
worldPositionOf,
|
||||
} from '../core/graph';
|
||||
import { newId } from '../core/ids';
|
||||
import { type Asset, isPrivateVariable, type TilemapAsset } from '../core/model/Asset';
|
||||
import { sameAtRest, withoutObjects } from '../core/graph/liveObjects';
|
||||
@@ -17,6 +23,7 @@ import { apply, IDENTITY } from '../core/model/matrix';
|
||||
import { hasTransformProps } from '../core/properties/PropertySchema';
|
||||
import { carriedTransform, localMatrixOf, transformOf } from '../core/properties/transform';
|
||||
import { footprintContains } from '../core/properties/bounds';
|
||||
import { pointerThrough } from '../core/properties/camera';
|
||||
import { colliderFrame, colliderShape, shapeContains } from '../core/properties/collider';
|
||||
import { resolveSceneObjectProps } from '../core/properties/resolve';
|
||||
import type { Entity } from './Entity';
|
||||
@@ -129,6 +136,8 @@ export class World {
|
||||
released: false,
|
||||
wheel: 0,
|
||||
};
|
||||
/** The mouse on the main camera's picture, from its centre in parts of its width and height (#304). */
|
||||
private pointerOnPicture: Vec2 = { x: 0, y: 0 };
|
||||
/** Objects the editor is dragging (ids): the physics puts them where the drag does, at rest. */
|
||||
held: ReadonlySet<string> = new Set();
|
||||
/** Messages logged by behaviors (Log node), newest last. */
|
||||
@@ -484,11 +493,32 @@ export class World {
|
||||
this.pointer.wheel = 0;
|
||||
}
|
||||
|
||||
/** Game view: where the mouse is (world units) and whether it is over the view at all. */
|
||||
movePointer(x: number, y: number, inside: boolean): void {
|
||||
/**
|
||||
* Game view: where the mouse is (world units), whether it is over the view at all, and where it
|
||||
* lies on the main camera's picture (see `pointerOnPicture`).
|
||||
*/
|
||||
movePointer(x: number, y: number, inside: boolean, onPicture: Vec2 = { x: 0, y: 0 }): void {
|
||||
this.pointer.x = x;
|
||||
this.pointer.y = y;
|
||||
this.pointer.inside = inside;
|
||||
this.pointerOnPicture = onPicture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the mouse is in the world as `camera` sees it (#304). The main camera answers `pointer`.
|
||||
* Kept on the picture rather than in the world, so a main camera on the move does not drag the
|
||||
* mouse across an overlay. Only a camera that is drawn can have the mouse on its picture.
|
||||
*/
|
||||
pointerThrough(camera: Entity): { x: number; y: number; inside: boolean } {
|
||||
const { x, y, inside } = this.pointer;
|
||||
const main = this.activeCamera();
|
||||
if (camera === main) return { x, y, inside };
|
||||
if (!main || camera.type !== 'camera') return { x, y, inside: false };
|
||||
const seen = pointerThrough(this.pointerOnPicture, inside, main.props, {
|
||||
...worldPositionOf(camera),
|
||||
props: camera.props,
|
||||
});
|
||||
return { ...seen, inside: seen.inside && this.overlayCameras().includes(camera) };
|
||||
}
|
||||
|
||||
/** Game view: the wheel turned by `notches`; what turns within one tick adds up. */
|
||||
|
||||
@@ -76,6 +76,11 @@ export function behaviorContext(
|
||||
wasKeyPressed: (key) => run.input.justPressed.has(keyName(key)),
|
||||
wasKeyReleased: (key) => run.input.justReleased.has(keyName(key)),
|
||||
pointer: world.pointer,
|
||||
pointerThrough: (camera) => {
|
||||
const found = resolve(camera);
|
||||
const { x, y } = world.pointer;
|
||||
return found ? world.pointerThrough(found) : { x, y, inside: false };
|
||||
},
|
||||
hitTest: (target, x, y) => {
|
||||
const found = resolve(target);
|
||||
return Boolean(found && world.hitTest(found, x, y));
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import type { Container, FederatedMouseEvent, FederatedPointerEvent, FederatedWheelEvent } from 'pixi.js';
|
||||
import { cameraFrame } from '../core/properties/camera';
|
||||
import type { World } from '../runtime/World';
|
||||
import type { ActiveCamera } from './activeCamera';
|
||||
import { isInside, screenToWorld, type View, wheelNotches } from './view';
|
||||
|
||||
/**
|
||||
* Hands the mouse over `target` to a running world: position, left button and wheel. Shared by the
|
||||
* editor's game view and a released game (#266). Outside the camera frame counts as "not in view":
|
||||
* that region is letterbox, not game.
|
||||
* that region is letterbox, not game. `camera` is the main camera `view` shows.
|
||||
*/
|
||||
export function routePointer(target: Container, world: () => World | null, view: () => View | null): void {
|
||||
export function routePointer(
|
||||
target: Container,
|
||||
world: () => World | null,
|
||||
view: () => View | null,
|
||||
camera: () => ActiveCamera | null,
|
||||
): void {
|
||||
const report = (e: FederatedMouseEvent): void => {
|
||||
const w = world();
|
||||
const v = view();
|
||||
if (!w || !v) return;
|
||||
const c = camera();
|
||||
if (!w || !v || !c) return;
|
||||
const local = target.toLocal(e.global);
|
||||
const point = screenToWorld(v, local.x, local.y);
|
||||
w.movePointer(point.x, point.y, isInside(v, local.x, local.y));
|
||||
const frame = cameraFrame(c.props);
|
||||
const onPicture = { x: (point.x - v.x) / frame.w, y: (point.y - v.y) / frame.h };
|
||||
w.movePointer(point.x, point.y, isInside(v, local.x, local.y), onPicture);
|
||||
};
|
||||
target.on('pointerdown', (e: FederatedPointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
|
||||
@@ -1318,6 +1318,16 @@ const CASES: Record<string, Case[]> = {
|
||||
context: pointer({ x: 5, y: 5, inside: false }),
|
||||
outputs: { x: 5, y: 5, inside: false },
|
||||
},
|
||||
{
|
||||
name: 'through the camera given, where that camera sees the mouse',
|
||||
inputs: { camera: fakeEntity('Ui camera') },
|
||||
context: {
|
||||
...pointer({ x: 100, y: 50, inside: true }),
|
||||
pointerThrough: (camera) =>
|
||||
camera.id === 'Ui camera' ? { x: 1050, y: 25, inside: false } : { x: 0, y: 0, inside: true },
|
||||
},
|
||||
outputs: { x: 1050, y: 25, inside: false },
|
||||
},
|
||||
],
|
||||
MouseButton: [
|
||||
{
|
||||
@@ -1355,6 +1365,26 @@ const CASES: Record<string, Case[]> = {
|
||||
context: { ...pointer({ inside: true }), hitTest: () => true },
|
||||
outputs: { result: false },
|
||||
},
|
||||
{
|
||||
name: 'through the camera given, the test is where that camera sees the mouse',
|
||||
inputs: { object: fakeEntity('Button'), camera: fakeEntity('Ui camera') },
|
||||
context: {
|
||||
...pointer({ x: 100, y: 50, inside: true }),
|
||||
pointerThrough: () => ({ x: 1050, y: 25, inside: true }),
|
||||
hitTest: (_, x, y) => x === 1050 && y === 25,
|
||||
},
|
||||
outputs: { result: true },
|
||||
},
|
||||
{
|
||||
name: 'off the picture of the camera given, nothing is hovered',
|
||||
inputs: { object: fakeEntity('Button'), camera: fakeEntity('Ui camera') },
|
||||
context: {
|
||||
...pointer({ x: 1050, y: 25, inside: true }),
|
||||
pointerThrough: () => ({ x: 1050, y: 25, inside: false }),
|
||||
hitTest: () => true,
|
||||
},
|
||||
outputs: { result: false },
|
||||
},
|
||||
],
|
||||
IsTouching: [
|
||||
{
|
||||
|
||||
@@ -42,9 +42,9 @@ describe('play settings in the project', () => {
|
||||
expect(gameScaleOf({ gameScale: 'fit' })).toBe('fit');
|
||||
expect(gameScaleOf({ gameScale: 0.25 })).toBe(0.25);
|
||||
expect(gameScaleOf({ gameScale: 3 })).toBe(3);
|
||||
expect(gameScaleOf({ gameScale: 6 })).toBe(6);
|
||||
expect(gameScaleOf({ gameScale: 20 })).toBe(20);
|
||||
expect(gameScaleOf({ gameScale: 2.5 })).toBe(1);
|
||||
expect(gameScaleOf({ gameScale: 8 })).toBe(1);
|
||||
expect(gameScaleOf({ gameScale: 21 })).toBe(1);
|
||||
expect(gameScaleOf({ gameScale: '2' })).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -238,6 +238,7 @@ export function fakeContext(overrides: Partial<NodeContext> = {}): FakeContext {
|
||||
wasKeyPressed: () => false,
|
||||
wasKeyReleased: () => false,
|
||||
pointer: { ...NO_POINTER },
|
||||
pointerThrough: () => ({ x: 0, y: 0, inside: false }),
|
||||
hitTest: () => false,
|
||||
isTouching: () => false,
|
||||
raycast: () => null,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { pointerThrough } from '../../src/core/properties/camera';
|
||||
import { OVERLAY_KEY } from '../../src/core/model/renderLayers';
|
||||
import { cameraView, isInside, overlayView, screenToWorld } from '../../src/viewport/view';
|
||||
import { makeSpriteScene, place, startWorld } from '../helpers';
|
||||
|
||||
const MAIN = { width: 640, height: 360, zoom: 1 };
|
||||
|
||||
describe('the mouse through a camera (#304)', () => {
|
||||
it('lands where the overlay picture #302 draws shows that world point', () => {
|
||||
const main = cameraView({ x: 0, y: 0, props: MAIN }, 1280, 720, 2);
|
||||
const cameras = [
|
||||
{ x: 1000, y: 0, props: { ...MAIN, zoom: 2 } },
|
||||
{ x: 50, y: 7, props: { width: 320, height: 360, zoom: 2 } },
|
||||
];
|
||||
for (const camera of cameras) {
|
||||
const drawn = overlayView(camera, main, { w: 640, h: 360 });
|
||||
for (const [sx, sy] of [
|
||||
[640, 360],
|
||||
[840, 460],
|
||||
[100, 700],
|
||||
[330, 10],
|
||||
] as const) {
|
||||
const onMain = screenToWorld(main, sx, sy);
|
||||
const seen = pointerThrough({ x: onMain.x / 640, y: onMain.y / 360 }, true, MAIN, camera);
|
||||
const expected = screenToWorld(drawn, sx, sy);
|
||||
expect(seen.x).toBeCloseTo(expected.x, 9);
|
||||
expect(seen.y).toBeCloseTo(expected.y, 9);
|
||||
expect(seen.inside).toBe(isInside(drawn, sx, sy));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('gives one mouse two world points through the main camera and a zoomed overlay', () => {
|
||||
const f = makeSpriteScene();
|
||||
const cam = new CreateAssetCommand('camera', 'Cam');
|
||||
f.stack.execute(cam);
|
||||
const set = (id: string, key: string, value: number | boolean) =>
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, id, key, value));
|
||||
const mainId = place(f, cam.asset.id, { x: 0, y: 0 });
|
||||
const overId = place(f, cam.asset.id, { x: 1000, y: 0 });
|
||||
const plainId = place(f, cam.asset.id, { x: -1000, y: 0 });
|
||||
set(overId, OVERLAY_KEY, true);
|
||||
set(overId, 'zoom', 2);
|
||||
const { world } = startWorld(f);
|
||||
const [main, over, plain] = [mainId, overId, plainId].map((id) => world.findByObjectId(id)!);
|
||||
|
||||
world.movePointer(100, 50, true, { x: 100 / 640, y: 50 / 360 });
|
||||
expect(world.pointerThrough(main!)).toEqual({ x: 100, y: 50, inside: true });
|
||||
expect(world.pointerThrough(over!)).toEqual({ x: 1050, y: 25, inside: true });
|
||||
// A second plain camera is not drawn, so the mouse is never on its picture.
|
||||
expect(world.pointerThrough(plain!).inside).toBe(false);
|
||||
|
||||
// The main camera moving on under a mouse that stands still does not move it on the overlay.
|
||||
main!.x = 500;
|
||||
expect(world.pointerThrough(over!)).toEqual({ x: 1050, y: 25, inside: true });
|
||||
|
||||
world.movePointer(100, 50, false, { x: 100 / 640, y: 50 / 360 });
|
||||
expect(world.pointerThrough(over!).inside).toBe(false);
|
||||
|
||||
world.movePointer(100, 50, true, { x: 100 / 640, y: 50 / 360 });
|
||||
set(overId, 'enabled', false);
|
||||
world.syncFromModel();
|
||||
expect(world.pointerThrough(over!)).toMatchObject({ x: 1050, y: 25, inside: false });
|
||||
});
|
||||
|
||||
it('hovers a button only the overlay shows through that camera only', () => {
|
||||
const f = makeSpriteScene('Button');
|
||||
const cam = new CreateAssetCommand('camera', 'Cam');
|
||||
f.stack.execute(cam);
|
||||
place(f, cam.asset.id, { x: 0, y: 0 });
|
||||
const overId = place(f, cam.asset.id, { x: 1000, y: 0 });
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, overId, OVERLAY_KEY, true));
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, overId, 'zoom', 2));
|
||||
const buttonId = place(f, f.spriteId, { x: 1050, y: 25 });
|
||||
const { world } = startWorld(f);
|
||||
const button = world.findByObjectId(buttonId)!;
|
||||
const over = world.findByObjectId(overId)!;
|
||||
|
||||
world.movePointer(100, 50, true, { x: 100 / 640, y: 50 / 360 });
|
||||
const { x, y } = world.pointerThrough(over);
|
||||
expect(world.hitTest(button, x, y)).toBe(true);
|
||||
expect(world.hitTest(button, world.pointer.x, world.pointer.y)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
* 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.
|
||||
*
|
||||
@@ -119,6 +120,73 @@ export async function renderLayers(t) {
|
||||
);
|
||||
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…');
|
||||
|
||||
Reference in new issue
Block a user