Pick an existing collider or rigidbody from the object menu (#222)
"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:
1 parent
2cf72022eb
commit
abf3b1f618
9 files changed
+361
-72
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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() };
|
||||
};
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user