Files
GDevelop/Extensions/PlatformBehavior/tests/JumpAndFallingPlatformer.spec.js

2036 lines
74 KiB
JavaScript

describe('gdjs.PlatformerObjectRuntimeBehavior', function () {
const epsilon = 1 / (2 << 16);
describe('(falling)', function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object in the air.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 1500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -100);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
runtimeScene.renderAndStep(1000 / 60);
});
it('can fall when in the air', function () {
// The character falls.
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
}
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// The platform stopped the character.
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The character walk out of the platform.
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getX()).to.be.above(84);
expect(object.getY()).to.be(-26.875);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
// Let the speed on X axis go back to 0.
for (let i = 0; i < 50; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getBehavior('auto1').getCurrentSpeed()).to.be(0);
});
it('falls when a platform is moved away', function () {
object.setPosition(0, -32);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be.within(-31, -30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// move the platform away
platform.setPosition(-100, -100);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('falls when a platform is removed', function () {
object.setPosition(0, -32);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be.within(-31, -30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Remove the platform
runtimeScene.markObjectForDeletion(platform);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
[
'Jump',
'Right',
'Left',
'Up',
'Down',
'Ladder',
'Release',
'Release Ladder',
].forEach((key) => {
it(`can tell that ${key} key is used`, function () {
object.getBehavior('auto1').simulateControl(key);
// The condition applies only after the key is actually used by the platformer character.
expect(object.getBehavior('auto1').isUsingControl(key)).to.be(false);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isUsingControl(key)).to.be(true);
runtimeScene.renderAndStep(1000 / 60);
// The key wasn't hold. It's forgotten.
expect(object.getBehavior('auto1').isUsingControl(key)).to.be(false);
});
});
});
[20, 30, 60, 120].forEach((framesPerSecond) => {
describe(`(FPS independent trajectory: ${framesPerSecond} fps)`, function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene(1000 / framesPerSecond);
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -32);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < framesPerSecond / 6; ++i) {
runtimeScene.renderAndStep(1000 / framesPerSecond);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining 1/10 of second
// A jump will at least sustain one frame,
// because the jump key is pressed.
// To have the same sustain time for each fps,
// we use their greatest common divisor: 10.
for (let i = 0; i < framesPerSecond / 10; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / framesPerSecond);
}
expect(object.getY()).to.be.within(-112.5 - epsilon, -112.5 + epsilon);
// Jump without sustaining
for (let i = 0; i < framesPerSecond / 4 - 1; ++i) {
runtimeScene.renderAndStep(1000 / framesPerSecond);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.above(-206.25);
// At 30 fps, the maximum value is between 2 frames.
if (framesPerSecond !== 30) {
runtimeScene.renderAndStep(1000 / framesPerSecond);
expect(object.getY()).to.be.within(
-206.25 - epsilon,
-206.25 + epsilon
);
}
runtimeScene.renderAndStep(1000 / framesPerSecond);
expect(object.getY()).to.be.above(-206.25);
// Then let the object fall
for (let i = 0; i < framesPerSecond / 3 - 2; ++i) {
runtimeScene.renderAndStep(1000 / framesPerSecond);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// The landing happens 1 or 2 frames sooner for some fps.
// This is expected as a collision is involved.
runtimeScene.renderAndStep(1000 / framesPerSecond);
runtimeScene.renderAndStep(1000 / framesPerSecond);
runtimeScene.renderAndStep(1000 / framesPerSecond);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be(-30);
});
});
});
// The legacy trajectory calculus uses Euler method instead of Verlet integration.
// In this mode, the character is jumping higher at lower frame rates.
describe('(FPS dependent trajectory: 120 fps)', function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene(1000 / 120);
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: true,
useRepeatedJump: true,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -32);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 120 / 6; ++i) {
runtimeScene.renderAndStep(1000 / 120);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining 1/10 of second
for (let i = 0; i < 120 / 10; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 120);
}
expect(object.getY()).to.be(-113.125);
// Jump without sustaining
for (let i = 0; i < 120 / 4; ++i) {
runtimeScene.renderAndStep(1000 / 120);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be(-210);
// The maximum is between these 2 frames
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-210);
// Then, let the object fall.
for (let i = 0; i < 120 / 3; ++i) {
runtimeScene.renderAndStep(1000 / 120);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
runtimeScene.renderAndStep(1000 / 120);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be(-30);
});
});
describe('(FPS dependent trajectory: 60 fps)', function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene(1000 / 60);
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: true,
useRepeatedJump: true,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -32);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining 1/10 of second
for (let i = 0; i < 60 / 10; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 120);
}
expect(object.getY()).to.be(-113.75);
// Jump without sustaining
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 60 / 4; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.above(-220);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-220);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.above(-220);
// Then let the object fall
for (let i = 0; i < 60 / 3; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be(-30);
});
});
describe('(FPS dependent trajectory: 30 fps)', function () {
let runtimeScene;
let object;
let platform;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene(1000 / 30);
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: true,
useRepeatedJump: true,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -32);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 30);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining 1/10 of second
for (let i = 0; i < 30 / 10; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 120);
}
expect(object.getY()).to.be(-115);
// Jump without sustaining
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 30 / 4; ++i) {
runtimeScene.renderAndStep(1000 / 30);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be(-233 - 1 / 3);
// The maximum is between these 2 frames
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be(-233 - 1 / 3);
// Then let the object fall
for (let i = 0; i < 30 / 3; ++i) {
runtimeScene.renderAndStep(1000 / 30);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
runtimeScene.renderAndStep(1000 / 30);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getY()).to.be(-30);
});
});
describe('(jump and jump sustain)', function () {
let runtimeScene;
let object;
let platform;
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
let characterBehavior;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -32);
characterBehavior = object.getBehavior('auto1');
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
});
it('can jump, sustaining the jump', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Forbid to jump
object.getBehavior('auto1').setCanNotAirJump();
// It has no impact as the object is on a platform.
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Jump with sustaining as much as possible, and
// even more (18 frames at 60fps is greater than 0.2s)
for (let i = 0; i < 18; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check the height reached
expect(object.getY()).to.be.within(-225 - epsilon, -225 + epsilon);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(
-229.5833333333333 - epsilon,
-229.5833333333333 + epsilon
);
for (let i = 0; i < 4; ++i) {
// Verify that pressing the jump key does not change anything
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.above(-240);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-240 - epsilon, -240 + epsilon);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.above(-240);
// Then let the object fall
for (let i = 0; i < 60; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30);
});
it('can only jump once while the jump key is held', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(characterBehavior.isFalling()).to.be(false);
expect(characterBehavior.isFallingWithoutJumping()).to.be(false);
expect(characterBehavior.isMoving()).to.be(false);
// The character jumps a first time.
for (let i = 0; i < 80; ++i) {
characterBehavior.simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
characterBehavior.isJumping(true);
}
// The character lands back on the floor
// while the player holds the jump key.
for (let i = 0; i < 4; ++i) {
characterBehavior.simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
characterBehavior.isOnFloor(true);
expect(object.getY()).to.be(-30);
// The character doesn't jump a 2nd time.
characterBehavior.simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
characterBehavior.isOnFloor(true);
// The player release the jump key.
runtimeScene.renderAndStep(1000 / 60);
characterBehavior.isOnFloor(true);
// The character can now jump again.
characterBehavior.simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
characterBehavior.isJumping(true);
});
it('can jump, and only sustain the jump while key held', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining a bit (5 frames at 60fps = 0.08s), then stop
for (let i = 0; i < 5; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(-100, -99);
// Stop holding the jump key
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 13; ++i) {
// then hold it again (but it's too late, jump sustain is gone for this jump)
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.above(-199.7916666666666 + epsilon);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(
-199.7916666666666 - epsilon,
-199.7916666666666 + epsilon
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(
-199.7916666666666 - epsilon,
-199.7916666666666 + epsilon
);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.above(-199.7916666666666 + epsilon);
// Then let the object fall
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30);
});
it('should not jump after falling from a platform', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
// So at this point, the object could jump
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Fall from the platform
for (let i = 0; i < 35; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Try to jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('can be allowed to jump in mid air after falling from a platform', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Fall from the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Allow to jump in mid air
expect(object.getBehavior('auto1').isFalling()).to.be(true);
object.getBehavior('auto1').setCanJump();
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Can jump in the air
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
for (let i = 0; i < 40; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getBehavior('auto1').isJumping()).to.be(false);
// Can no longer to jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('can still be allowed to jump in mid air after its jump speed reached 0', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').canJump()).to.be(false);
// Allow to jump again
object.getBehavior('auto1').setCanJump();
// Is jumping
for (let i = 0; i < 40; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
// Can still jump...
expect(object.getBehavior('auto1').canJump()).to.be(true);
}
// ...even when after the jump ended.
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
// Jump again
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').canJump()).to.be(false);
});
it('can allow coyote time', function () {
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Fall from the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Allow to jump
expect(object.getBehavior('auto1').isFalling()).to.be(true);
object.getBehavior('auto1').setCanJump();
expect(object.getBehavior('auto1').canJump()).to.be(true);
// Still falling from the platform
for (let i = 0; i < 4; ++i) {
object.getBehavior('auto1').simulateLeftKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Suppose that we miss an eventual time frame or some condition.
// So we forbid to jump again:
object.getBehavior('auto1').setCanNotAirJump();
expect(object.getBehavior('auto1').canJump()).to.be(false);
// Can no longer to jump in mid air
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
it('should not grab a platform while in the ascending phase of a jump', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(12, -80);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump without sustaining
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 3; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
}
// the object is against the platform side
expect(object.getY()).to.be.within(
topPlatform.getY(),
topPlatform.getY() + object.getHeight()
);
// try to grab the platform
for (let i = 0; i < 20; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that the object didn't grabbed the platform
expect(object.getX()).to.be.above(
topPlatform.getX() - object.getWidth() + 20
);
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
it('can grab a platform while in the descending phase of a jump', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(12, -120);
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump, reach the top and go down
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 29; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
}
// the object is against the platform side
expect(object.getY()).to.be.within(
topPlatform.getY() - object.getHeight(),
topPlatform.getY()
);
// Verify the object is in the falling state of the jump:
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// try to grab the platform
for (let i = 0; i < 30; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
// Check that the object grabbed the platform
expect(object.getY()).to.be(topPlatform.getY());
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(true);
});
it('should not grab a platform while walking', function () {
const topPlatform = addPlatformObject(runtimeScene);
topPlatform.setPosition(20, platform.getY() - object.getHeight());
runtimeScene.renderAndStep(1000 / 60);
// Ensure the object falls on the platform
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
// Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// try to grab the platform
for (let i = 0; i < 30; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
}
// The object is where it could grab the top platform if it was falling.
expect(object.getX()).to.be.within(
topPlatform.getX() - object.getWidth(),
topPlatform.getX() - object.getWidth() + 2
);
expect(object.getY()).to.be(topPlatform.getY());
// Check that the object didn't grabbed the platform
expect(object.getBehavior('auto1').isGrabbingPlatform()).to.be(false);
});
const goToJumpPeak = () => {
// Ensure the object falls on the platform
for (let i = 0; i < 60 / 6; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
//Check the object is on the platform
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Jump with sustaining 1/10 of second
// A jump will at least sustain one frame,
// because the jump key is pressed.
// To have the same sustain time for each fps,
// we use their greatest common divisor: 10.
for (let i = 0; i < 60 / 10; ++i) {
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(-112.5 - epsilon, -112.5 + epsilon);
// Jump without sustaining
for (let i = 0; i < 60 / 4 - 1; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Check that we reached the maximum height
expect(object.getY()).to.be.above(-206.25);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-206.25 - epsilon, -206.25 + epsilon);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.above(-206.25);
};
it('can change the maximum falling speed without changing the vertical speed', function () {
goToJumpPeak();
// Change the maximum falling speed from 550 to 200 (for instance, for a gliding mode)
const previousFallSpeed = object
.getBehavior('auto1')
.getCurrentFallSpeed();
const previousJumpSpeed = object
.getBehavior('auto1')
.getCurrentJumpSpeed();
object.getBehavior('auto1').setMaxFallingSpeed(200, true);
runtimeScene.renderAndStep(1000 / 60);
// The character speed stays the same (25 is the acceleration).
// The jump speed is reduced as much as the falling speed.
expect(object.getBehavior('auto1').getCurrentFallSpeed()).to.be(
previousFallSpeed - 350
);
expect(object.getBehavior('auto1').getCurrentJumpSpeed()).to.be(
previousJumpSpeed - 350 - 25
);
});
it('can change the maximum falling speed and avoid changing the vertical speed too much', function () {
goToJumpPeak();
// Then let the object fall a bit
for (let i = 0; i < 15; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
}
// Change the maximum falling speed from 550 to 200 (for instance, for a gliding mode)
expect(object.getBehavior('auto1').getCurrentFallSpeed()).to.be(925);
expect(object.getBehavior('auto1').getCurrentJumpSpeed()).to.be(125);
object.getBehavior('auto1').setMaxFallingSpeed(200, true);
runtimeScene.renderAndStep(1000 / 60);
// The character jump speed is set to 0 to reduce the speed gap.
expect(object.getBehavior('auto1').getCurrentFallSpeed()).to.be(200);
expect(object.getBehavior('auto1').getCurrentJumpSpeed()).to.be(0);
});
it('can abort a jump', function () {
goToJumpPeak();
// Abort the jump
object.getBehavior('auto1').abortJump();
runtimeScene.renderAndStep(1000 / 60);
// jump and fall speeds are reset (25 is the acceleration).
expect(object.getBehavior('auto1').getCurrentFallSpeed()).to.be(25);
expect(object.getBehavior('auto1').getCurrentJumpSpeed()).to.be(0);
expect(object.getBehavior('auto1').isJumping()).to.be(false);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(true);
});
});
describe('(jumpthru)', function () {
let runtimeScene;
let object;
let platform;
let jumpthru;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
canGoDownFromJumpthru: true,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -30);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// Put a jump thru, higher than the platform so that the object jump from under it
// and will land on it at the end of the jump.
jumpthru = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
platformType: 'Jumpthru',
},
],
effects: [],
});
jumpthru.setCustomWidthAndHeight(60, 5);
runtimeScene.addObject(jumpthru);
});
it('can jump through a jumpthru and land', function () {
jumpthru.setPosition(0, -32);
//Check the platform stopped the platformer object.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// Check that the jump starts properly, and is not stopped on the jumpthru
object.getBehavior('auto1').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-39, -38);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-46, -45);
runtimeScene.renderAndStep(1000 / 60);
// At this step, the object is almost on the jumpthru (-52 + 20 (object height) = -32 (jump thru Y position)),
// but the object should not stop.
expect(object.getY()).to.be.within(-53, -52);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-60, -59);
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(-66, -65);
// Verify the object is still jumping
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
// Continue the simulation and check that position is correct in the middle of the jump
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(-83, -82);
// Verify the object is now considered as falling in its jump:
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// Continue simulation and check that we arrive on the jumpthru
for (let i = 0; i < 10; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be.within(
jumpthru.getY() - object.getHeight(),
jumpthru.getY() - object.getHeight() + 1
);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
});
it('can go down from a jump through', function () {
object.setPosition(0, -55);
jumpthru.setPosition(0, -32);
// The character lands on the jumpthru.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-52); // -52 = -32 (jumpthru y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// The character goes down from the jumpthru.
object.getBehavior('auto1').simulateDownKey();
runtimeScene.renderAndStep(1000 / 60);
for (let i = 0; i < 10; ++i) {
const previousY = object.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getY()).to.be.greaterThan(previousY);
}
});
it('can jump right under a jumpthru without landing', function () {
// A big one because the object jump to the right.
jumpthru.setCustomWidthAndHeight(600, 20);
const highestJumpY = -99.41666666666661;
// Right above the maximum reach by jumping
jumpthru.setPosition(0, Math.floor(highestJumpY) + object.getHeight());
// The object landed on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object jumps.
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 16; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
}
// The object is at the highest of the jump.
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(
highestJumpY - epsilon,
highestJumpY + epsilon
);
// The object starts to fall.
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
// The object still falls.
for (let i = 0; i < 10; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
}
expect(object.getY()).to.be.above(-85);
});
it('can jump right above a jumpthru and landing', function () {
// A big one because the object jump to the right.
jumpthru.setCustomWidthAndHeight(600, 20);
const highestJumpY = -99.41666666666661;
// Right above the maximum reach by jumping
jumpthru.setPosition(0, Math.ceil(highestJumpY) + object.getHeight());
// The object landed on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(-30); // -30 = -10 (platform y) + -20 (object height)
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
// The object jumps.
object.getBehavior('auto1').simulateJumpKey();
for (let i = 0; i < 16; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isJumping()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
}
// The object is at the highest of the jump.
runtimeScene.renderAndStep(1000 / 60);
expect(object.getY()).to.be.within(
highestJumpY - epsilon,
highestJumpY + epsilon
);
// The object landed on the jumpthru.
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
false
);
expect(object.getY()).to.be(jumpthru.getY() - object.getHeight());
});
it('can fall through the jumpthru from the left side', function () {
object.setPosition(0, -100);
jumpthru.setPosition(12, -90);
jumpthru.setCustomWidthAndHeight(60, 100);
// The jumpthru lets the character go through.
for (let i = 0; i < 10; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
}
// The character is overlapping the jumpthru.
expect(object.getX()).to.above(jumpthru.getX() - object.getWidth() + 3);
expect(object.getY()).to.be.within(
jumpthru.getY() - object.getHeight() + 10,
jumpthru.getY() + jumpthru.getHeight() - 10
);
});
it('can fall through a jumpthru from the left side and land on another jumpthru', function () {
object.setPosition(0, -100);
jumpthru.setPosition(12, -90);
jumpthru.setCustomWidthAndHeight(60, 20);
// Add another jumpthru under with a 10 pixels interleave (less than object height).
bottomJumpthru = addJumpThroughPlatformObject(runtimeScene);
bottomJumpthru.setPosition(0, -70);
bottomJumpthru.setCustomWidthAndHeight(60, 20);
// The jumpthru lets the character go through.
for (let i = 0; i < 7; ++i) {
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
}
// The character is overlapping the jumpthru.
expect(object.getX()).to.above(jumpthru.getX() - object.getWidth() + 1);
expect(object.getY()).to.be.within(
jumpthru.getY() - object.getHeight() + 1,
jumpthru.getY() + jumpthru.getHeight() - 1
);
// The character lands on the other jumpthru
// while still overlapping the other one.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
expect(object.getY()).to.be(bottomJumpthru.getY() - object.getHeight());
});
it('can fall through a jumpthru from the left side and land on another jumpthru at the exact same frame', function () {
object.setPosition(0, -100);
jumpthru.setPosition(10, -90);
jumpthru.setCustomWidthAndHeight(60, 20);
// Add another jumpthru under with a 10 pixels interleave (less than object height).
bottomJumpthru = addJumpThroughPlatformObject(runtimeScene);
bottomJumpthru.setPosition(0, -70);
bottomJumpthru.setCustomWidthAndHeight(60, 20);
// The character falls next to the jumpthru.
for (let i = 0; i < 8; ++i) {
runtimeScene.renderAndStep(1000 / 60);
expect(object.getBehavior('auto1').isFalling()).to.be(true);
expect(object.getBehavior('auto1').isFallingWithoutJumping()).to.be(
true
);
expect(object.getBehavior('auto1').isMoving()).to.be(true);
}
// The character is right at the left of "jumpthru".
expect(object.getX()).to.be(0);
// The character is right above "bottomJumpthru".
expect(object.getY()).to.be.within(
bottomJumpthru.getY() - object.getHeight() - 2,
bottomJumpthru.getY() - object.getHeight() - 1
);
object.getBehavior('auto1').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
// The character is overlapping "jumpthru"...
expect(object.getX()).to.be.above(0);
// ...and "bottomJumpthru" at the same frame.
// The character lands on "bottomJumpthru".
expect(object.getY()).to.be(bottomJumpthru.getY() - object.getHeight());
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
// The character stays on the jumpthru
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.be(bottomJumpthru.getY() - object.getHeight());
expect(object.getBehavior('auto1').isOnFloor()).to.be(true);
expect(object.getBehavior('auto1').isFalling()).to.be(false);
expect(object.getBehavior('auto1').isMoving()).to.be(false);
});
});
describe('and gdjs.PlatformRuntimeBehavior at same time - ', function () {
let runtimeScene;
let object;
let platform;
var object2;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform.
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
object.setPosition(0, -30);
// Put a platform.
platform = addPlatformObject(runtimeScene);
platform.setPosition(0, -10);
// Put a platformer object that is also a platform itself.
object2 = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj2',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'PlatformerObject',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
{
type: 'PlatformBehavior::PlatformBehavior',
name: 'Platform',
canBeGrabbed: true,
platformType: 'Platform',
},
],
effects: [],
});
object2.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object2);
// Position it above the other platformer object and just on its right,
// but one pixel too much so that the first platformer object will be moved
// left by 1px when the second platformer object+platform falls.
object2.setPosition(9, -60);
});
it('can move', function () {
// The 2nd object falls (it's not stopped by itself).
expect(object2.getY()).to.be(-60);
for (let i = 0; i < 4; i++) {
const previousY = object2.getY();
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be.above(previousY);
expect(object2.getBehavior('PlatformerObject').isFalling()).to.be(true);
}
expect(object2.getY()).to.be(-58);
// The 1st object stays on the platform.
expect(object.getY()).to.be(-30);
// The 2nd object can't jump on itself.
for (let i = 0; i < 4; ++i) {
object2.getBehavior('PlatformerObject').simulateJumpKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getY()).to.be.below(object.getY() - object2.getHeight());
expect(object.getX()).to.be(0);
expect(object.getY()).to.be(-30);
// At the 1st frame of collision, the result depends on execution order.
// The effect on the 1st object is not tested.
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be.above(object.getY() - object2.getHeight());
// 1st the object can be pushed down, when the intersection height < 1.
runtimeScene.renderAndStep(1000 / 60);
// The falling platformer object+platform collides with the 1st object.
// The 1st object moves 1px to the left.
runtimeScene.renderAndStep(1000 / 60);
expect(object2.getY()).to.be.above(
object.getY() - object2.getHeight() + 1
);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be(-30);
// The 2nd object reaches the floor.
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be(-30);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be(-30);
// Start a jump for both objects
object.getBehavior('PlatformerObject').simulateJumpKey();
object2.getBehavior('PlatformerObject').simulateJumpKey();
for (let i = 0; i < 6; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be.below(-70);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be.below(-70);
// Try to go right for the first object: won't work because the other
// object is a platform.
for (let i = 0; i < 5; ++i) {
object.getBehavior('PlatformerObject').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be(9);
expect(object2.getY()).to.be.below(-90);
expect(object.getX()).to.be(-1);
expect(object.getY()).to.be.below(-90);
// Try to go right for the first and second object: can do.
for (let i = 0; i < 3; ++i) {
object.getBehavior('PlatformerObject').simulateRightKey();
object2.getBehavior('PlatformerObject').simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be.above(9.5);
expect(object2.getY()).to.be.below(-95);
expect(object.getX()).to.be.above(-0.8);
expect(object.getY()).to.be.below(-95);
// Let the object fall back on the floor.
for (let i = 0; i < 20; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object2.getX()).to.be.above(9.5);
expect(object2.getY()).to.be(-30);
expect(object.getX()).to.be.above(-0.8);
expect(object.getY()).to.be(-30);
});
});
describe('(jump against a wall)', function () {
/** @type {gdjs.RuntimeScene} */
let runtimeScene;
/** @type {gdjs.RuntimeObject} */
let object;
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
let behavior;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 1500,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 900,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
behavior = object.getBehavior('auto1');
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
});
[
{
wallBeing: 'distinct from the floor',
createPlatforms: (runtimeScene) => {
const floor = addPlatformObject(runtimeScene);
floor.setPosition(0, 80);
floor.setCustomWidthAndHeight(100, 20);
const wall = addPlatformObject(runtimeScene);
wall.setPosition(80, 0);
wall.setCustomWidthAndHeight(20, 100);
},
},
{
wallBeing: 'merged with the floor',
createPlatforms: (runtimeScene) => {
const platform = addFloorAndWallPlatformObject(runtimeScene);
platform.setPosition(0, 0);
},
},
].forEach(({ wallBeing, createPlatforms }) => {
it(`can jump while moving against a wall ${wallBeing}`, function () {
createPlatforms(runtimeScene);
// The object is in the corner of the platform.
object.setPosition(80 - 10, 80 - 20);
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(60 - epsilon, 60 + epsilon);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
// Jump without sustain.
behavior.simulateJumpKey();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
// The object is jumping and get higher.
for (let i = 0; i < 5; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(object.getX()).to.be(80 - 10);
expect(object.getY()).to.be.lessThan(oldY);
}
});
it(`can move against a wall ${wallBeing}`, function () {
createPlatforms(runtimeScene);
// The object is 20 pixels from the wall.
object.setPosition(80 - 10 - 10, 80 - 20);
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(60 - epsilon, 60 + epsilon);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
// It's important the character moves from several pixels per frame.
behavior.setCurrentSpeed(behavior.getMaxSpeed());
for (let i = 0; i < 10; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
// The object is against the wall.
expect(object.getX()).to.be(80 - 10);
});
});
});
describe('(jump from slopes)', function () {
/** @type {gdjs.RuntimeScene} */
let runtimeScene;
/** @type {gdjs.RuntimeObject} */
let object;
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
let behavior;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 150,
maxFallingSpeed: 1500,
acceleration: 1000000,
deceleration: 1500,
maxSpeed: 2000,
// This is a very low speed relatively to other properties.
// This is not a playable configuration.
jumpSpeed: 100,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
behavior = object.getBehavior('auto1');
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
// The object is in the slope.
object.setPosition(0, 70);
const platform = addUpSlopePlatformObject(runtimeScene);
platform.setPosition(0, 0);
});
// This is a edge case. The test can be changed if necessary.
// Usually characters jump speed is higher than the speed on Y following a
// slope.
it('can jump while moving up on a slope', function () {
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(70, 70 + 1);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
// Jump without sustain.
behavior.simulateJumpKey();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
// The object is jumping and is kind of sliding on the slope.
// This behavior is not expected but it avoid the character to be stuck
// into the floor in more common cases.
for (let i = 0; i < 19; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(object.getY()).to.be.lessThan(oldY);
// As soon as: behavior.getCurrentJumpSpeed() - behavior.getCurrentFallSpeed()
// The character is in the falling step of the jump and it can land.
expect(behavior.isFalling()).to.be(false);
}
// The character lands.
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
});
});
describe('(jump from slopes)', function () {
/** @type {gdjs.RuntimeScene} */
let runtimeScene;
/** @type {gdjs.RuntimeObject} */
let object;
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
let behavior;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 500,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
behavior = object.getBehavior('auto1');
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
// The object is in the slope.
object.setPosition(0, 70);
const platform = addUpSlopePlatformObject(runtimeScene);
platform.setPosition(0, 0);
});
it('keep its speed on X when jumping while moving up on a slope', function () {
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(70, 70 + 1);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
// Walk and gain speed.
for (let i = 0; i < 5; ++i) {
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
const walkingSpeed = behavior.getCurrentSpeed();
// Jump and keep the speed.
behavior.simulateJumpKey();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(behavior.getCurrentSpeed()).to.be.greaterThan(walkingSpeed);
for (let i = 0; i < 5; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(behavior.isFalling()).to.be(false);
expect(behavior.getCurrentSpeed()).to.be.greaterThan(walkingSpeed);
}
});
});
describe('(jump to slopes)', function () {
/** @type {gdjs.RuntimeScene} */
let runtimeScene;
/** @type {gdjs.RuntimeObject} */
let object;
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */
let behavior;
beforeEach(function () {
runtimeScene = makePlatformerTestRuntimeScene();
// Put a platformer object on a platform
object = new gdjs.TestRuntimeObject(runtimeScene, {
name: 'obj1',
type: '',
behaviors: [
{
type: 'PlatformBehavior::PlatformerObjectBehavior',
name: 'auto1',
gravity: 900,
maxFallingSpeed: 1500,
acceleration: 500,
deceleration: 1500,
maxSpeed: 500,
jumpSpeed: 200,
canGrabPlatforms: true,
ignoreDefaultControls: true,
slopeMaxAngle: 60,
jumpSustainTime: 0.2,
useLegacyTrajectory: false,
useRepeatedJump: false,
},
],
effects: [],
});
behavior = object.getBehavior('auto1');
object.setCustomWidthAndHeight(10, 20);
runtimeScene.addObject(object);
// The object is in the slope.
object.setPosition(10, -20);
addPlatformObject(runtimeScene);
const platform = addUpSlopePlatformObject(runtimeScene);
platform.setPosition(60, -100);
});
it('keep its speed on X when landing on a slope', function () {
// The object stays on the platform.
for (let i = 0; i < 5; ++i) {
runtimeScene.renderAndStep(1000 / 60);
}
expect(object.getY()).to.within(-20, -20 + 1);
expect(behavior.isFalling()).to.be(false);
expect(behavior.isFallingWithoutJumping()).to.be(false);
expect(behavior.isMoving()).to.be(false);
// Walk and gain speed.
for (let i = 0; i < 20; ++i) {
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
}
const walkingSpeed = behavior.getCurrentSpeed();
// Jump and keep the speed.
behavior.simulateJumpKey();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(behavior.getCurrentSpeed()).to.be.greaterThan(100);
for (let i = 0; i < 6; ++i) {
const oldY = object.getY();
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isJumping()).to.be(true);
expect(behavior.getCurrentSpeed()).to.be.greaterThan(100);
}
// Land on the slope and keep the speed.
behavior.simulateRightKey();
runtimeScene.renderAndStep(1000 / 60);
expect(behavior.isOnFloor()).to.be(true);
expect(behavior.getCurrentSpeed()).to.be.greaterThan(100);
});
});
});