Files
feuerwerk/tests/core/resolve.test.ts
T
rootandClaude Fable 5.1 016e83c9b5
game-maker / suite (push) Successful in 9m5s
Transform: scale, rotation and mirror as properties, world transform as a matrix chain, oriented colliders (#76)
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.

The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.

Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 17:25:06 +02:00

143 lines
5.1 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 { buildAsset } from '../../src/core/commands/assetCommands';
import { createEmptyProject } from '../../src/core/model/Project';
import { createSceneObject } from '../../src/core/model/SceneObject';
import { coerceValue, defaultsFor, schemaFor } from '../../src/core/properties/PropertySchema';
import {
deviatesFromAsset,
deviatesFromTemplate,
inheritedValue,
isModified,
resolveAssetProps,
resolveSceneObjectProps,
} from '../../src/core/properties/resolve';
describe('PropertySchema', () => {
it('has no position on any asset type', () => {
for (const type of ['scene', 'camera', 'sprite'] as const) {
const keys = schemaFor(type).map((d) => d.key);
expect(keys).not.toContain('x');
expect(keys).not.toContain('y');
expect(keys).not.toContain('position');
}
});
it('provides defaults for every key', () => {
expect(defaultsFor('sprite')).toEqual({
enabled: true,
scale: 1,
rotation: 0,
mirrorX: false,
mirrorY: false,
width: 100,
height: 100,
color: 0xffffff,
texture: '',
shader: '',
});
});
it('coerces values against the definition', () => {
const width = schemaFor('sprite').find((d) => d.key === 'width')!;
expect(coerceValue(width, -5)).toBe(1);
expect(coerceValue(width, '42')).toBe(42);
expect(coerceValue(width, 'abc')).toBe(100);
const color = schemaFor('sprite').find((d) => d.key === 'color')!;
expect(coerceValue(color, '#ff0000')).toBe(0xff0000);
expect(coerceValue(color, 0x1ffffff)).toBe(0xffffff);
});
});
describe('resolve', () => {
const project = createEmptyProject('p', 'P');
it('fills asset props with defaults and ignores unknown keys', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props = { width: 50, bogus: 1 };
expect(resolveAssetProps(project, sprite)).toEqual({
enabled: true,
scale: 1,
rotation: 0,
mirrorX: false,
mirrorY: false,
width: 50,
height: 100,
color: 0xffffff,
texture: '',
shader: '',
});
});
it('applies sparse overrides on top of asset values', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.color = 0x00ff00;
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(resolveSceneObjectProps(project, sprite, object)).toEqual({
enabled: true,
scale: 1,
rotation: 0,
mirrorX: false,
mirrorY: false,
width: 100,
height: 100,
color: 0x00ff00,
texture: '',
shader: '',
});
object.overrides.color = 0x0000ff;
expect(resolveSceneObjectProps(project, sprite, object).color).toBe(0x0000ff);
expect(inheritedValue(project, sprite, 'color')).toBe(0x00ff00);
});
it('detects asset values that differ from the schema default', () => {
const sprite = buildAsset('sprite', 'Box');
expect(isModified(project, sprite, 'width')).toBe(false); // explicit default counts as unmodified
sprite.props.width = 250;
expect(isModified(project, sprite, 'width')).toBe(true);
delete sprite.props.height; // missing → resolved from default → unmodified
expect(isModified(project, sprite, 'height')).toBe(false);
expect(isModified(project, sprite, 'bogus')).toBe(false);
});
it('ignores an override that carries the asset value – nothing to reset or apply', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.width = 250;
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false); // no override at all
object.overrides.width = 250; // same value as the asset → not a deviation
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false);
object.overrides.width = 300;
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(true);
// The asset moving to the object's value settles the difference again.
sprite.props.width = 300;
expect(deviatesFromAsset(project, object, sprite, 'width')).toBe(false);
});
it('compares an instance against its template as overrides, not as values', () => {
const sprite = buildAsset('sprite', 'Box');
const template = createSceneObject('t1', sprite.id, { x: 0, y: 0 });
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromTemplate(object, template, 'width')).toBe(false); // neither overrides
template.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
object.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(false);
object.overrides.width = 77;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
});
it('keeps position on the scene object only', () => {
const sprite = buildAsset('sprite', 'Box');
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(object.position).toEqual({ x: 10, y: 20, z: 0 });
expect('position' in sprite).toBe(false);
expect(resolveSceneObjectProps(project, sprite, object)).not.toHaveProperty('x');
});
});