Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions GDJS/Runtime/gameplay-tests/gameplay-test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,9 @@ namespace gdjs {
_startTimeMs: number = 0;
_stopped: boolean = false;
_assertions: Array<GameplayTestAssertion> = [];
/** What the harness noticed about the test itself while it ran (see
* `_addRunWarning`), reported with the warnings about the game. */
_runWarnings: Array<string> = [];
_consoleLogs: Array<GameplayTestLog> = [];
_consoleLogsTotalChars: number = 0;
_eventLog: Array<GameplayTestEvent> = [];
Expand Down Expand Up @@ -986,6 +989,41 @@ namespace gdjs {
return objectCounts;
}

/**
* Something noticed while the test ran, reported once whatever the
* assertions said.
*/
private _addRunWarning(message: string): void {
if (this._runWarnings.length >= MAX_WARNINGS) return;
if (this._runWarnings.indexOf(message) !== -1) return;
this._runWarnings.push(message);
}

/**
* A `getNearby` radius that cannot tell anything apart: it reaches
* further than the whole screen AND let every instance through, so a
* check on what it returns passes wherever the game put them.
*/
private _warnOnUnselectiveRadius(
objectName: string,
referenceObjectName: string,
radius: float,
keptCount: integer,
instancesCount: integer
): void {
if (keptCount < instancesCount) return;
const screenDiagonal = Math.hypot(
this._runtimeGame.getGameResolutionWidth(),
this._runtimeGame.getGameResolutionHeight()
);
if (!(radius > screenDiagonal)) return;
this._addRunWarning(
`getNearby("${objectName}", "${referenceObjectName}", ${radius}) kept every instance of "${objectName}": that radius is larger than the whole screen (${Math.round(
screenDiagonal
)}px diagonal), so it says nothing about where they are. Use a radius of the size of what is being checked.`
);
}

/**
* What the harness noticed about the game by itself, whatever the test
* asserted: a test can pass on the values it checks while the game is
Expand Down Expand Up @@ -2239,8 +2277,9 @@ namespace gdjs {
const reference = this._makeObjectSnapshot(referenceInstances[0], 0);
const referenceZ = reference.centerZ || 0;

const instances = this._getInstances(objectName);
const nearby: Array<GameplayTestNearbyObjectSnapshot> = [];
for (const object of this._getInstances(objectName)) {
for (const object of instances) {
const snapshot = this._makeObjectSnapshot(object, childrenDepth);
const relativeX = snapshot.centerX - reference.centerX;
const relativeY = snapshot.centerY - reference.centerY;
Expand All @@ -2263,6 +2302,13 @@ namespace gdjs {
});
}
nearby.sort((a, b) => a.distance - b.distance);
this._warnOnUnselectiveRadius(
objectName,
referenceObjectName,
radius,
nearby.length,
instances.length
);
return nearby;
}

Expand Down Expand Up @@ -3565,7 +3611,7 @@ namespace gdjs {
gameTimeMs: Math.round(this._gameTimeMs),
assertions: this._assertions,
errors: errors.slice(0, MAX_ERRORS),
warnings: this._getWarnings(),
warnings: [...this._runWarnings, ...this._getWarnings()],
consoleLogs: this._consoleLogs,
eventLog: this._eventLog,
finalState: {
Expand Down
31 changes: 31 additions & 0 deletions GDJS/tests/tests/gameplaytestharness.js
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,37 @@ describe('gdjs.gameplayTests', () => {
expect(tankCanon.x).to.be(140);
});

it('warns about a getNearby radius that keeps everything and covers the screen', async () => {
const harness = await makeHarnessWithSpawnedTank();
harness.spawn('MyObject', 100, 200);
await harness.stepFrames(1);

// The game is 800x600: a 100000px radius is not a proximity check.
harness.getNearby('CombinedTank', 'MyObject', 100000);

expect(harness._runWarnings.length).to.be(1);
expect(harness._runWarnings[0]).to.contain(
'kept every instance of "CombinedTank"'
);
expect(harness._runWarnings[0]).to.contain(
'larger than the whole screen (1000px diagonal)'
);
});

it('says nothing about a radius that leaves an instance out, or one the screen holds', async () => {
const harness = await makeHarnessWithSpawnedTank();
harness.spawn('MyObject', 100, 200);
harness.spawn('CombinedTank', 50000, 200, undefined, 'UI');
await harness.stepFrames(1);

// Far bigger than the screen, but it does tell the two tanks apart.
harness.getNearby('CombinedTank', 'MyObject', 2000);
// Keeps everything, but within what the screen shows.
harness.getNearby('MyObject', 'CombinedTank', 900);

expect(harness._runWarnings.length).to.be(0);
});

it('moves a point with the Z and Z scale of a parent without a THREE object', () => {
const harness = makeStartedHarness(makeRuntimeGame());
// A 3D custom object places its children at
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,17 @@ describe('CustomObjectFunctions', () => {
});

expect(result.success).toBe(true);
// The area, where the object turns and where its children ended up:
// "Top" is not centered on "Body", which only the boxes tell.
expect(result.message).toContain(
'Fitted the area of the default variant to its children: 0;0;0 to 40;40;60 (children moved so (0;0;0) is their minimum corner).'
);
expect(result.message).toContain(
'Its center of rotation, the center of that area unless its events set another one, is at 20;20;30 from its own position'
);
expect(result.message).toContain(
'Children: "Body" X 0 to 40, Y 0 to 40, Z 0 to 40, middle 20;20;20; "Top" X 10 to 30, Y 10 to 30, Z 40 to 60, middle 20;20;50.'
);
// Children spanning 10..50 on X and Y, 0..60 on Z, moved to start at 0.
expect(getArea(dialog)).toEqual({
minX: 0,
Expand All @@ -481,6 +492,9 @@ describe('CustomObjectFunctions', () => {
});

expect(result.success).toBe(true);
expect(result.message).toContain(
'Its center of rotation, the center of that area unless its events set another one, is now its own position (0;0;0).'
);
// A symmetric area: its center (the center of rotation) is the origin.
expect(getArea(dialog)).toEqual({
minX: -20,
Expand Down
139 changes: 108 additions & 31 deletions newIDE/app/src/EditorFunctions/Extensions/FitAreaToChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,30 @@ type Box = {| min: Array<number>, max: Array<number> |};

const roundBound = (value: number): number => Math.round(value * 1e6) / 1e6;

// The box of the children, or the child objects whose size could not be
// measured: moving the children and writing an area on a guessed size would
// silently shrink the custom object to nothing.
type ChildInstancesBox =
| {| box: Box | null, unmeasurableObjectNames: [] |}
| {| box: null, unmeasurableObjectNames: Array<string> |};
const roundCoordinate = (value: number): number =>
Math.round(value * 100) / 100;

// What the instances of one child object occupy, all of them together.
type ChildBox = {| objectName: string, box: Box, instancesCount: number |};

// The box of the children (`box` is null when there is no instance at all), or
// the child objects whose size could not be measured: moving the children and
// writing an area on a guessed size would silently shrink the custom object to
// nothing, so nothing is done when there is any.
type ChildInstancesBox = {|
box: Box | null,
unmeasurableObjectNames: Array<string>,
childBoxes: Array<ChildBox>,
|};

const growBox = (box: Box | null, added: Box): Box => {
if (!box) return { min: [...added.min], max: [...added.max] };
for (let axis = 0; axis < 3; axis++) {
box.min[axis] = Math.min(box.min[axis], added.min[axis]);
box.max[axis] = Math.max(box.max[axis], added.max[axis]);
}
return box;
};

const forEachInstance = (
variant: gdEventsBasedObjectVariant,
Expand Down Expand Up @@ -133,6 +151,7 @@ const getChildInstancesBox = (
const objects = variant.getObjects();
let box: Box | null = null;
const unmeasurableObjectNames: Array<string> = [];
const childBoxes: Array<ChildBox> = [];

forEachInstance(variant, instance => {
const objectName = instance.getObjectName();
Expand Down Expand Up @@ -198,26 +217,64 @@ const getChildInstancesBox = (
);

const instanceBox = getInstanceBox(instance, minimumCorner, size, center);
const currentBox = box;
if (!currentBox) {
box = { min: [...instanceBox.min], max: [...instanceBox.max] };
box = growBox(box, instanceBox);
const childBox = childBoxes.find(
candidate => candidate.objectName === objectName
);
if (childBox) {
growBox(childBox.box, instanceBox);
childBox.instancesCount++;
} else {
for (let axis = 0; axis < 3; axis++) {
currentBox.min[axis] = Math.min(
currentBox.min[axis],
instanceBox.min[axis]
);
currentBox.max[axis] = Math.max(
currentBox.max[axis],
instanceBox.max[axis]
);
}
childBoxes.push({
objectName,
box: growBox(null, instanceBox),
instancesCount: 1,
});
}
});

if (unmeasurableObjectNames.length > 0)
return { box: null, unmeasurableObjectNames };
return { box, unmeasurableObjectNames: [] };
return { box: null, unmeasurableObjectNames, childBoxes: [] };
return { box, unmeasurableObjectNames: [], childBoxes };
};

// At most this many child objects are described: the boxes are there to be
// compared with each other, which nobody does over a long list.
const MAX_DESCRIBED_CHILD_BOXES = 10;

/**
* Where the children ended up, so that a part meant to be centered on another
* (a turret on a hull) can be seen not to be. Each child object is given the
* box of all of its instances, moved like them.
*/
const getChildBoxesDescription = (
childBoxes: Array<ChildBox>,
offsets: Array<number>,
axesCount: number
): string => {
if (childBoxes.length === 0) return '';
const axes = ['X', 'Y', 'Z'];
const described = childBoxes
.slice(0, MAX_DESCRIBED_CHILD_BOXES)
.map(({ objectName, box, instancesCount }) => {
const bounds = [];
const middle = [];
for (let axis = 0; axis < axesCount; axis++) {
const min = box.min[axis] + offsets[axis];
const max = box.max[axis] + offsets[axis];
bounds.push(
`${axes[axis]} ${roundCoordinate(min)} to ${roundCoordinate(max)}`
);
middle.push(roundCoordinate((min + max) / 2));
}
return `"${objectName}"${
instancesCount > 1 ? ` (${instancesCount} instances)` : ''
} ${bounds.join(', ')}, middle ${middle.join(';')}`;
});
const notDescribedCount = childBoxes.length - described.length;
return `Children: ${described.join('; ')}${
notDescribedCount > 0 ? ` and ${notDescribedCount} more` : ''
}.`;
};

/**
Expand All @@ -234,7 +291,7 @@ const fitVariantAreaToChildren = (
variantLabel: string,
pixiResourcesLoader: any
): string | null => {
const { box, unmeasurableObjectNames } = getChildInstancesBox(
const { box, unmeasurableObjectNames, childBoxes } = getChildInstancesBox(
project,
variant,
pixiResourcesLoader
Expand Down Expand Up @@ -299,16 +356,36 @@ const fitVariantAreaToChildren = (
variant.setAreaMaxY(areaMax[1]);
variant.setAreaMaxZ(areaMax[2]);

const area = isRenderedIn3D
? `${areaMin[0]};${areaMin[1]};${areaMin[2]} to ${areaMax[0]};${
areaMax[1]
};${areaMax[2]}`
: `${areaMin[0]};${areaMin[1]} to ${areaMax[0]};${areaMax[1]}`;
return `Fitted the area of ${variantLabel} to its children: ${area}${
const axesCount = isRenderedIn3D ? 3 : 2;
const formatPoint = (point: Array<number>): string =>
point
.slice(0, axesCount)
.map(roundCoordinate)
.join(';');
const area = `${formatPoint(areaMin)} to ${formatPoint(areaMax)}`;
const zeroPoint = formatPoint([0, 0, 0]);
// The custom object turns around the center of its area (unless its own
// events move that center at runtime): said with the area, the one moment
// the agent can act on it.
const areaCenter = areaMin.map((min, axis) => (min + areaMax[axis]) / 2);
const rotationCenter =
mode === 'centered_on_origin'
? ' (children moved so (0;0;0) is their center, which is also the center of rotation of the custom object)'
: ' (children moved so (0;0;0) is their minimum corner)'
}.`;
? `Its center of rotation, the center of that area unless its events set another one, is now its own position (${zeroPoint}).`
: `Its center of rotation, the center of that area unless its events set another one, is at ${formatPoint(
areaCenter
)} from its own position (\`centered_on_origin\` puts the two together).`;

return [
`Fitted the area of ${variantLabel} to its children: ${area}${
mode === 'centered_on_origin'
? ` (children moved so (${zeroPoint}) is their center)`
: ` (children moved so (${zeroPoint}) is their minimum corner)`
}.`,
rotationCenter,
getChildBoxesDescription(childBoxes, offsets, axesCount),
]
.filter(Boolean)
.join(' ');
};

/**
Expand Down
Loading
Loading