Files
feuerwerk/tools/e2e/57-object-references.mjs
T
adminandClaude Opus 5.5 f84defb9b6
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 10m2s
Pick an object of the same scene for an Object variable (#258)
An Object variable could only be set by a graph while the game runs.
A placed object always showed "(null)", although the other objects of
its scene are there to choose from and the runtime can already find
any object by its id. Now the placed object stores a reference to one
of them, and the running game turns it into the real object.

- Stored as {"$object": "<id>"}. The empty placeholder stays as it
  was, so old projects load unchanged and empty still means "(null)".
- The row of an Object variable on a placed object is a dropdown with
  None and every object of the scene, indented like the Hierarchy.
  The same goes for Object fields in structs and Object items in
  lists, also in the Variables and List tabs.
- The behavior asset keeps "(null)". An asset has no scene, so "Apply
  to asset" is gone from Object rows.
- The runtime resolves the reference when the Variables or Properties
  node reads it. A destroyed target reads as null, a missing one shows
  "(missing)" in the row.
- Inside a container only objects of the same container are offered.
  The reference keeps the template id, so every placement finds its
  own copy.
- Deleting the target clears the reference in the same undo step.
  Paste points references inside the pasted group at the copies and
  leaves outside references alone. Create container and Unpack carry
  references along with the new ids.
- A value a graph writes while running stays in the run. The scene
  keeps the picked object, also in "Run in editor".

Found on the way:

- Setting an override coerced every object slot to empty, in plain
  values, struct fields and list items. The shared objectPlace() now
  keeps the reference.

Not done:

- No eyedropper to pick in the scene, the dropdown is enough for now.
- Deleting a template object in the container editor does not clean up
  references in other scenes. They read as null there.

Tests:

- ./check.sh: 187 files, 2724 tests, CHECK EXIT 0
- e2e 1-9, 51, 57: ALL PASSED, 389 checks
- e2e 1, 57 on the final bundle: ALL PASSED, 17 checks
- 13 unit tests in object-references.test.ts, 5 checks in e2e 57
- Counter-checks: 8 unit breaks fail their tests, the browser break
  without a pick list fails 57b-57d

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:35:17 +02:00

128 lines
4.8 KiB
JavaScript

/**
* Picking an object of the same scene for an object variable (#258), with the real mouse: the
* placed behavior offers None and the scene's objects, the pick is stored as the object's id and a
* run reads the object itself. Removing that object drops the reference; undo brings it back. On the
* behavior asset there is no scene, so the row still says "(null)".
*
* Everything it adds carries an `or57` id and goes again at the end.
*/
import { MAKE_OBJECT } from './lib/scaffold.mjs';
export async function objectReferences(t) {
const { check, json, evaluate, mutate, click, press, sleep, shot, select, undo, stop } = t;
const press_ = async (at, what) => {
if (!at) throw new Error(`nothing to click: ${what}`);
await click(at.x, at.y);
await sleep(150);
};
const target = () =>
json("__gm.state.activeScene.objects.find((o) => o.id === 'or57-obj')?.overrides.target ?? null");
// On start the behavior logs the object its `target` names.
const graph = {
nodes: [
{ id: 'or57-start', type: 'OnStart', x: 0, y: 0, params: {} },
{
id: 'or57-vars',
type: 'Variables',
x: 0,
y: 200,
params: {},
ports: [{ key: 'target', kind: 'object' }],
},
{ id: 'or57-log', type: 'Log', x: 320, y: 0, params: {} },
],
edges: [
{ id: 'or57-e1', from: { node: 'or57-start', port: 'exec' }, to: { node: 'or57-log', port: 'exec' } },
{
id: 'or57-e2',
from: { node: 'or57-vars', port: 'target' },
to: { node: 'or57-log', port: 'message' },
},
],
};
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
p.assets.push({
id: 'or57-aim',
type: 'behavior',
name: 'Aim 57',
props: { enabled: true, runInEditor: false, target: { $object: '' } },
variables: [{ key: 'target', label: 'target', kind: 'object', default: { $object: '' } }],
graph: ${JSON.stringify(graph)},
});
${MAKE_OBJECT}
const sprite = p.assets.find((a) => a.type === 'sprite');
scene.objects.push(make('or57-obj', 'or57-aim', '', 400, 'Aim 57'));
scene.objects.push(make('or57-crate', sprite.id, '', 450, 'Crate 57'));
`);
const sceneId = await json('__gm.state.activeScene.id');
// 57a. The behavior asset has no scene: its object variable only says "(null)".
await select({ kind: 'asset', assetId: 'or57-aim' });
await sleep(200);
check(
'on the behavior asset the object variable says (null)',
(await json("__gm.locate('(null)')")) !== null,
);
// 57b. On the placed object the row is a dropdown: None, the object itself and the Crate. A click
// on the Crate stores its id.
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-obj' });
await sleep(200);
await press_(await json("__gm.inspectorField('target')"), 'the target field');
const offered = (await json('__gm.overlayTexts()')) ?? [];
await shot('01-menu');
check(
'the placed object offers None, itself and the other objects of the scene',
['None', 'Aim 57', 'Crate 57'].every((label) => offered.includes(label)),
JSON.stringify(offered),
);
await press_(await json("__gm.locate('Crate 57', true)"), 'Crate 57 in the menu');
const picked = await target();
check(
'picking the Crate stores its id and the row shows its name',
JSON.stringify(picked) === JSON.stringify({ $object: 'or57-crate' }) &&
(await json("__gm.locate('Crate 57')")) !== null,
JSON.stringify(picked),
);
await shot('02-picked');
// 57c. A run reads the object itself.
await evaluate('__gm.state.select(null)');
const logFrom = t.consoleLog.length;
await t.play();
await sleep(600);
await stop();
const logged = t.consoleLog
.slice(logFrom)
.filter((line) => line.startsWith('[behavior Aim 57]'))
.map((line) => line.split(']').pop().trim());
check('in a run the variable is the picked object', logged.includes('Crate 57'), JSON.stringify(logged));
// 57d. Removing the Crate drops the reference with it; one undo brings both back.
await select({ kind: 'object', holderId: sceneId, objectId: 'or57-crate' });
await sleep(150);
await press('Delete');
await sleep(150);
const dropped = await target();
await undo();
const back = await target();
check(
'removing the picked object drops the reference, undo brings it back',
JSON.stringify(dropped) === JSON.stringify({ $object: '' }) &&
JSON.stringify(back) === JSON.stringify({ $object: 'or57-crate' }),
JSON.stringify({ dropped, back }),
);
// Clean up.
await evaluate('__gm.state.select(null)');
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
scene.objects = scene.objects.filter((o) => !o.id.startsWith('or57-'));
p.assets = p.assets.filter((a) => a.id !== 'or57-aim');
`);
}