"Add collider" on a sprite always made a box of the sprite's size, even when the sprite's picture is a frame that has a collision outline. The outline then had to be picked by hand as "Frame collision". - newColliderProps in colliderCommands.ts is the rule as a pure function. A sprite whose picture is a frame with an outline gets a collider with shape "frame" and that frame. Anything else keeps the box of the sprite's size. - The frame is the one the sprite shows when the collider is added, also when only this placement overrides it. - Decided here, the issue says nothing about size: a frame collider is as big as its frame, so a sprite drawn bigger or smaller with the same proportions gets the collider's scale set to match. - Decided here as well: a sprite stretched out of the frame's proportions keeps the box, because an outline cannot be stretched in one direction. Tests: - Gate: 198 files, 2914 tests (2911 + 3), exit 0 - e2e 8,42: 46 checks passed (42: +1 for 42e-ii) - Three new unit tests in tests/core/collider-parts.test.ts: the frame with undo, the scale with an overridden frame, and the cases that keep the box - 42e-ii goes through the context menu: "Frame collision" with scale 2 on the sprite wearing the wedge frame, a box on a sprite without a picture - Five unit counter-probes fail, 1 to 2 tests each. They ran on stein2, not in the container. - The browser counter-probe fails 42e-ii with shape "box" - The e2e sum of 1617 is calculated (1616 + 1). Neither the full suite nor the smokes ran for this state. - Vector art has no test of its own, it takes the same branch as a whole texture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
8.2 KiB
TypeScript
184 lines
8.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||
import { AddFrameCommand, SetFrameCollisionCommand } from '../../src/core/commands/frameCommands';
|
||
import {
|
||
AddColliderCommand,
|
||
AddRigidbodyCommand,
|
||
colliderFit,
|
||
FitColliderCommand,
|
||
} from '../../src/core/commands/colliderCommands';
|
||
import { MoveSceneObjectCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import type { TextureAsset } from '../../src/core/model/Asset';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { colliderFrame } from '../../src/core/properties/collider';
|
||
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/,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('a new collider on a sprite whose frame has an outline (#218)', () => {
|
||
const WEDGE = [
|
||
{ x: 0, y: 20 },
|
||
{ x: 40, y: 0 },
|
||
{ x: 40, y: 20 },
|
||
];
|
||
|
||
/** A sprite wearing a 40 × 20 frame at that size, and `added`: the props of a collider added to it. */
|
||
function wearing() {
|
||
const f = makeSpriteScene('Wedge');
|
||
const texture = new CreateAssetCommand('texture', 'Sheet');
|
||
f.stack.execute(texture);
|
||
Object.assign(findAsset(f.project, texture.asset.id) as TextureAsset, {
|
||
file: `${'a'.repeat(64)}.png`,
|
||
width: 64,
|
||
height: 64,
|
||
});
|
||
const frame = new AddFrameCommand(texture.asset.id, { x: 0, y: 0, w: 40, h: 20 });
|
||
f.stack.execute(frame);
|
||
const set = (key: string, value: number | string) =>
|
||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, key, value));
|
||
set('texture', frame.frameId);
|
||
set('width', 40);
|
||
set('height', 20);
|
||
const id = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const added = () => {
|
||
const command = new AddColliderCommand(f.sceneId, id);
|
||
f.stack.execute(command);
|
||
return findAsset(f.project, command.assetId)!.props;
|
||
};
|
||
const outline = () =>
|
||
f.stack.execute(new SetFrameCollisionCommand(texture.asset.id, frame.frameId, [WEDGE]));
|
||
return { ...f, id, textureId: texture.asset.id, frameId: frame.frameId, set, added, outline };
|
||
}
|
||
|
||
it('wears that frame, and one undo takes collider and asset away again', () => {
|
||
const f = wearing();
|
||
f.outline();
|
||
const count = f.project.assets.length;
|
||
const props = f.added();
|
||
expect(props).toMatchObject({ shape: 'frame', frame: f.frameId, width: 40, height: 20, scale: 1 });
|
||
expect(colliderFrame(f.project, props)?.pieces).toHaveLength(1);
|
||
f.stack.undo();
|
||
expect(f.project.assets).toHaveLength(count);
|
||
});
|
||
|
||
it('is scaled to a sprite drawn bigger, and reads the frame this placement overrides', () => {
|
||
const f = wearing();
|
||
f.outline();
|
||
f.set('texture', '');
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.id, 'texture', f.frameId));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.id, 'width', 100));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.id, 'height', 50));
|
||
expect(f.added()).toMatchObject({ shape: 'frame', frame: f.frameId, width: 40, height: 20, scale: 2.5 });
|
||
});
|
||
|
||
it('stays the box of the sprite without an outline, with the whole texture or stretched', () => {
|
||
const f = wearing();
|
||
const box = { shape: 'box', width: 40, height: 20, scale: 1 };
|
||
expect(f.added()).toMatchObject(box);
|
||
f.outline();
|
||
f.set('texture', f.textureId);
|
||
expect(f.added()).toMatchObject(box);
|
||
f.set('texture', f.frameId);
|
||
f.set('height', 60);
|
||
expect(f.added()).toMatchObject({ ...box, height: 60 });
|
||
});
|
||
});
|