Let a graph read and change the z of an object
Until now a behavior could not move an object in front of another while the game runs. Set position writes only x and y, and z lives on the running object rather than in its props, so Properties cannot reach it either. Drawing already reads the z every frame, so only the nodes were missing. - Get Z (data): object in, z and world z out. World z is summed up with the parents, the way drawing does it (worldTransformOf). - Set Z (action): object and z in. It sets the object's own z, local to its parent like Set position. Higher is drawn in front from the next frame on. - Two nodes of their own rather than a z port on Set position and Get position, as asked. Tests: - node-catalog: Get Z for a child (2 below 5 is world z 7) and for no object, Set Z writes only z and leaves x/y. - set-z.test.ts: in a running world a key sets the lower of two sprites to z 2, drawSlots then draws it in front, Get Z logs 2 and 2. - Two counter-checks bite: Set Z without the write and Get Z without the parent fail 3 tests. - Gate: 233 files, 3548 tests (3542 + 6), CHECK EXIT 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e05cbbed5
commit
8452a99c64
5 files changed
+110
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asEntity, asNumber } from '../../values';
|
||||
import { exec, num, obj } from '../shared';
|
||||
|
||||
export const SetZ: NodeDef = {
|
||||
type: 'SetZ',
|
||||
label: 'Set Z',
|
||||
category: 'action',
|
||||
description: 'Sets the z of an object, local to its parent: higher is drawn in front.',
|
||||
inputs: [exec(), obj('object'), num('z')],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (target) target.z = asNumber(inputs.z);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { worldTransformOf } from '../../NodeContext';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asEntity } from '../../values';
|
||||
import { num, obj } from '../shared';
|
||||
|
||||
export const GetZ: NodeDef = {
|
||||
type: 'GetZ',
|
||||
label: 'Get Z',
|
||||
category: 'data',
|
||||
description:
|
||||
'The z of an object, which decides what is drawn in front: local to its parent (z) and summed up (world z).',
|
||||
inputs: [obj('object')],
|
||||
outputs: [num('z'), num('worldZ', 'world z')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
return { outputs: { z: target?.z ?? 0, worldZ: target ? worldTransformOf(target).z : 0 } };
|
||||
},
|
||||
};
|
||||
@@ -23,6 +23,7 @@ import { Raycast } from './action/Raycast';
|
||||
import { RaycastAll } from './action/RaycastAll';
|
||||
import { RestartGame } from './action/RestartGame';
|
||||
import { SetPosition } from './action/SetPosition';
|
||||
import { SetZ } from './action/SetZ';
|
||||
import { Spawn } from './action/Spawn';
|
||||
import { StopGame } from './action/StopGame';
|
||||
import { StopSound } from './action/StopSound';
|
||||
@@ -43,6 +44,7 @@ import { FindText } from './data/FindText';
|
||||
import { FirstNotEmpty } from './data/FirstNotEmpty';
|
||||
import { Floor } from './data/Floor';
|
||||
import { GetPosition } from './data/GetPosition';
|
||||
import { GetZ } from './data/GetZ';
|
||||
import { ChildByName } from './data/ChildByName';
|
||||
import { Children } from './data/Children';
|
||||
import { ChildrenByName } from './data/ChildrenByName';
|
||||
@@ -199,6 +201,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
IsEnabled,
|
||||
Properties,
|
||||
GetPosition,
|
||||
GetZ,
|
||||
TextureSize,
|
||||
AnimationPlaying,
|
||||
AnimationLength,
|
||||
@@ -266,6 +269,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
Move,
|
||||
MoveToward,
|
||||
SetPosition,
|
||||
SetZ,
|
||||
GetVelocity,
|
||||
ApplyForce,
|
||||
ApplyImpulse,
|
||||
|
||||
@@ -752,6 +752,14 @@ const CASES: Record<string, Case[]> = {
|
||||
outputs: { x: 0, y: 0, worldX: 0, worldY: 0 },
|
||||
},
|
||||
],
|
||||
GetZ: [
|
||||
{
|
||||
name: 'a child reports its own z and the z summed up with its parents',
|
||||
inputs: { object: { ...at('child', 0, 0), z: 2, parent: { ...at('parent', 0, 0), z: 5 } } },
|
||||
outputs: { z: 2, worldZ: 7 },
|
||||
},
|
||||
{ name: 'no object is 0', inputs: { object: null }, outputs: { z: 0, worldZ: 0 } },
|
||||
],
|
||||
|
||||
Number: [
|
||||
{ name: 'outputs its setting', params: { value: 7.5 }, outputs: { value: 7.5 } },
|
||||
@@ -1500,6 +1508,10 @@ const CASES: Record<string, Case[]> = {
|
||||
},
|
||||
{ name: 'no object is a no-op', inputs: { object: null, x: 1, y: 1 }, next: 'exec' },
|
||||
],
|
||||
SetZ: [
|
||||
{ name: 'sets the z and continues', inputs: { object: at('z', 0, 0), z: 3 }, next: 'exec' },
|
||||
{ name: 'no object is a no-op', inputs: { object: null, z: 3 }, next: 'exec' },
|
||||
],
|
||||
ApplyForce: [
|
||||
{
|
||||
name: 'pushes for the length of the frame: force × dt, handed on as an impulse',
|
||||
@@ -2142,6 +2154,12 @@ describe('action nodes write through to the object they are given', () => {
|
||||
expect({ x: target.x, y: target.y }).toEqual({ x: 30, y: -40 });
|
||||
});
|
||||
|
||||
it('SetZ sets only the z', () => {
|
||||
const target = { ...at('z', 10, 20), z: 1 };
|
||||
runDef('SetZ', fakeContext(), { object: target, z: -4 }, {});
|
||||
expect({ x: target.x, y: target.y, z: target.z }).toEqual({ x: 10, y: 20, z: -4 });
|
||||
});
|
||||
|
||||
it('Destroy hands the object to the context, children and all unless told to keep them', () => {
|
||||
const removed: string[] = [];
|
||||
const moved = [fakeEntity('Lid'), fakeEntity('Nail')];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { PlaceAssetCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { drawSlots } from '../../src/viewport/drawOrder';
|
||||
import { collectWorldItems } from '../../src/viewport/items';
|
||||
import { graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
|
||||
|
||||
/**
|
||||
* Two sprites on the same spot, "front" at z 1 and "back" at z 0. On `f` a behavior on "back" sets
|
||||
* its z to 2 with `Set Z` and logs `Get Z` of it: z, then world z.
|
||||
*/
|
||||
function start() {
|
||||
const f = makeSpriteScene();
|
||||
const back = place(f, f.spriteId, { x: 0, y: 0, z: 0 });
|
||||
const front = place(f, f.spriteId, { x: 0, y: 0, z: 1 });
|
||||
const behavior = makeBehavior(f, 'Raise');
|
||||
f.stack.execute(new PlaceAssetCommand(f.sceneId, behavior.id, { x: 0, y: 0 }, back));
|
||||
const edit = graphEditor(f, behavior);
|
||||
const owner = edit.addNode('Owner');
|
||||
const set = edit.addNode('SetZ', { z: 2 });
|
||||
edit.connect(edit.addNode('OnKeyDown', { key: 'f' }), 'exec', set, 'exec');
|
||||
edit.connect(owner, 'object', set, 'object');
|
||||
const get = edit.addNode('GetZ');
|
||||
edit.connect(owner, 'object', get, 'object');
|
||||
let from = set;
|
||||
for (const output of ['z', 'worldZ']) {
|
||||
const log = edit.addNode('Log');
|
||||
edit.connect(get, output, log, 'message');
|
||||
edit.connect(from, 'exec', log, 'exec');
|
||||
from = log;
|
||||
}
|
||||
|
||||
const started = startWorld(f, { behaviors: true });
|
||||
const name = (id: string) => (id === back ? 'back' : id === front ? 'front' : id);
|
||||
const order = () =>
|
||||
drawSlots(collectWorldItems(started.world).items.filter((item) => item.type === 'sprite')).map((slot) =>
|
||||
name(slot.id),
|
||||
);
|
||||
started.frame();
|
||||
return { ...started, order };
|
||||
}
|
||||
|
||||
describe('a graph changes the z of an object during the run', () => {
|
||||
it('draws the object in front once Set Z has raised it, and Get Z reads the new z', () => {
|
||||
const r = start();
|
||||
expect(r.order()).toEqual(['back', 'front']);
|
||||
r.input.press('f');
|
||||
r.frame();
|
||||
expect(r.order()).toEqual(['front', 'back']);
|
||||
expect(r.world.log).toEqual(['2', '2']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user