Files
feuerwerk/tests/core/collider-parts.test.ts
T
adminandClaude Opus 5.5 abf3b1f618
game-maker / gate (push) Successful in 3m8s
game-maker / suite (push) Successful in 9m52s
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>
2026-09-23 18:29:34 +02:00

111 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
import {
AddColliderCommand,
AddRigidbodyCommand,
colliderFit,
FitColliderCommand,
} from '../../src/core/commands/colliderCommands';
import { MoveSceneObjectCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { makeBehavior, makeSpriteScene, place } from '../helpers';
/** A 100×60 crate placed twice, the first with a collider of its own. */
function setup() {
const f = makeSpriteScene('Crate');
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, 'height', 60));
const a = place(f, f.spriteId, { x: 0, y: 0 });
const b = place(f, f.spriteId, { x: 300, y: 0 });
const collider = new AddColliderCommand(f.sceneId, a);
f.stack.execute(collider);
return { ...f, a, b, collider };
}
describe('adding an existing collider or rigidbody (#222)', () => {
it('places the existing collider below the object without a new asset; undo keeps the asset', () => {
const f = setup();
const count = f.project.assets.length;
const reuse = new AddColliderCommand(f.sceneId, f.b, f.collider.assetId);
f.stack.execute(reuse);
expect(f.project.assets).toHaveLength(count);
expect(reuse.assetId).toBe(f.collider.assetId);
expect(f.object(reuse.objectId)).toMatchObject({ assetId: f.collider.assetId, parentId: f.b });
f.stack.undo();
expect(f.sceneOf().objects.some((o) => o.id === reuse.objectId)).toBe(false);
expect(f.project.assets.some((x) => x.id === f.collider.assetId)).toBe(true);
});
it('refuses an asset of the wrong kind and keeps the rules of a new part', () => {
const f = setup();
const behavior = makeBehavior(f, 'Hit');
expect(() => f.stack.execute(new AddColliderCommand(f.sceneId, f.b, behavior.id))).toThrow(
/not a collider/,
);
expect(() =>
f.stack.execute(new AddColliderCommand(f.sceneId, f.collider.objectId, f.collider.assetId)),
).toThrow(/cannot have a collider/);
const body = new AddRigidbodyCommand(f.sceneId, f.a);
f.stack.execute(body);
const shared = new AddRigidbodyCommand(f.sceneId, f.b, body.assetId);
f.stack.execute(shared);
expect(f.object(shared.objectId)).toMatchObject({ assetId: body.assetId, parentId: f.b });
expect(() => f.stack.execute(new AddRigidbodyCommand(f.sceneId, f.b, body.assetId))).toThrow(
/has a rigidbody already/,
);
});
});
describe('fitting a collider to its sprite (#222)', () => {
it('covers the sprite as it is now and centres the collider; undo puts everything back', () => {
const f = setup();
const { collider } = f;
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, 'width', 240));
f.stack.execute(new MoveSceneObjectCommand(f.sceneId, collider.objectId, { x: 30, y: -10, z: 2 }));
f.stack.execute(new FitColliderCommand(f.sceneId, collider.objectId));
expect(f.object(collider.objectId).overrides).toEqual({ width: 240 });
expect(f.object(collider.objectId).position).toEqual({ x: 0, y: 0, z: 2 });
f.stack.undo();
expect(f.object(collider.objectId).overrides).toEqual({});
expect(f.object(collider.objectId).position).toEqual({ x: 30, y: -10, z: 2 });
});
it('writes this placement only, so a collider shared by two sprites keeps its asset size', () => {
const f = setup();
f.stack.execute(new SetOverrideCommand(f.sceneId, f.b, 'width', 50));
const shared = new AddColliderCommand(f.sceneId, f.b, f.collider.assetId);
f.stack.execute(shared);
f.stack.execute(new FitColliderCommand(f.sceneId, shared.objectId));
expect(f.object(shared.objectId).overrides).toEqual({ width: 50 });
expect(f.object(f.collider.objectId).overrides).toEqual({});
});
it('divides out the collider’s scale, swaps for a quarter turn and fits a circle inside', () => {
const f = setup();
const id = f.collider.objectId;
const fit = () => colliderFit(f.project, f.sceneOf(), f.object(id));
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'scale', 2));
expect(fit()).toEqual({ width: 50, height: 30 });
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'rotation', 90));
expect(fit()).toEqual({ width: 30, height: 50 });
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'shape', 'circle'));
expect(fit()).toEqual({ radius: 15 });
});
it('says why a collider cannot be fitted', () => {
const f = setup();
f.stack.execute(new SetOverrideCommand(f.sceneId, f.collider.objectId, 'shape', 'polygon'));
expect(colliderFit(f.project, f.sceneOf(), f.object(f.collider.objectId))).toMatch(/polygon/);
expect(colliderFit(f.project, f.sceneOf(), f.object(f.a))).toMatch(/Only a collider/);
const text = new CreateAssetCommand('text', 'Label');
f.stack.execute(text);
const label = place(f, text.asset.id, { x: 0, y: 200 });
const onText = new AddColliderCommand(f.sceneId, label);
f.stack.execute(onText);
expect(colliderFit(f.project, f.sceneOf(), f.object(onText.objectId))).toMatch(/not sit on a sprite/);
expect(() => f.stack.execute(new FitColliderCommand(f.sceneId, onText.objectId))).toThrow(
/not sit on a sprite/,
);
});
});