Files
GDevelop/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js
T
D8H 7dd4d13a7a Upgrade to Three.js 0.185.1 (#8853)
- Point lights and spot lights now uses a realistic decay. It's not possible to find the exact same result as before, but you can tweak the intensity and the decay properties. Hemisphere lights, directional lights and ambient lights still work the same as before.
- The 3D particle emitter must be updated to the last version to handle the new 3D renderer world scale property.
- Ensure any 3D extension you use is also updated to the latest version.
2026-10-03 13:55:50 +02:00

806 lines
25 KiB
JavaScript

// @ts-check
describe('gdjs.PathfindingRuntimeBehavior', function () {
// limit tests cases on the legacy collision methods.
// Note that the legacy collision mode is still the only mode that exists
// because the new one were never merged.
let doLegacyPathFindingTests = (
cellSize,
objectCenteredOnCells,
direction
) => {
const pathFindingName = 'auto1';
let createScene = () => {
const runtimeGame = gdjs.getPixiRuntimeGame();
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
runtimeScene.loadFromScene({
sceneData: {
layers: [
{
name: '',
visibility: true,
effects: [],
cameras: [],
ambientLightColorR: 0,
ambientLightColorG: 0,
ambientLightColorB: 0,
isLightingLayer: false,
followBaseLayerCamera: true,
},
],
variables: [],
r: 0,
v: 0,
b: 0,
mangledName: 'Scene1',
name: 'Scene1',
stopSoundsOnStartup: false,
title: '',
renderer3DWorldScale: 100,
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});
runtimeScene._timeManager.getElapsedTime = function () {
return (1 / 60) * 1000;
};
return runtimeScene;
};
let addPlayer = (runtimeScene) => {
const player = new gdjs.SpriteRuntimeObject(runtimeScene, {
name: 'player',
type: '',
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
centerPoint: {
name: 'Center',
x: 80,
y: 80,
automatic: false,
},
points: [
{ name: 'Center', x: 80, y: 80 },
objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
],
hasCustomCollisionMask: false,
customCollisionMask: [],
image: '',
},
],
timeBetweenFrames: 0,
looping: false,
},
],
useMultipleDirections: false,
},
],
behaviors: [
{
type: 'PathfindingBehavior::PathfindingBehavior',
name: 'auto1',
allowDiagonals: false,
acceleration: 400,
maxSpeed: 200,
angularMaxSpeed: 180,
rotateObject: false,
angleOffset: 0,
cellWidth: cellSize,
cellHeight: cellSize,
extraBorder: 0,
},
],
variables: [],
effects: [],
updateIfNotVisible: true,
});
player.getWidth = function () {
return 160;
};
player.getHeight = function () {
return 160;
};
runtimeScene.addObject(player);
return player;
};
let addObstacle = (runtimeScene) => {
const obstacle = new gdjs.SpriteRuntimeObject(runtimeScene, {
name: 'obstacle',
type: '',
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
centerPoint: {
name: 'Center',
x: 80,
y: 80,
automatic: false,
},
points: [
{ name: 'Center', x: 80, y: 80 },
objectCenteredOnCells
? { name: 'Origin', x: 80, y: 80 }
: { name: 'Origin', x: 87, y: 87 },
],
hasCustomCollisionMask: false,
customCollisionMask: [],
image: '',
},
],
timeBetweenFrames: 0,
looping: false,
},
],
useMultipleDirections: false,
},
],
behaviors: [
{
type: 'PathfindingBehavior::PathfindingObstacleBehavior',
impassable: true,
cost: 2,
},
],
variables: [],
effects: [],
updateIfNotVisible: true,
});
obstacle.getWidth = function () {
return 160;
};
obstacle.getHeight = function () {
return 160;
};
runtimeScene.addObject(obstacle);
return obstacle;
};
let playerLeftBorder;
let playerRightBorder;
let playerTopBorder;
let playerBottomBorder;
let obstacleLeftBorder;
let obstacleRightBorder;
let obstacleTopBorder;
let obstacleBottomBorder;
if (cellSize == 160) {
if (objectCenteredOnCells) {
playerLeftBorder = 80;
playerRightBorder = 80;
playerTopBorder = 80;
playerBottomBorder = 80;
} else {
playerLeftBorder = 87;
playerRightBorder = 73;
playerTopBorder = 87;
playerBottomBorder = 73;
}
} else {
// The player radius is at most 2 cells
playerLeftBorder = 2 * cellSize;
playerRightBorder = 2 * cellSize;
playerTopBorder = 2 * cellSize;
playerBottomBorder = 2 * cellSize;
}
if (objectCenteredOnCells) {
obstacleLeftBorder = 80;
obstacleRightBorder = 80;
obstacleTopBorder = 80;
obstacleBottomBorder = 80;
} else {
obstacleLeftBorder = 87;
obstacleRightBorder = 73;
obstacleTopBorder = 87;
obstacleBottomBorder = 73;
}
let inclusiveMargin;
if (cellSize == 160) {
// The collision is strict
inclusiveMargin = 0;
} else {
// The collision is not strict
inclusiveMargin = 1;
}
// multiple of 20 and 160
const playerX = 480;
const playerY = 320;
let runtimeScene;
let player;
beforeEach(function () {
runtimeScene = createScene();
player = addPlayer(runtimeScene);
});
it('can find a path with an obstacle near the start', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// right on the player right
obstacleX = playerX + playerRightBorder + obstacleLeftBorder;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX = playerX - playerLeftBorder - obstacleRightBorder;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY = playerY - playerTopBorder - obstacleBottomBorder;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY = playerY + playerBottomBorder + obstacleTopBorder;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it('can find a path when an obstacle is overlapping only at the start', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// The obstacle will be right on the player right after the first step.
// So, it's overlapping the player by one cell length at start.
obstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder -
cellSize +
inclusiveMargin;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX =
playerX -
playerLeftBorder -
obstacleRightBorder +
cellSize -
inclusiveMargin;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY =
playerY -
playerTopBorder -
obstacleBottomBorder +
cellSize -
inclusiveMargin;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder -
cellSize +
inclusiveMargin;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it("mustn't find a path when an obstacle is slightly overlapping the only first cell to go", function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
// The obstacle will be right on the player right after the first step.
// So, it's overlapping the player by one cell length at start.
obstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder -
cellSize +
inclusiveMargin -
1;
obstacleY = playerY;
targetX = playerX - 2 * cellSize;
targetY = playerY;
break;
case 'left':
obstacleX =
playerX -
playerLeftBorder -
obstacleRightBorder +
cellSize -
inclusiveMargin +
1;
obstacleY = playerY;
targetX = playerX + 2 * cellSize;
targetY = playerY;
break;
case 'up':
obstacleX = playerX;
obstacleY =
playerY -
playerTopBorder -
obstacleBottomBorder +
cellSize -
inclusiveMargin +
1;
targetX = playerX;
targetY = playerY + 2 * cellSize;
break;
case 'down':
obstacleX = playerX;
obstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder -
cellSize +
inclusiveMargin -
1;
targetX = playerX;
targetY = playerY - 2 * cellSize;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move away from the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
if (cellSize == 160) {
// The obstacle is the size of the cell so the player can bypass it.
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be.above(
3
);
} else {
expect(player.getBehavior(pathFindingName).pathFound()).to.be(false);
}
});
it('can find a path with an obstacle adjacent to the target', function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
targetX = playerX + 2 * cellSize;
targetY = playerY;
// will be right on the player right at the end
obstacleX =
targetX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
obstacleY = playerY;
break;
case 'left':
targetX = playerX - 2 * cellSize;
targetY = playerY;
obstacleX =
targetX - playerRightBorder - obstacleLeftBorder - inclusiveMargin;
obstacleY = playerY;
break;
case 'up':
targetX = playerX;
targetY = playerY - 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
break;
case 'down':
targetX = playerX;
targetY = playerY + 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// so obstacles register
runtimeScene.renderAndStep(1000 / 60);
// move as close as possible to the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(3);
});
it("mustn't find a path with an obstacle slightly overlapping to the target", function () {
const obstacle = addObstacle(runtimeScene);
let obstacleX;
let obstacleY;
let targetX;
let targetY;
switch (direction) {
case 'right':
targetX = playerX + 2 * cellSize;
targetY = playerY;
obstacleX =
targetX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
obstacleY = playerY;
break;
case 'left':
targetX = playerX - 2 * cellSize;
targetY = playerY;
obstacleX =
targetX -
playerLeftBorder -
obstacleRightBorder -
inclusiveMargin +
1;
obstacleY = playerY;
break;
case 'up':
targetX = playerX;
targetY = playerY - 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY -
playerTopBorder -
obstacleBottomBorder -
inclusiveMargin +
1;
break;
case 'down':
targetX = playerX;
targetY = playerY + 2 * cellSize;
obstacleX = playerX;
obstacleY =
targetY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
break;
default:
throw new Error('Invalid direction for the test');
}
obstacle.setPosition(obstacleX, obstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
// move as close as possible to the obstacle
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(false);
});
it('can find a path between 2 obstacles making a path perfectly adjusted to the object', function () {
const obstacleTop = addObstacle(runtimeScene);
const obstacleBottom = addObstacle(runtimeScene);
let topObstacleX;
let bottomObstacleX;
let topObstacleY;
let bottomObstacleY;
let targetX;
let targetY;
let straightLineCellCount;
switch (direction) {
case 'right': {
const obstacleX =
playerX + playerRightBorder + 2 * cellSize + obstacleLeftBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
targetX =
obstacleX + obstacleRightBorder + playerLeftBorder + 2 * cellSize;
targetY = playerY;
straightLineCellCount = (targetX - playerX) / cellSize + 1;
break;
}
case 'left': {
const obstacleX =
playerX - playerLeftBorder - 2 * cellSize - obstacleRightBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY + playerBottomBorder + obstacleTopBorder + inclusiveMargin;
targetX =
obstacleX - obstacleLeftBorder - playerRightBorder - 2 * cellSize;
targetY = playerY;
straightLineCellCount = (playerX - targetX) / cellSize + 1;
break;
}
case 'up': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY - playerTopBorder - 2 * cellSize - obstacleBottomBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
targetY =
obstacleY - obstacleTopBorder - playerBottomBorder - 2 * cellSize;
targetX = playerX;
straightLineCellCount = (playerY - targetY) / cellSize + 1;
break;
}
case 'down': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY + playerBottomBorder + 2 * cellSize + obstacleTopBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX + playerRightBorder + obstacleLeftBorder + inclusiveMargin;
targetY =
obstacleY + obstacleBottomBorder + playerTopBorder + 2 * cellSize;
targetX = playerX;
straightLineCellCount = (targetY - playerY) / cellSize + 1;
break;
}
default:
throw new Error('Invalid direction for the test');
}
obstacleTop.setPosition(topObstacleX, topObstacleY);
obstacleBottom.setPosition(bottomObstacleX, bottomObstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be(
straightLineCellCount
);
});
it("mustn't find a direct path between 2 slightly too much tighten obstacles", function () {
const obstacleTop = addObstacle(runtimeScene);
const obstacleBottom = addObstacle(runtimeScene);
let topObstacleX;
let bottomObstacleX;
let topObstacleY;
let bottomObstacleY;
let targetX;
let targetY;
let straightLineCellCount;
switch (direction) {
case 'right': {
const obstacleX =
playerX + playerRightBorder + 2 * cellSize + obstacleLeftBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
targetX =
obstacleX + obstacleRightBorder + playerLeftBorder + 2 * cellSize;
targetY = playerY;
straightLineCellCount = (targetX - playerX) / cellSize + 1;
break;
}
case 'left': {
const obstacleX =
playerX - playerLeftBorder - 2 * cellSize - obstacleRightBorder;
topObstacleX = obstacleX;
bottomObstacleX = obstacleX;
topObstacleY =
playerY - playerTopBorder - obstacleBottomBorder - inclusiveMargin;
bottomObstacleY =
playerY +
playerBottomBorder +
obstacleTopBorder +
inclusiveMargin -
1;
targetX =
obstacleX - obstacleLeftBorder - playerRightBorder - 2 * cellSize;
targetY = playerY;
straightLineCellCount = (playerX - targetX) / cellSize + 1;
break;
}
case 'up': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY - playerTopBorder - 2 * cellSize - obstacleBottomBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
targetY =
obstacleY - obstacleTopBorder - playerBottomBorder - 2 * cellSize;
targetX = playerX;
straightLineCellCount = (playerY - targetY) / cellSize + 1;
break;
}
case 'down': {
// topObstacle and bottomObstacle are actually left and right but for convenience...
const obstacleY =
playerY + playerBottomBorder + 2 * cellSize + obstacleTopBorder;
topObstacleY = obstacleY;
bottomObstacleY = obstacleY;
topObstacleX =
playerX - playerLeftBorder - obstacleRightBorder - inclusiveMargin;
bottomObstacleX =
playerX +
playerRightBorder +
obstacleLeftBorder +
inclusiveMargin -
1;
targetY =
obstacleY + obstacleBottomBorder + playerTopBorder + 2 * cellSize;
targetX = playerX;
straightLineCellCount = (targetY - playerY) / cellSize + 1;
break;
}
default:
throw new Error('Invalid direction for the test');
}
obstacleTop.setPosition(topObstacleX, topObstacleY);
obstacleBottom.setPosition(bottomObstacleX, bottomObstacleY);
// To ensure obstacles are registered.
runtimeScene.renderAndStep(1000 / 60);
player.setPosition(playerX, playerY);
player
.getBehavior(pathFindingName)
.moveTo(runtimeScene, targetX, targetY);
expect(player.getBehavior(pathFindingName).pathFound()).to.be(true);
// had to do a little detour
expect(player.getBehavior(pathFindingName).getNodeCount()).to.be.above(
straightLineCellCount
);
});
};
describe('(collisionMethod: Legacy,', function () {
// Small cells and cells the object size
[20, 160].forEach((cellSize) => {
describe(`cellSize: ${cellSize},`, function () {
[false, true].forEach((objectCenteredOnCells) => {
describe(`centered: ${objectCenteredOnCells},`, function () {
['left', 'right', 'down', 'up'].forEach((direction) => {
describe(`direction: ${direction})`, function () {
doLegacyPathFindingTests(
cellSize,
objectCenteredOnCells,
direction
);
});
});
});
});
});
});
});
});