Add a node that names the render layer of an object (#305)
The issue asked for Set render layer, Set sees and Render layer of. As with #211, a probe showed that writing already works. A Properties node of the object's type writes Render layer during a run, Inherit included, and one of type Camera writes Sees. The next picture follows both. As agreed, only Render layer of is built, and writing stays with Properties. - Render layer of is a data node with the input object and the outputs name and number. It answers the layer that holds, so an object without a choice of its own names its parent's. - NodeContext.renderLayerOf answers the number with the same rule the drawing uses (renderLayerAlong). The new field renderLayers on GraphLookup brings the names to the node. - hasRenderLayer in PropertySchema says which types are drawn. - Decided here: for an object that is not drawn (a camera, a behavior, a collider) the name is empty and the number -1, as Layer of does without a collider. Changing the render layer or Sees during a run had no test so far. Three unit tests now hold the proof from the issue. Tests: - Gate: 231 files, 3534 tests (3526 + 8), exit 0 - e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1) - Unit: the coin moves into the overlay with Set and back with Inherit, a HUD container takes its sprite along, the main camera sees a second layer once its Sees is set, and Render layer of names the inherited layer, and none for a behavior - Node catalog: four cases for Render layer of - 63c3 moves a sprite onto UI in a Play run: the main camera no longer draws it, and the log reads "UI" and "1" - Three counter-probes fail 1, 1 and 2 tests: without inheriting in the node, without the check for a drawn type, and without inheriting in the drawing - "layer of" in the palette still finds Layer of first (6z-x) - The e2e sum is 1731 (1730 plus 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70f3608733
commit
d9c6bbfa4d
12 files changed
+250
-8
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:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1730 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1731 checks (GM_STEPS=20 for one step)
|
||||||
# every tool signs in first – see the Accounts page
|
# every tool signs in first – see the Accounts page
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
|
|||||||
@@ -205,6 +205,11 @@ export interface NodeContext {
|
|||||||
* other object's that of its first collider child; null for an object without a collider.
|
* other object's that of its first collider child; null for an object without a collider.
|
||||||
*/
|
*/
|
||||||
collisionLayerOf(target: EntityHandle): number | null;
|
collisionLayerOf(target: EntityHandle): number | null;
|
||||||
|
/**
|
||||||
|
* The number of the render layer `target` is drawn on (#305): its own choice, else its parent's,
|
||||||
|
* at the top 0; null for an object that is not drawn.
|
||||||
|
*/
|
||||||
|
renderLayerOf(target: EntityHandle): number | null;
|
||||||
/** Lets `target` fall through one-way colliders and tiles for `seconds` (#155). */
|
/** Lets `target` fall through one-way colliders and tiles for `seconds` (#155). */
|
||||||
ignoreOneWay(target: EntityHandle, seconds: number): void;
|
ignoreOneWay(target: EntityHandle, seconds: number): void;
|
||||||
/** Whether `target` is a placed tilemap – what the tile nodes act on (#135). */
|
/** Whether `target` is a placed tilemap – what the tile nodes act on (#135). */
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export interface StateMachine {
|
|||||||
* What a node may look up in the project around its graph: the behaviors it can run, the state
|
* What a node may look up in the project around its graph: the behaviors it can run, the state
|
||||||
* machines it can read, the curves a `Lerp` eases along (#247) and the struct types `Add item`
|
* machines it can read, the curves a `Lerp` eases along (#247) and the struct types `Add item`
|
||||||
* checks (#254), what a variable written by a graph is held to (#271), how long an animation is
|
* checks (#254), what a variable written by a graph is held to (#271), how long an animation is
|
||||||
* (#233) and what the collision layers are called (#211). One handle, so no caller can resolve a
|
* (#233) and what the collision and render layers are called (#211, #305). One handle, so no caller
|
||||||
* node with half of it.
|
* can resolve a node with half of it.
|
||||||
*/
|
*/
|
||||||
export interface GraphLookup {
|
export interface GraphLookup {
|
||||||
behavior(assetId: string): BehaviorProgram | null;
|
behavior(assetId: string): BehaviorProgram | null;
|
||||||
@@ -62,6 +62,8 @@ export interface GraphLookup {
|
|||||||
animation(assetId: string): AnimationClip | null;
|
animation(assetId: string): AnimationClip | null;
|
||||||
/** The project's collision layers by number, as `Layer of` names them (#211). */
|
/** The project's collision layers by number, as `Layer of` names them (#211). */
|
||||||
collisionLayers(): readonly string[];
|
collisionLayers(): readonly string[];
|
||||||
|
/** The project's render layers by number, as `Render layer of` names them (#305). */
|
||||||
|
renderLayers(): readonly string[];
|
||||||
/** The asset whose graph this is, `''` for none. */
|
/** The asset whose graph this is, `''` for none. */
|
||||||
ownerOf(graph: Graph): string;
|
ownerOf(graph: Graph): string;
|
||||||
/** What a variable of that asset declares about the structs it holds (#271); none for nothing typed. */
|
/** What a variable of that asset declares about the structs it holds (#271); none for nothing typed. */
|
||||||
@@ -94,6 +96,7 @@ export const NO_LOOKUP: GraphLookup = {
|
|||||||
textureSize: () => null,
|
textureSize: () => null,
|
||||||
animation: () => null,
|
animation: () => null,
|
||||||
collisionLayers: () => [],
|
collisionLayers: () => [],
|
||||||
|
renderLayers: () => [],
|
||||||
ownerOf: () => '',
|
ownerOf: () => '',
|
||||||
variableTyping: () => undefined,
|
variableTyping: () => undefined,
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { layerName } from '../../../model/collisionLayers';
|
||||||
|
import type { NodeDef } from '../../NodeRegistry';
|
||||||
|
import { asEntity } from '../../values';
|
||||||
|
import { num, obj, str } from '../shared';
|
||||||
|
|
||||||
|
export const RenderLayerOf: NodeDef = {
|
||||||
|
type: 'RenderLayerOf',
|
||||||
|
label: 'Render layer of',
|
||||||
|
category: 'data',
|
||||||
|
description:
|
||||||
|
'The render layer an object is drawn on, by name and by number: its own, else the one it ' +
|
||||||
|
'inherits from its parent. For an object that is not drawn the name is empty and the number -1.',
|
||||||
|
inputs: [obj('object')],
|
||||||
|
outputs: [str('name'), num('number')],
|
||||||
|
params: [],
|
||||||
|
read: (ctx, _params, input, _def, lookup) => {
|
||||||
|
const target = asEntity(input('object'));
|
||||||
|
const layer = target ? ctx.renderLayerOf(target) : null;
|
||||||
|
if (layer === null) return { name: '', number: -1 };
|
||||||
|
return { name: layerName(lookup.renderLayers(), layer), number: layer };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -88,6 +88,7 @@ import { PackStruct } from './data/PackStruct';
|
|||||||
import { Properties } from './data/Properties';
|
import { Properties } from './data/Properties';
|
||||||
import { Parent } from './data/Parent';
|
import { Parent } from './data/Parent';
|
||||||
import { Random } from './data/Random';
|
import { Random } from './data/Random';
|
||||||
|
import { RenderLayerOf } from './data/RenderLayerOf';
|
||||||
import { Round } from './data/Round';
|
import { Round } from './data/Round';
|
||||||
import { Select } from './data/Select';
|
import { Select } from './data/Select';
|
||||||
import { Self } from './data/Self';
|
import { Self } from './data/Self';
|
||||||
@@ -261,6 +262,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
|||||||
IsHovered,
|
IsHovered,
|
||||||
IsTouching,
|
IsTouching,
|
||||||
LayerOf,
|
LayerOf,
|
||||||
|
RenderLayerOf,
|
||||||
Move,
|
Move,
|
||||||
MoveToward,
|
MoveToward,
|
||||||
SetPosition,
|
SetPosition,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function graphLookup(project: Project): GraphLookup {
|
|||||||
return asset ? clipOf(asset) : null;
|
return asset ? clipOf(asset) : null;
|
||||||
},
|
},
|
||||||
collisionLayers: () => project.collisionLayers,
|
collisionLayers: () => project.collisionLayers,
|
||||||
|
renderLayers: () => project.renderLayers,
|
||||||
ownerOf: (graph) => project.assets.find((a) => isGraphAsset(a) && a.graph === graph)?.id ?? '',
|
ownerOf: (graph) => project.assets.find((a) => isGraphAsset(a) && a.graph === graph)?.id ?? '',
|
||||||
variableTyping: (assetId, key) => {
|
variableTyping: (assetId, key) => {
|
||||||
const owner = findAsset(project, assetId);
|
const owner = findAsset(project, assetId);
|
||||||
|
|||||||
@@ -402,8 +402,13 @@ const RENDER_LAYER_TYPES: ReadonlySet<AssetType> = new Set([
|
|||||||
'tilemap',
|
'tilemap',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** Whether objects of that type are drawn and so sit on a render layer. */
|
||||||
|
export function hasRenderLayer(type: AssetType): boolean {
|
||||||
|
return RENDER_LAYER_TYPES.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
function renderLayerPropsOf(type: AssetType): PropertySchema {
|
function renderLayerPropsOf(type: AssetType): PropertySchema {
|
||||||
return RENDER_LAYER_TYPES.has(type) ? [RENDER_LAYER_ROW] : [];
|
return hasRenderLayer(type) ? [RENDER_LAYER_ROW] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMON_KEYS = new Set(
|
const COMMON_KEYS = new Set(
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import type { Vec2 } from '../../core/model/Properties';
|
|||||||
import { JOINT_CONNECTED_KEY } from '../../core/model/joint';
|
import { JOINT_CONNECTED_KEY } from '../../core/model/joint';
|
||||||
import { objectReference } from '../../core/model/objectRefs';
|
import { objectReference } from '../../core/model/objectRefs';
|
||||||
import { findAsset, findAssetOf, findScene, type Project } from '../../core/model/Project';
|
import { findAsset, findAssetOf, findScene, type Project } from '../../core/model/Project';
|
||||||
|
import { renderLayerAlong } from '../../core/model/renderLayers';
|
||||||
import { soundFileOf } from '../../core/model/sound';
|
import { soundFileOf } from '../../core/model/sound';
|
||||||
|
import { hasRenderLayer } from '../../core/properties/PropertySchema';
|
||||||
import { resolveAssetProps } from '../../core/properties/resolve';
|
import { resolveAssetProps } from '../../core/properties/resolve';
|
||||||
import { loopsOf, volumeOf } from '../../core/properties/sounds';
|
import { loopsOf, volumeOf } from '../../core/properties/sounds';
|
||||||
import type { Entity } from '../Entity';
|
import type { Entity } from '../Entity';
|
||||||
@@ -128,6 +130,15 @@ export function behaviorContext(
|
|||||||
const holder = found && layerHolderOf(world, found);
|
const holder = found && layerHolderOf(world, found);
|
||||||
return holder ? collisionFilterOf(holder.props).layer : null;
|
return holder ? collisionFilterOf(holder.props).layer : null;
|
||||||
},
|
},
|
||||||
|
renderLayerOf: (target) => {
|
||||||
|
const found = resolve(target);
|
||||||
|
if (!found || !hasRenderLayer(found.type)) return null;
|
||||||
|
return renderLayerAlong(
|
||||||
|
found,
|
||||||
|
(at) => at.props,
|
||||||
|
(at) => at.parent ?? null,
|
||||||
|
);
|
||||||
|
},
|
||||||
ignoreOneWay: (target, seconds) => {
|
ignoreOneWay: (target, seconds) => {
|
||||||
const found = resolve(target);
|
const found = resolve(target);
|
||||||
if (found) world.ignoreOneWay(found, seconds);
|
if (found) world.ignoreOneWay(found, seconds);
|
||||||
|
|||||||
@@ -609,6 +609,28 @@ const CASES: Record<string, Case[]> = {
|
|||||||
},
|
},
|
||||||
{ name: 'no object is on no layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
|
{ name: 'no object is on no layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
|
||||||
],
|
],
|
||||||
|
RenderLayerOf: [
|
||||||
|
{
|
||||||
|
name: 'names the render layer the context says the object is drawn on (#305)',
|
||||||
|
inputs: { object: fakeEntity('Coin') },
|
||||||
|
context: { renderLayerOf: (target) => (target.name === 'Coin' ? 1 : null) },
|
||||||
|
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default', 'UI'] },
|
||||||
|
outputs: { name: 'UI', number: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a render layer the project has not named goes by its number',
|
||||||
|
inputs: { object: fakeEntity('Coin') },
|
||||||
|
context: { renderLayerOf: () => 3 },
|
||||||
|
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default'] },
|
||||||
|
outputs: { name: 'Layer 3', number: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'an object that is not drawn is on no render layer',
|
||||||
|
inputs: { object: fakeEntity('Cam') },
|
||||||
|
outputs: { name: '', number: -1 },
|
||||||
|
},
|
||||||
|
{ name: 'no object is on no render layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
|
||||||
|
],
|
||||||
ObjectsByName: [
|
ObjectsByName: [
|
||||||
{
|
{
|
||||||
name: 'every object so named, in scene order',
|
name: 'every object so named, in scene order',
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ export function fakeContext(overrides: Partial<NodeContext> = {}): FakeContext {
|
|||||||
raycast: () => null,
|
raycast: () => null,
|
||||||
raycastAll: () => [],
|
raycastAll: () => [],
|
||||||
collisionLayerOf: () => null,
|
collisionLayerOf: () => null,
|
||||||
|
renderLayerOf: () => null,
|
||||||
ignoreOneWay: () => {},
|
ignoreOneWay: () => {},
|
||||||
isTilemap: () => false,
|
isTilemap: () => false,
|
||||||
tileAt: () => null,
|
tileAt: () => null,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||||
import { SetLayerNamesCommand } from '../../src/core/commands/projectCommands';
|
import { SetLayerNamesCommand } from '../../src/core/commands/projectCommands';
|
||||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||||
|
import type { GraphNode } from '../../src/core/graph';
|
||||||
|
import { OBJECT_TYPE_PARAM } from '../../src/core/graph/propertyPorts';
|
||||||
|
import type { PropertyBag } from '../../src/core/model/Properties';
|
||||||
import {
|
import {
|
||||||
cameraSees,
|
cameraSees,
|
||||||
INHERIT_LAYER,
|
INHERIT_LAYER,
|
||||||
@@ -13,10 +16,15 @@ import {
|
|||||||
} from '../../src/core/model/renderLayers';
|
} from '../../src/core/model/renderLayers';
|
||||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||||
import { settingsChanges } from '../../src/editor/ProjectSettingsDialog';
|
import { settingsChanges } from '../../src/editor/ProjectSettingsDialog';
|
||||||
import { cameraOfItems, overlaysOfItems, overlaysOfWorld } from '../../src/viewport/activeCamera';
|
import {
|
||||||
|
cameraOfItems,
|
||||||
|
cameraOfWorld,
|
||||||
|
overlaysOfItems,
|
||||||
|
overlaysOfWorld,
|
||||||
|
} from '../../src/viewport/activeCamera';
|
||||||
import { collectHolderItems, collectWorldItems } from '../../src/viewport/items';
|
import { collectHolderItems, collectWorldItems } from '../../src/viewport/items';
|
||||||
import { cameraView, overlayView } from '../../src/viewport/view';
|
import { cameraView, overlayView } from '../../src/viewport/view';
|
||||||
import { makeSpriteScene, place, startWorld } from '../helpers';
|
import { type GraphEditor, graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
|
||||||
|
|
||||||
function sceneWithCamera() {
|
function sceneWithCamera() {
|
||||||
const fixture = makeSpriteScene();
|
const fixture = makeSpriteScene();
|
||||||
@@ -134,3 +142,116 @@ describe('render layers (#302)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A coin on `Default` and a HUD container with a sprite in it, a main camera that sees `Default` and
|
||||||
|
* an overlay that sees `UI`. Behaviors change the layers with Properties nodes, the way a graph does
|
||||||
|
* it (#305): the coin goes to `UI` on `u` and back to Inherit on `i`, the HUD to `UI` on `h`, and the
|
||||||
|
* main camera sees both layers on `s`. On `l` the HUD's sprite asks `Render layer of` of itself, of
|
||||||
|
* the HUD and of its behavior, which is not drawn.
|
||||||
|
*/
|
||||||
|
function layerSwitchScene() {
|
||||||
|
const f = sceneWithCamera();
|
||||||
|
f.stack.execute(new SetLayerNamesCommand('renderLayers', ['Default', 'UI']));
|
||||||
|
const coin = place(f, f.spriteId, { x: 0, y: 0 });
|
||||||
|
const hud = place(f, f.containerId, { x: 0, y: 0 });
|
||||||
|
const inside = place(f, f.spriteId, { x: 0, y: 0 }, hud);
|
||||||
|
const main = place(f, f.cameraId, { x: 0, y: 0 });
|
||||||
|
const over = place(f, f.cameraId, { x: 0, y: 0 });
|
||||||
|
f.set(main, SEES_KEY, 0b01);
|
||||||
|
f.set(over, SEES_KEY, 0b10);
|
||||||
|
f.set(over, OVERLAY_KEY, true);
|
||||||
|
|
||||||
|
const behaviorOn = (objectId: string, name: string) => {
|
||||||
|
const behavior = makeBehavior(f, name);
|
||||||
|
f.stack.execute(new PlaceAssetCommand(f.sceneId, behavior.id, { x: 0, y: 0 }, objectId));
|
||||||
|
return graphEditor(f, behavior);
|
||||||
|
};
|
||||||
|
const setOnKey = (edit: GraphEditor, key: string, type: string, port: string, value: number) => {
|
||||||
|
const set = edit.properties([{ key: port, kind: 'number', write: true }], {
|
||||||
|
[OBJECT_TYPE_PARAM]: type,
|
||||||
|
[port]: value,
|
||||||
|
});
|
||||||
|
edit.connect(edit.addNode('Owner'), 'object', set, 'object');
|
||||||
|
edit.connect(edit.addNode('OnKeyDown', { key }), 'exec', set, 'exec');
|
||||||
|
};
|
||||||
|
const onCoin = behaviorOn(coin, 'Coin');
|
||||||
|
setOnKey(onCoin, 'u', 'sprite', RENDER_LAYER_KEY, 1);
|
||||||
|
setOnKey(onCoin, 'i', 'sprite', RENDER_LAYER_KEY, INHERIT_LAYER);
|
||||||
|
const onHud = behaviorOn(hud, 'Hud');
|
||||||
|
setOnKey(onHud, 'h', 'container', RENDER_LAYER_KEY, 1);
|
||||||
|
const onCamera = behaviorOn(main, 'Cam');
|
||||||
|
setOnKey(onCamera, 's', 'camera', SEES_KEY, 0b11);
|
||||||
|
|
||||||
|
const onInside = behaviorOn(inside, 'Inside');
|
||||||
|
let from = onInside.addNode('OnKeyDown', { key: 'l' });
|
||||||
|
const ask = (source: GraphNode, port: string) => {
|
||||||
|
const of = onInside.addNode('RenderLayerOf');
|
||||||
|
onInside.connect(source, port, of, 'object');
|
||||||
|
for (const output of ['name', 'number']) {
|
||||||
|
const log = onInside.addNode('Log');
|
||||||
|
onInside.connect(of, output, log, 'message');
|
||||||
|
onInside.connect(from, 'exec', log, 'exec');
|
||||||
|
from = log;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const owner = onInside.addNode('Owner');
|
||||||
|
const parent = onInside.addNode('Parent');
|
||||||
|
onInside.connect(owner, 'object', parent, 'object');
|
||||||
|
ask(owner, 'object');
|
||||||
|
ask(parent, 'parent');
|
||||||
|
ask(onInside.addNode('Self'), 'object');
|
||||||
|
return { ...f, coin, hud, inside };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('a graph changes render layers and what a camera sees during the run (#305)', () => {
|
||||||
|
const start = () => {
|
||||||
|
const s = layerSwitchScene();
|
||||||
|
const started = startWorld(s, { behaviors: true });
|
||||||
|
const press = (key: string) => {
|
||||||
|
started.input.press(key);
|
||||||
|
started.frame();
|
||||||
|
started.input.release(key);
|
||||||
|
};
|
||||||
|
const seenBy = (camera: { props: PropertyBag } | null) => {
|
||||||
|
const items = collectWorldItems(started.world).items.filter((item) => item.type === 'sprite');
|
||||||
|
const seen = items.filter((item) => camera && cameraSees(camera.props, item.renderLayer));
|
||||||
|
return seen.map((item) => (item.id === s.coin ? 'coin' : item.id === s.inside ? 'inside' : item.id));
|
||||||
|
};
|
||||||
|
const main = () => seenBy(cameraOfWorld(started.world));
|
||||||
|
const overlay = () => seenBy(overlaysOfWorld(started.world)[0]!);
|
||||||
|
started.frame();
|
||||||
|
return { ...s, ...started, press, main, overlay };
|
||||||
|
};
|
||||||
|
|
||||||
|
it('moves the coin into the overlay with Set, and back with Inherit', () => {
|
||||||
|
const r = start();
|
||||||
|
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
|
||||||
|
r.press('u');
|
||||||
|
expect([r.main(), r.overlay()]).toEqual([['inside'], ['coin']]);
|
||||||
|
r.press('i');
|
||||||
|
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes the HUD container’s sprite along when the container changes', () => {
|
||||||
|
const r = start();
|
||||||
|
r.press('h');
|
||||||
|
expect([r.main(), r.overlay()]).toEqual([['coin'], ['inside']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets the main camera see a second layer once its Sees is set', () => {
|
||||||
|
const r = start();
|
||||||
|
r.press('h');
|
||||||
|
r.press('s');
|
||||||
|
expect(r.main()).toEqual(['coin', 'inside']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names the layer that holds, the inherited one too, and none for what is not drawn', () => {
|
||||||
|
const r = start();
|
||||||
|
r.press('l');
|
||||||
|
expect(r.world.log).toEqual(['Default', '0', 'Default', '0', '', '-1']);
|
||||||
|
r.press('h');
|
||||||
|
r.press('l');
|
||||||
|
expect(r.world.log.slice(6)).toEqual(['UI', '1', 'UI', '1', '', '-1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
* a green sprite of the overlay with z -10 covers a red one of the main camera with z 1000, and
|
* 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
|
* 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.
|
* `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).
|
* `Is hovered` and `Mouse position` find the mouse through the overlay camera too (#304), and a
|
||||||
|
* graph moves a sprite onto another layer while the game runs (#305).
|
||||||
* Then the main camera wears a shader over its picture (#299), in the Game tab and, switched on in
|
* 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.
|
* the eye, in the scene view.
|
||||||
*
|
*
|
||||||
@@ -187,6 +188,54 @@ export async function renderLayers(t) {
|
|||||||
JSON.stringify({ hovered, overlayX }),
|
JSON.stringify({ hovered, overlayX }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 63c3. (#305) A graph moves the red square A onto `UI` with a Properties node once the game runs:
|
||||||
|
// the main camera no longer draws it, and `Render layer of` names its new layer.
|
||||||
|
const switchGraph = {
|
||||||
|
nodes: [
|
||||||
|
node('rl63-start', 'OnStart', 0),
|
||||||
|
node('rl63-a-find', 'ObjectByName', 0, { name: 'A 63' }),
|
||||||
|
{
|
||||||
|
...node('rl63-set', 'Properties', 200, { objectType: 'sprite', renderLayer: 1 }),
|
||||||
|
ports: [{ key: 'renderLayer', kind: 'number', write: true }],
|
||||||
|
},
|
||||||
|
node('rl63-of', 'RenderLayerOf', 200),
|
||||||
|
node('rl63-name', 'Log', 400),
|
||||||
|
node('rl63-number', 'Log', 600),
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
edge('rl63-s1', 'rl63-start', 'exec', 'rl63-set', 'exec'),
|
||||||
|
edge('rl63-s2', 'rl63-set', 'exec', 'rl63-name', 'exec'),
|
||||||
|
edge('rl63-s3', 'rl63-name', 'exec', 'rl63-number', 'exec'),
|
||||||
|
edge('rl63-s4', 'rl63-a-find', 'object', 'rl63-set', 'object'),
|
||||||
|
edge('rl63-s5', 'rl63-a-find', 'object', 'rl63-of', 'object'),
|
||||||
|
edge('rl63-s6', 'rl63-of', 'name', 'rl63-name', 'message'),
|
||||||
|
edge('rl63-s7', 'rl63-of', 'number', 'rl63-number', 'message'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
p.assets.push({ id: 'rl63-switch', type: 'behavior', name: 'Switch 63', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(switchGraph)} });
|
||||||
|
${MAKE_OBJECT}
|
||||||
|
scene.objects.push(make('rl63-switcher', 'rl63-switch', '', 0, 'Switch 63'));
|
||||||
|
`);
|
||||||
|
await play();
|
||||||
|
await sleep(400);
|
||||||
|
const switched = await picture();
|
||||||
|
const named = (await json('__gm.world()'))?.log.slice(-2) ?? [];
|
||||||
|
await stop();
|
||||||
|
await mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
scene.objects = scene.objects.filter((o) => o.id !== 'rl63-switcher');
|
||||||
|
p.assets = p.assets.filter((a) => a.id !== 'rl63-switch');
|
||||||
|
`);
|
||||||
|
check(
|
||||||
|
'a graph moves a sprite onto UI while the game runs, and Render layer of names it',
|
||||||
|
!switched.main.includes('rl63-a') && !is(switched.right, RED) && JSON.stringify(named) === '["UI","1"]',
|
||||||
|
JSON.stringify({ switched, named }),
|
||||||
|
);
|
||||||
|
|
||||||
// 63d. The project settings list the render layers, apart from the collision layers.
|
// 63d. The project settings list the render layers, apart from the collision layers.
|
||||||
await t.clickToolbar('menu');
|
await t.clickToolbar('menu');
|
||||||
await clickText('Project settings…');
|
await clickText('Project settings…');
|
||||||
|
|||||||
Reference in new issue
Block a user