Pick an existing collider or rigidbody from the object menu (#222)
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m52s

"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.

- "Add collider…" and "Add rigidbody…" open a second menu at the same
  spot: a search field, "Create new …", a separator, then every asset
  of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
  the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
  the collider commands. An object without a resolvable asset is
  greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
  sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
  collider can now sit on several sprites. A value equal to the asset
  value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
  90 degrees and gives a circle the radius that fits (min/2). It is
  greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
  "Add Component" in Unity. That costs one click more than before.

Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
  existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
  centring each fail at their own check. Breaking the override rule
  fails 2 unit tests.
- CI confirmed 1345 checks at 2cf7202 (task 265). New total 1348,
  calculated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-23 18:29:34 +02:00
1 parent 2cf72022eb
commit abf3b1f618
9 files changed
+361 -72

No files matched your search

+64 -7
View File
@@ -33,14 +33,35 @@ export async function colliders(t) {
stop,
} = t;
// 8a. Right-click the red crate → "Add collider": a collider asset sized like the crate
// (100×60) is placed as its child at the centre, selected and drawn in the overlay. The
// seed already gives both crates a "Crate collider" child (oc1/oc2), so the added one is
// undone again to keep exactly one collider per body for the checks below.
// 8a. Right-click the red crate → "Add collider…" opens a second, searchable menu (#222): a new
// collider or one the project has, the shared "Crate collider" the crate wears greyed out.
// "Create new collider" places a collider asset sized like the crate (100×60) as its child at
// the centre, selected and drawn in the overlay. The seed already gives both crates a
// "Crate collider" child (oc1/oc2), so the added one is undone again to keep exactly one
// collider per body for the checks below.
const objectsBefore = (await objects()).length;
const redRow = await locate('Red crate');
await click(redRow.x, redRow.y, 'right');
await clickText('Add collider');
const assetCount = () => json('__gm.state.project.assets.length');
const colliderMenuOf = async (id) => {
await evaluate('__gm.hierarchyExpandAll()');
const row = await inView(t, '__gm.leftColumn().objects', `__gm.hierarchyRow(${JSON.stringify(id)})`);
await click(row.x, row.y, 'right');
await clickText('Add collider…');
};
await colliderMenuOf('o1');
const pickRows = await json('__gm.menuRows()');
const pickEnabled = await json('__gm.menuEnabled()');
await typeText('crate');
const searched = ((await json('__gm.menuRows()')) ?? []).filter(Boolean);
await press('Escape');
check(
'Add collider… offers a new or an existing collider, searchable; one the crate wears is greyed out',
JSON.stringify(pickRows) === JSON.stringify(['Create new collider', null, 'Crate collider']) &&
JSON.stringify(pickEnabled) === JSON.stringify([true, null, false]) &&
JSON.stringify(searched) === JSON.stringify(['Crate collider']),
JSON.stringify({ pickRows, pickEnabled, searched }),
);
await colliderMenuOf('o1');
await clickText('Create new collider');
const s = await state();
const colObj = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
const colAsset = colObj ? await asset(colObj.assetId) : null;
@@ -58,6 +79,22 @@ export async function colliders(t) {
);
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box')`)));
await shot('21-collider');
// The blue crate takes that new collider as it is: its name typed and Enter place it below the
// crate, no second asset made.
const assetsWithNew = await assetCount();
await colliderMenuOf('o2');
if (colAsset) await typeText(colAsset.name);
await press('Enter');
const sShared = await state();
const shared = sShared.selection?.kind === 'object' ? await object(sShared.selection.objectId) : null;
const assetsAfterShare = await assetCount();
check(
'picking an existing collider places it below the blue crate without a new asset',
shared?.parentId === 'o2' && shared.assetId === colAsset?.id && assetsAfterShare === assetsWithNew,
JSON.stringify({ shared, assetsWithNew, assetsAfterShare }),
);
if (shared) await undo();
await undo();
const colliderAssetGone = colAsset ? !(await asset(colAsset.id)) : false;
check(
@@ -65,6 +102,26 @@ export async function colliders(t) {
(await objects()).length === objectsBefore && colliderAssetGone,
);
// 8a-iii. "Fit to sprite" on a collider (#222): the blue crate's collider, pushed aside and
// narrowed, covers its 140-wide crate again and sits centred – which is how the seed has it.
await mutate(`
const oc2 = __gm.state.activeScene.objects.find(o => o.id === 'oc2');
oc2.position = { x: 30, y: 12, z: 0 };
oc2.overrides = { width: 90 };`);
const oc2Row = await inView(t, '__gm.leftColumn().objects', `__gm.hierarchyRow('oc2')`);
await click(oc2Row.x, oc2Row.y, 'right');
const colliderRows = ((await json('__gm.menuRows()')) ?? []).filter(Boolean);
if (colliderRows.includes('Fit to sprite')) await clickText('Fit to sprite');
else await press('Escape');
const fitted = await object('oc2');
check(
'Fit to sprite sizes the collider like its sprite and centres it on the sprite',
JSON.stringify(fitted?.overrides) === JSON.stringify({ width: 140 }) &&
fitted.position.x === 0 &&
fitted.position.y === 0,
JSON.stringify({ colliderRows, fitted }),
);
// 8b. Wire "On collision enter → Log(other)" into the Mover (owner = red crate) and play with
// the crates overlapping: the log names the blue crate exactly once (enter, not stay),
// and colliders are not rendered in play mode.
+3 -1
View File
@@ -302,7 +302,9 @@ export async function containers(t) {
const before = (await objects()).length;
const noticeNow = await notice();
await click(row.x, row.y, 'right');
await clickText('Add collider');
await clickText('Add collider…');
// An enabled entry opens the second menu (#222); a disabled one leaves the first standing.
if (await has('Create new collider')) await clickText('Create new collider');
await press('Escape');
return { added: (await objects()).length - before, notice: await noticeNow() };
};
+2 -2
View File
@@ -771,8 +771,8 @@ export async function tilemap(t) {
const OBJECT_MENU = [
'Select asset',
'Open asset',
'Add collider',
'Add rigidbody',
'Add collider…',
'Add rigidbody…',
'Create container',
'Unpack container',
'Unparent (move to top level)',
+3 -1
View File
@@ -125,7 +125,9 @@ export async function physics(t) {
const row = await inView(t, '__gm.leftColumn().objects', `__gm.hierarchyRow(${JSON.stringify(id)})`);
const before = (await objects()).length;
await click(row.x, row.y, 'right');
await clickText('Add rigidbody');
await clickText('Add rigidbody…');
// An enabled entry opens the second menu (#222); a disabled one leaves the first standing.
if (await has('Create new rigidbody')) await clickText('Create new rigidbody');
await press('Escape');
const s = await state();
const selected = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;