Files
GDevelop/Extensions/PlatformBehavior/platformerobjectruntimebehavior.ts
T
ClementPasteau a1b840a4fa Add Release Ladder action to Platformer Object Behavior (#2892)
Allow a Platformer Object to simulate a "Release Ladder" action
2021-08-12 15:54:19 +02:00

1675 lines
50 KiB
TypeScript

/*
GDevelop - Platform Behavior Extension
Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
/**
* PlatformerObjectRuntimeBehavior represents a behavior allowing objects to be
* considered as a platform by objects having PlatformerObject Behavior.
*/
export class PlatformerObjectRuntimeBehavior extends gdjs.RuntimeBehavior {
// To achieve pixel-perfect precision when positioning object on platform or
// handling collision with "walls", edges of the hitboxes must be ignored during
// collision checks, so that two overlapping edges are not considered as colliding.
// For example, if a character is 10px width and is at position (0, 0), it must not be
// considered as colliding with a platform which is at position (10, 0). Edges will
// still be overlapping (because character hitbox right edge is at X position 10 and
// platform hitbox left edge is also at X position 10).
// This parameter "_ignoreTouchingEdges" will be passed to all collision handling functions.
_ignoreTouchingEdges: boolean = true;
_roundCoordinates: boolean;
_gravity: float;
private _maxFallingSpeed: float;
_ladderClimbingSpeed: float;
private _acceleration: float;
private _deceleration: float;
private _maxSpeed: float;
_jumpSpeed: float;
_canGrabPlatforms: boolean;
private _yGrabOffset: any;
private _xGrabTolerance: any;
_jumpSustainTime: float;
_currentFallSpeed: float = 0;
private _currentSpeed: float = 0;
_canJump: boolean = false;
private _ignoreDefaultControls: boolean;
private _leftKey: boolean = false;
private _rightKey: boolean = false;
private _ladderKey: boolean = false;
_upKey: boolean = false;
_downKey: boolean = false;
_jumpKey: boolean = false;
_releasePlatformKey: boolean = false;
_releaseLadderKey: boolean = false;
private _state: State;
_falling: Falling;
_onFloor: OnFloor;
_jumping: Jumping;
_grabbingPlatform: GrabbingPlatform;
_onLadder: OnLadder;
/** Platforms near the object, updated with `_updatePotentialCollidingObjects`. */
_potentialCollidingObjects: Array<gdjs.PlatformRuntimeBehavior>;
/** Overlapped jump-thru platforms, updated with `_updateOverlappedJumpThru`. */
private _overlappedJumpThru: Array<gdjs.PlatformRuntimeBehavior>;
private _hasReallyMoved: boolean = false;
private _manager: gdjs.PlatformObjectsManager;
private _slopeMaxAngle: float;
_slopeClimbingFactor: float = 1;
_requestedDeltaX: float = 0;
_requestedDeltaY: float = 0;
_lastDeltaY: float = 0;
constructor(
runtimeScene: gdjs.RuntimeScene,
behaviorData,
owner: gdjs.RuntimeObject
) {
super(runtimeScene, behaviorData, owner);
this._roundCoordinates = behaviorData.roundCoordinates;
this._gravity = behaviorData.gravity;
this._maxFallingSpeed = behaviorData.maxFallingSpeed;
this._ladderClimbingSpeed = behaviorData.ladderClimbingSpeed || 150;
this._acceleration = behaviorData.acceleration;
this._deceleration = behaviorData.deceleration;
this._maxSpeed = behaviorData.maxSpeed;
this._jumpSpeed = behaviorData.jumpSpeed;
this._canGrabPlatforms = behaviorData.canGrabPlatforms || false;
this._yGrabOffset = behaviorData.yGrabOffset || 0;
this._xGrabTolerance = behaviorData.xGrabTolerance || 10;
this._jumpSustainTime = behaviorData.jumpSustainTime || 0;
this._ignoreDefaultControls = behaviorData.ignoreDefaultControls;
this._potentialCollidingObjects = [];
this._overlappedJumpThru = [];
this._slopeMaxAngle = 0;
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
this._manager = gdjs.PlatformObjectsManager.getManager(runtimeScene);
this._falling = new Falling(this);
this._onFloor = new OnFloor(this);
this._jumping = new Jumping(this);
this._grabbingPlatform = new GrabbingPlatform(this);
this._onLadder = new OnLadder(this);
this._state = this._falling;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
if (
oldBehaviorData.roundCoordinates !== newBehaviorData.roundCoordinates
) {
this._roundCoordinates = newBehaviorData.roundCoordinates;
}
if (oldBehaviorData.gravity !== newBehaviorData.gravity) {
this.setGravity(newBehaviorData.gravity);
}
if (oldBehaviorData.maxFallingSpeed !== newBehaviorData.maxFallingSpeed) {
this.setMaxFallingSpeed(newBehaviorData.maxFallingSpeed);
}
if (oldBehaviorData.acceleration !== newBehaviorData.acceleration) {
this.setAcceleration(newBehaviorData.acceleration);
}
if (oldBehaviorData.deceleration !== newBehaviorData.deceleration) {
this.setDeceleration(newBehaviorData.deceleration);
}
if (oldBehaviorData.maxSpeed !== newBehaviorData.maxSpeed) {
this.setMaxSpeed(newBehaviorData.maxSpeed);
}
if (oldBehaviorData.jumpSpeed !== newBehaviorData.jumpSpeed) {
this.setJumpSpeed(newBehaviorData.jumpSpeed);
}
if (
oldBehaviorData.canGrabPlatforms !== newBehaviorData.canGrabPlatforms
) {
this.setCanGrabPlatforms(newBehaviorData.canGrabPlatforms);
}
if (oldBehaviorData.yGrabOffset !== newBehaviorData.yGrabOffset) {
this._yGrabOffset = newBehaviorData.yGrabOffset;
}
if (oldBehaviorData.xGrabTolerance !== newBehaviorData.xGrabTolerance) {
this._xGrabTolerance = newBehaviorData.xGrabTolerance;
}
if (oldBehaviorData.jumpSustainTime !== newBehaviorData.jumpSustainTime) {
this.setJumpSustainTime(newBehaviorData.jumpSustainTime);
}
return true;
}
doStepPreEvents(runtimeScene: gdjs.RuntimeScene) {
const LEFTKEY = 37;
const UPKEY = 38;
const RIGHTKEY = 39;
const DOWNKEY = 40;
const LSHIFTKEY = 1016;
const RSHIFTKEY = 2016;
const SPACEKEY = 32;
const object = this.owner;
const timeDelta = this.owner.getElapsedTime(runtimeScene) / 1000;
//0.1) Get the player input:
this._requestedDeltaX = 0;
this._requestedDeltaY = 0;
const inputManager = runtimeScene.getGame().getInputManager();
this._leftKey ||
(this._leftKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(LEFTKEY));
this._rightKey ||
(this._rightKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(RIGHTKEY));
this._jumpKey ||
(this._jumpKey =
!this._ignoreDefaultControls &&
(inputManager.isKeyPressed(LSHIFTKEY) ||
inputManager.isKeyPressed(RSHIFTKEY) ||
inputManager.isKeyPressed(SPACEKEY)));
this._ladderKey ||
(this._ladderKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(UPKEY));
this._upKey ||
(this._upKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(UPKEY));
this._downKey ||
(this._downKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(DOWNKEY));
this._releasePlatformKey ||
(this._releasePlatformKey =
!this._ignoreDefaultControls && inputManager.isKeyPressed(DOWNKEY));
this._requestedDeltaX += this._updateSpeed(timeDelta);
//0.2) Track changes in object size
this._state.beforeUpdatingObstacles(timeDelta);
this._onFloor._oldHeight = object.getHeight();
//0.3) Update list of platforms around/related to the object
//Compute the list of the objects that will be used
this._updatePotentialCollidingObjects(
Math.max(this._requestedDeltaX, this._maxFallingSpeed * timeDelta)
);
this._updateOverlappedJumpThru();
//1) X axis:
this._state.checkTransitionBeforeX();
this._state.beforeMovingX();
//Ensure the object is not stuck
if (this._separateFromPlatforms(this._potentialCollidingObjects, true)) {
//After being unstuck, the object must be able to jump again.
this._canJump = true;
}
const oldX = object.getX();
this._moveX();
//2) Y axis:
this._state.checkTransitionBeforeY(timeDelta);
this._state.beforeMovingY(timeDelta, oldX);
const oldY = object.getY();
this._moveY();
//3) Update the current floor data for the next tick:
this._updateOverlappedJumpThru();
//TODO what about a moving platforms, remove this condition to do the same as for grabbing?
if (this._state !== this._onLadder) {
this._checkTransitionOnFloorOrFalling();
}
//4) Do not forget to reset pressed keys
this._leftKey = false;
this._rightKey = false;
this._ladderKey = false;
this._releaseLadderKey = false;
this._upKey = false;
this._downKey = false;
this._releasePlatformKey = false;
this._jumpKey = false;
//5) Track the movement
this._hasReallyMoved =
Math.abs(object.getX() - oldX) >= 1 ||
Math.abs(object.getY() - oldY) >= 1;
this._lastDeltaY = object.getY() - oldY;
}
doStepPostEvents(runtimeScene: gdjs.RuntimeScene) {}
private _updateSpeed(timeDelta: float): float {
//Change the speed according to the player's input.
// @ts-ignore
if (this._leftKey) {
this._currentSpeed -= this._acceleration * timeDelta;
}
if (this._rightKey) {
this._currentSpeed += this._acceleration * timeDelta;
}
//Take deceleration into account only if no key is pressed.
if (this._leftKey === this._rightKey) {
const wasPositive = this._currentSpeed > 0;
this._currentSpeed -=
this._deceleration * timeDelta * (wasPositive ? 1.0 : -1.0);
//Set the speed to 0 if the speed was too low.
if (wasPositive && this._currentSpeed < 0) {
this._currentSpeed = 0;
}
if (!wasPositive && this._currentSpeed > 0) {
this._currentSpeed = 0;
}
}
if (this._currentSpeed > this._maxSpeed) {
this._currentSpeed = this._maxSpeed;
}
if (this._currentSpeed < -this._maxSpeed) {
this._currentSpeed = -this._maxSpeed;
}
return this._currentSpeed * timeDelta;
}
private _moveX() {
const object = this.owner;
//Move the object on x axis.
const oldX = object.getX();
if (this._requestedDeltaX !== 0) {
const floorPlatformId =
this._onFloor.getFloorPlatform() !== null
? this._onFloor.getFloorPlatform()!.owner.id
: null;
object.setX(object.getX() + this._requestedDeltaX);
let tryRounding = true;
//Colliding: Try to push out from the solid.
//Note that jump thru are never obstacle on X axis.
while (
this._isCollidingWithOneOf(
this._potentialCollidingObjects,
floorPlatformId,
/*excludeJumpthrus=*/
true
)
) {
if (
(this._requestedDeltaX > 0 && object.getX() <= oldX) ||
(this._requestedDeltaX < 0 && object.getX() >= oldX)
) {
object.setX(
//Unable to move the object without being stuck in an obstacle.
oldX
);
break;
}
//If on floor: try get up a bit to bypass not perfectly aligned floors.
if (this._state === this._onFloor) {
object.setY(object.getY() - 1);
if (
!this._isCollidingWithOneOf(
this._potentialCollidingObjects,
floorPlatformId,
/*excludeJumpthrus=*/
true
)
) {
break;
}
object.setY(object.getY() + 1);
}
if (tryRounding) {
// First try rounding the position as this might be sufficient to get the object
// out of the wall.
object.setX(Math.round(object.getX()));
tryRounding = false;
} else {
object.setX(
Math.round(object.getX()) + (this._requestedDeltaX > 0 ? -1 : 1)
);
}
this._currentSpeed = 0;
}
}
}
private _moveY() {
const object = this.owner;
//Move the object on Y axis
if (this._requestedDeltaY !== 0) {
let oldY = object.getY();
object.setY(object.getY() + this._requestedDeltaY);
//Stop when colliding with an obstacle.
while (
(this._requestedDeltaY < 0 &&
this._isCollidingWithOneOf(
this._potentialCollidingObjects,
null,
/*excludeJumpThrus=*/
true
)) ||
//Jumpthru = obstacle <=> Never when going up
(this._requestedDeltaY > 0 &&
this._isCollidingWithOneOfExcluding(
this._potentialCollidingObjects,
this._overlappedJumpThru
))
) {
//Jumpthru = obstacle <=> Only if not already overlapped when going down
if (this._state === this._jumping) {
this._setFalling();
}
if (
(this._requestedDeltaY > 0 && object.getY() <= oldY) ||
(this._requestedDeltaY < 0 && object.getY() >= oldY)
) {
object.setY(
//Unable to move the object without being stuck in an obstacle.
oldY
);
break;
}
object.setY(
Math.floor(object.getY()) + (this._requestedDeltaY > 0 ? -1 : 1)
);
}
}
}
_setFalling() {
this._state.leave();
this._state = this._falling;
this._falling.enter();
}
private _setOnFloor(collidingPlatform: PlatformRuntimeBehavior) {
this._state.leave();
this._state = this._onFloor;
this._onFloor.enter(collidingPlatform);
}
private _setJumping() {
this._state.leave();
const from = this._state;
this._state = this._jumping;
this._jumping.enter(from);
}
private _setGrabbingPlatform(grabbedPlatform: PlatformRuntimeBehavior) {
this._state.leave();
this._state = this._grabbingPlatform;
this._grabbingPlatform.enter(grabbedPlatform);
}
private _setOnLadder() {
this._state.leave();
this._state = this._onLadder;
this._onLadder.enter();
}
_checkTransitionOnLadder() {
if (this._ladderKey && this._isOverlappingLadder()) {
this._setOnLadder();
}
}
_checkTransitionJumping() {
if (this._canJump && this._jumpKey) {
this._setJumping();
}
}
_checkGrabPlatform() {
const object = this.owner;
let oldX = object.getX();
object.setX(
object.getX() +
(this._requestedDeltaX > 0
? this._xGrabTolerance
: -this._xGrabTolerance)
);
const collidingPlatforms: PlatformRuntimeBehavior[] = gdjs.staticArray(
PlatformerObjectRuntimeBehavior.prototype._checkGrabPlatform
);
collidingPlatforms.length = 0;
for (const platform of this._potentialCollidingObjects) {
if (this._isCollidingWith(platform) && this._canGrab(platform)) {
collidingPlatforms.push(platform);
}
}
object.setX(oldX);
//Check if we can grab the collided platform
let oldY = object.getY();
for (const collidingPlatform of collidingPlatforms) {
object.setY(
collidingPlatform.owner.getY() +
collidingPlatform.getYGrabOffset() -
this._yGrabOffset
);
if (
!this._isCollidingWithOneOf(
this._potentialCollidingObjects,
null,
/*excludeJumpthrus=*/
true
)
) {
this._setGrabbingPlatform(collidingPlatform);
this._requestedDeltaY = 0;
collidingPlatforms.length = 0;
return;
}
object.setY(oldY);
}
collidingPlatforms.length = 0;
}
private _checkTransitionOnFloorOrFalling() {
const object = this.owner;
//Check if the object is on a floor:
//In priority, check if the last floor platform is still the floor.
let oldY = object.getY();
object.setY(object.getY() + 1);
if (
this._state === this._onFloor &&
gdjs.RuntimeObject.collisionTest(
object,
this._onFloor.getFloorPlatform()!.owner,
this._ignoreTouchingEdges
)
) {
//Still on the same floor
this._onFloor.updateFloorPosition();
} else {
// Avoid landing on a platform if the object is not going down.
// (which could happen for Jumpthru, when the object jump and pass just at the top
// of a jumpthru, it could be considered as landing if not this this extra check).
const canLand = this._requestedDeltaY >= 0;
//Check if landing on a new floor: (Exclude already overlapped jump thru)
let collidingPlatform = this._getCollidingPlatform();
if (canLand && collidingPlatform !== null) {
//Register the colliding platform as the floor.
this._setOnFloor(collidingPlatform);
} else if (this._state === this._onFloor) {
// don't fall if GrabbingPlatform or OnLadder
this._setFalling();
}
}
object.setY(oldY);
}
_fall(timeDelta: float) {
this._currentFallSpeed += this._gravity * timeDelta;
if (this._currentFallSpeed > this._maxFallingSpeed) {
this._currentFallSpeed = this._maxFallingSpeed;
}
this._requestedDeltaY += this._currentFallSpeed * timeDelta;
this._requestedDeltaY = Math.min(
this._requestedDeltaY,
this._maxFallingSpeed * timeDelta
);
}
//Scene change is not supported
/*
if ( parentScene != &scene ) //Parent scene has changed
{
parentScene = &scene;
sceneManager = parentScene ? &ScenePlatformObjectsManager::managers[&scene] : null;
floorPlatform = null;
}
*/
/**
* Return true if the object owning the behavior can grab the specified platform. There must be a collision
* between the object and the platform.
* @param platform The platform the object is in collision with
* @param y The value in pixels on Y axis the object wants to move to
*/
private _canGrab(platform: gdjs.PlatformRuntimeBehavior) {
const y1 = this.owner.getY() + this._yGrabOffset - this._lastDeltaY;
const y2 = this.owner.getY() + this._yGrabOffset;
const platformY = platform.owner.getY() + platform.getYGrabOffset();
return (
platform.canBeGrabbed() &&
((y1 < platformY && platformY < y2) ||
(y2 < platformY && platformY < y1))
);
}
/**
* Mark the platformer object as not grabbing any platform.
*/
_releaseGrabbedPlatform() {
if (this._state === this._grabbingPlatform) {
this._setFalling();
}
}
/**
* Mark the platformer object as falling if on a ladder.
*/
_releaseLadder() {
if (this._state === this._onLadder) {
this._setFalling();
}
}
/**
* Separate the object from all platforms passed in parameter.
* @param candidates The platform to be tested for collision
* @param excludeJumpThrus If set to true, jumpthru platforms are excluded. false if not defined.
*/
private _separateFromPlatforms(
candidates: gdjs.PlatformRuntimeBehavior[],
excludeJumpThrus: boolean
) {
excludeJumpThrus = !!excludeJumpThrus;
const objects = gdjs.staticArray(
PlatformerObjectRuntimeBehavior.prototype._separateFromPlatforms
);
objects.length = 0;
for (let i = 0; i < candidates.length; ++i) {
const platform = candidates[i];
if (
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
) {
continue;
}
if (
excludeJumpThrus &&
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.JUMPTHRU
) {
continue;
}
objects.push(platform.owner);
}
return this.owner.separateFromObjects(objects, this._ignoreTouchingEdges);
}
/**
* Among the platforms passed in parameter, return true if there is a platform colliding with the object.
* Ladders are *always* excluded from the test.
* @param candidates The platform to be tested for collision
* @param exceptThisOne The object identifier of a platform to be excluded from the check. Can be null.
* @param excludeJumpThrus If set to true, jumpthru platforms are excluded. false if not defined.
*/
_isCollidingWithOneOf(
candidates: gdjs.PlatformRuntimeBehavior[],
exceptThisOne?: number | null,
excludeJumpThrus?: boolean
) {
excludeJumpThrus = !!excludeJumpThrus;
for (let i = 0; i < candidates.length; ++i) {
const platform = candidates[i];
if (platform.owner.id === exceptThisOne) {
continue;
}
if (
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
) {
continue;
}
if (
excludeJumpThrus &&
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.JUMPTHRU
) {
continue;
}
if (
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
) {
return true;
}
}
return false;
}
/**
* Among the platforms passed in parameter, return true if there is a platform colliding with the object.
* Ladders are *always* excluded from the test.
* @param candidates The platform to be tested for collision
* @param exceptTheseOnes The platforms to be excluded from the test
*/
private _isCollidingWithOneOfExcluding(
candidates: gdjs.PlatformRuntimeBehavior[],
exceptTheseOnes: gdjs.PlatformRuntimeBehavior[]
) {
for (let i = 0; i < candidates.length; ++i) {
const platform = candidates[i];
if (exceptTheseOnes && this._isIn(exceptTheseOnes, platform.owner.id)) {
continue;
}
if (
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
) {
continue;
}
if (
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
) {
return true;
}
}
return false;
}
/**
* Return (one of) the platform which is colliding with the behavior owner object.
* Overlapped jump thru and ladders are excluded.
* _updatePotentialCollidingObjects and _updateOverlappedJumpThru should have been called before.
*/
private _getCollidingPlatform() {
for (let i = 0; i < this._potentialCollidingObjects.length; ++i) {
const platform = this._potentialCollidingObjects[i];
if (
platform.getPlatformType() !== gdjs.PlatformRuntimeBehavior.LADDER &&
!this._isIn(this._overlappedJumpThru, platform.owner.id) &&
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
) {
return platform;
}
}
//Nothing is being colliding with the behavior object.
return null;
}
/**
* Return true if the platform is colliding with the behavior owner object.
* Overlapped jump thru and ladders are excluded.
* @param platform The platform to be tested for collision
*/
private _isCollidingWith(platform: gdjs.PlatformRuntimeBehavior): boolean {
return (
platform.getPlatformType() !== gdjs.PlatformRuntimeBehavior.LADDER &&
!this._isIn(this._overlappedJumpThru, platform.owner.id) &&
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
);
}
/**
* Update _overlappedJumpThru member, so that it contains all the jumpthru platforms colliding with
* the behavior owner object.
* Note: _updatePotentialCollidingObjects must have been called before.
*/
private _updateOverlappedJumpThru() {
this._overlappedJumpThru.length = 0;
for (let i = 0; i < this._potentialCollidingObjects.length; ++i) {
const platform = this._potentialCollidingObjects[i];
if (
platform.getPlatformType() ===
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
) {
this._overlappedJumpThru.push(platform);
}
}
}
/**
* Return true if the object is overlapping a ladder.
* Note: _updatePotentialCollidingObjects must have been called before.
*/
_isOverlappingLadder() {
for (let i = 0; i < this._potentialCollidingObjects.length; ++i) {
const platform = this._potentialCollidingObjects[i];
if (
platform.getPlatformType() !== gdjs.PlatformRuntimeBehavior.LADDER
) {
continue;
}
if (
gdjs.RuntimeObject.collisionTest(
this.owner,
platform.owner,
this._ignoreTouchingEdges
)
) {
return true;
}
}
return false;
}
_isIn(platformArray: gdjs.PlatformRuntimeBehavior[], id: integer) {
for (let i = 0; i < platformArray.length; ++i) {
if (platformArray[i].owner.id === id) {
return true;
}
}
return false;
}
/**
* Update _potentialCollidingObjects member with platforms near the object.
*/
private _updatePotentialCollidingObjects(maxMovementLength: float) {
this._manager.getAllPlatformsAround(
this.owner,
maxMovementLength,
this._potentialCollidingObjects
);
// Filter the potential colliding platforms to ensure that the object owning the behavior
// is not considered as colliding with itself, in the case that it also has the
// platform behavior.
for (let i = 0; i < this._potentialCollidingObjects.length; ) {
if (this._potentialCollidingObjects[i].owner === this.owner) {
this._potentialCollidingObjects.splice(i, 1);
} else {
i++;
}
}
}
/**
* Simulate a control action in the Platformer Object by specifying an input.
* @param input The string expression of the control action [Left,Right,Up,Down,Ladder,Jump,Release,Release Ladder].
*/
simulateControl(input: string) {
if (input === 'Left') {
this._leftKey = true;
} else if (input === 'Right') {
this._rightKey = true;
} else if (input === 'Up') {
this._upKey = true;
} else if (input === 'Down') {
this._downKey = true;
} else if (input === 'Ladder') {
this._ladderKey = true;
} else if (input === 'Jump') {
this._jumpKey = true;
} else if (input === 'Release') {
this._releasePlatformKey = true;
} else if (input === 'Release Ladder') {
this._releaseLadderKey = true;
}
}
/**
* Get the gravity of the Platformer Object.
* @returns The current gravity.
*/
getGravity(): float {
return this._gravity;
}
/**
* Get the maximum falling speed of the Platformer Object.
* @returns The maximum falling speed.
*/
getMaxFallingSpeed(): float {
return this._maxFallingSpeed;
}
/**
* Get the speed used to move on Y axis when climbing a ladder.
* @returns The speed of ladder climbing.
*/
getLadderClimbingSpeed(): float {
return this._ladderClimbingSpeed;
}
/**
* Get the acceleration value of the Platformer Object.
* @returns The current acceleration.
*/
getAcceleration(): float {
return this._acceleration;
}
/**
* Get the deceleration of the Platformer Object.
* @returns The current deceleration.
*/
getDeceleration(): float {
return this._deceleration;
}
/**
* Get the maximum speed of the Platformer Object.
* @returns The maximum speed.
*/
getMaxSpeed(): float {
return this._maxSpeed;
}
/**
* Get the jump speed of the Platformer Object.
* @returns The jump speed.
*/
getJumpSpeed(): float {
return this._jumpSpeed;
}
/**
* Get the jump sustain time of the Platformer Object.
* @returns The jump sustain time.
*/
getJumpSustainTime(): float {
return this._jumpSustainTime;
}
/**
* Get the speed at which the object is falling. It is 0 when the object is on a floor, and non 0 as soon as the object leaves the floor.
* @returns The current fall speed.
*/
getCurrentFallSpeed(): float {
return this._currentFallSpeed;
}
/**
* Get the current speed of the Platformer Object.
* @returns The current speed.
*/
getCurrentSpeed(): float {
return this._currentSpeed;
}
/**
* Get the current jump speed of the Platformer Object.
* @returns The current jump speed.
*/
getCurrentJumpSpeed(): float {
return this._jumping.getCurrentJumpSpeed();
}
/**
* Check if the Platformer Object can grab the platforms.
* @returns Returns true if the object can grab the platforms.
*/
canGrabPlatforms(): boolean {
return this._canGrabPlatforms;
}
/**
* Check if the Platformer Object can jump.
* @returns Returns true if the object can jump.
*/
canJump(): boolean {
return this._canJump;
}
/**
* Set the gravity of the Platformer Object.
* @param gravity The new gravity.
*/
setGravity(gravity: float): void {
this._gravity = gravity;
}
/**
* Set the maximum falling speed of the Platformer Object.
* @param maxFallingSpeed The maximum falling speed.
*/
setMaxFallingSpeed(maxFallingSpeed: float): void {
this._maxFallingSpeed = maxFallingSpeed;
}
/**
* Set the speed used to move on Y axis when climbing a ladder.
* @param ladderClimbingSpeed The speed of ladder climbing.
*/
setLadderClimbingSpeed(ladderClimbingSpeed: float): void {
this._ladderClimbingSpeed = ladderClimbingSpeed;
}
/**
* Set the acceleration of the Platformer Object.
* @param acceleration The new acceleration.
*/
setAcceleration(acceleration: float): void {
this._acceleration = acceleration;
}
/**
* Set the deceleration of the Platformer Object.
* @param deceleration The new deceleration.
*/
setDeceleration(deceleration: float): void {
this._deceleration = deceleration;
}
/**
* Set the maximum speed of the Platformer Object.
* @param maxSpeed The new maximum speed.
*/
setMaxSpeed(maxSpeed: float): void {
this._maxSpeed = maxSpeed;
}
/**
* Set the jump speed of the Platformer Object.
* @param jumpSpeed The new jump speed.
*/
setJumpSpeed(jumpSpeed: float): void {
this._jumpSpeed = jumpSpeed;
}
/**
* Set the jump sustain time of the Platformer Object.
* @param jumpSpeed The new jump sustain time.
*/
setJumpSustainTime(jumpSustainTime: float): void {
this._jumpSustainTime = jumpSustainTime;
}
/**
* Set the maximum slope angle of the Platformer Object.
* @param slopeMaxAngle The new maximum slope angle.
*/
setSlopeMaxAngle(slopeMaxAngle: float): void {
if (slopeMaxAngle < 0 || slopeMaxAngle >= 90) {
return;
}
this._slopeMaxAngle = slopeMaxAngle;
//Avoid rounding errors
if (slopeMaxAngle === 45) {
this._slopeClimbingFactor = 1;
} else {
this._slopeClimbingFactor = Math.tan(
(slopeMaxAngle * 3.1415926) / 180.0
);
}
}
/**
* Allow the Platformer Object to jump again.
*/
setCanJump(): void {
this._canJump = true;
}
/**
* Forbid the Platformer Object to air jump.
*/
setCanNotAirJump(): void {
if (this._state === this._jumping || this._state === this._falling) {
this._canJump = false;
}
}
/**
* Set if the Platformer Object can grab platforms.
* @param enable Enable / Disable grabbing of platforms.
*/
setCanGrabPlatforms(enable: boolean): void {
this._canGrabPlatforms = enable;
if (!this._canGrabPlatforms) {
this._releaseGrabbedPlatform();
}
}
/**
* Ignore the default controls of the Platformer Object.
* @param ignore Enable / Disable default controls.
*/
ignoreDefaultControls(ignore: boolean) {
this._ignoreDefaultControls = ignore;
}
/**
* Simulate the "Left" control of the Platformer Object.
*/
simulateLeftKey() {
this._leftKey = true;
}
/**
* Simulate the "Right" control of the Platformer Object.
*/
simulateRightKey() {
this._rightKey = true;
}
/**
* Simulate the "Ladder" control of the Platformer Object.
*/
simulateLadderKey() {
this._ladderKey = true;
}
/**
* Simulate the "Release Ladder" control of the Platformer Object.
*/
simulateReleaseLadderKey() {
this._releaseLadderKey = true;
}
/**
* Simulate the "Up" control of the Platformer Object.
*/
simulateUpKey() {
this._upKey = true;
}
/**
* Simulate the "Down" control of the Platformer Object.
*/
simulateDownKey() {
this._downKey = true;
}
/**
* Simulate the "Jump" control of the Platformer Object.
*/
simulateJumpKey() {
this._jumpKey = true;
}
/**
* Simulate the "Release" control of the Platformer Object.
*/
simulateReleasePlatformKey() {
this._releasePlatformKey = true;
}
/**
* Check if the Platformer Object is on the floor.
* @returns Returns true if on the floor and false if not.
*/
isOnFloor(): boolean {
return this._state === this._onFloor;
}
/**
* Check if the Platformer Object is on a ladder.
* @returns Returns true if on a ladder and false if not.
*/
isOnLadder(): boolean {
return this._state === this._onLadder;
}
/**
* Check if the Platformer Object is jumping.
* @returns Returns true if jumping and false if not.
*/
isJumping(): boolean {
return this._state === this._jumping;
}
/**
* Check if the Platformer Object is grabbing a platform.
* @returns Returns true if a platform is grabbed and false if not.
*/
isGrabbingPlatform(): boolean {
return this._state === this._grabbingPlatform;
}
/**
* Check if the Platformer Object is in the falling state. This is false
* if the object is jumping, even if the object is going down after reaching
* the jump peak.
* @returns Returns true if it is falling and false if not.
*/
isFallingWithoutJumping(): boolean {
return this._state === this._falling;
}
/**
* Check if the Platformer Object is "going down", either because it's in the
* falling state *or* because it's jumping but reached the jump peak and
* is now going down (because the jump speed can't compensate anymore the
* falling speed).
*
* If you want to check if the object is falling outside of a jump (or because
* the jump is entirely finished and there is no jump speed applied to the object
* anymore), consider using `isFallingWithoutJumping`.
*
* @returns Returns true if it is "going down" and false if not.
*/
isFalling(): boolean {
return (
this._state === this._falling ||
(this._state === this._jumping &&
this._currentFallSpeed > this._jumping.getCurrentJumpSpeed())
);
}
/**
* Check if the Platformer Object is moving.
* @returns Returns true if it is moving and false if not.
*/
isMoving(): boolean {
return (
(this._hasReallyMoved &&
(this._currentSpeed !== 0 || this._state === this._onLadder)) ||
this._jumping.getCurrentJumpSpeed() !== 0 ||
this._currentFallSpeed !== 0
);
}
}
/**
* The object can take 5 states: OnFloor, Falling, Jumping, GrabbingPlatform and OnLadder.
* The implementations of this interface hold the specific behaviors and internal state of theses 5 states.
* @see PlatformerObjectRuntimeBehavior.doStepPreEvents to understand how the functions are called.
*/
interface State {
/**
* Called when the object leaves this state.
* It's a good place to reset the internal state.
* @see OnFloor.enter that is not part of the interface because it takes specific parameters.
*/
leave(): void;
/**
* Called before the obstacle search.
* The object position may need adjustments to handle external changes.
*/
beforeUpdatingObstacles(timeDelta: float): void;
/**
* Check if transitions to other states are needed and apply them before moving horizontally.
*/
checkTransitionBeforeX(): void;
/**
* Use _requestedDeltaX and _requestedDeltaY to choose the movement that suits the state before moving horizontally.
*/
beforeMovingX(): void;
/**
* Check if transitions to other states are needed and apply them before moving vertically.
*/
checkTransitionBeforeY(timeDelta: float): void;
/**
* Use _requestedDeltaY to choose the movement that suits the state before moving vertically.
*/
beforeMovingY(timeDelta: float, oldX: float): void;
}
/**
* The object is on the floor standing or walking.
*/
class OnFloor implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
private _floorPlatform: PlatformRuntimeBehavior | null = null;
private _floorLastX: float = 0;
private _floorLastY: float = 0;
_oldHeight: float = 0;
constructor(behavior: PlatformerObjectRuntimeBehavior) {
this._behavior = behavior;
}
getFloorPlatform() {
return this._floorPlatform;
}
enter(floorPlatform: PlatformRuntimeBehavior) {
this._floorPlatform = floorPlatform;
this.updateFloorPosition();
this._behavior._canJump = true;
this._behavior._currentFallSpeed = 0;
}
leave() {
this._floorPlatform = null;
}
updateFloorPosition() {
this._floorLastX = this._floorPlatform!.owner.getX();
this._floorLastY = this._floorPlatform!.owner.getY();
}
beforeUpdatingObstacles(timeDelta: float) {
const object = this._behavior.owner;
//Stick the object to the floor if its height has changed.
if (this._oldHeight !== object.getHeight()) {
object.setY(
this._floorLastY -
object.getHeight() +
(object.getY() - object.getDrawableY()) -
1
);
}
}
checkTransitionBeforeX() {
const behavior = this._behavior;
//Check that the floor object still exists and is near the object.
if (
!behavior._isIn(
behavior._potentialCollidingObjects,
this._floorPlatform!.owner.id
)
) {
behavior._setFalling();
}
}
beforeMovingX() {
const behavior = this._behavior;
//Shift the object according to the floor movement.
behavior._requestedDeltaX +=
this._floorPlatform!.owner.getX() - this._floorLastX;
behavior._requestedDeltaY +=
this._floorPlatform!.owner.getY() - this._floorLastY;
}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
//Go on a ladder
behavior._checkTransitionOnLadder();
//Jumping
behavior._checkTransitionJumping();
}
beforeMovingY(timeDelta: float, oldX: float) {
const behavior = this._behavior;
const object = behavior.owner;
//Follow the floor
if (
gdjs.RuntimeObject.collisionTest(
object,
this._floorPlatform!.owner,
behavior._ignoreTouchingEdges
)
) {
//Floor is getting up, as the object is colliding with it.
let oldY = object.getY();
let step = 0;
let stillInFloor = false;
do {
if (
step >=
Math.floor(
Math.abs(
behavior._requestedDeltaX * behavior._slopeClimbingFactor
)
)
) {
//Slope is too step ( > 50% )
object.setY(
object.getY() -
(Math.abs(
behavior._requestedDeltaX * behavior._slopeClimbingFactor
) -
step)
);
//Try to add the decimal part.
if (
gdjs.RuntimeObject.collisionTest(
object,
this._floorPlatform!.owner,
behavior._ignoreTouchingEdges
)
) {
stillInFloor = true;
}
//Too steep.
break;
}
//Try to get out of the floor.
object.setY(object.getY() - 1);
step++;
} while (
gdjs.RuntimeObject.collisionTest(
object,
this._floorPlatform!.owner,
behavior._ignoreTouchingEdges
)
);
if (stillInFloor) {
object.setY(
//Unable to follow the floor ( too steep ): Go back to the original position.
oldY
);
object.setX(
//And also revert the shift on X axis.
oldX
);
}
} else {
//Floor is flat or get down.
let oldY = object.getY();
const tentativeStartY = object.getY() + 1;
object.setY(
behavior._roundCoordinates
? Math.round(tentativeStartY)
: tentativeStartY
);
let step = 0;
let noMoreOnFloor = false;
while (
!behavior._isCollidingWithOneOf(behavior._potentialCollidingObjects)
) {
if (
step >
Math.abs(behavior._requestedDeltaX * behavior._slopeClimbingFactor)
) {
//Slope is too step ( > 50% )
noMoreOnFloor = true;
break;
}
//Object was on floor, but no more: Maybe a slope, try to follow it.
object.setY(object.getY() + 1);
step++;
}
//Unable to follow the floor: Go back to the original position. Fall will be triggered next tick.
if (noMoreOnFloor) {
object.setY(oldY);
} else {
object.setY(
//Floor touched: Go back 1 pixel over.
object.getY() - 1
);
}
}
}
toString(): String {
return 'OnFloor';
}
}
/**
* The object is falling.
*/
class Falling implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
constructor(behavior: PlatformerObjectRuntimeBehavior) {
this._behavior = behavior;
}
enter() {
this._behavior._canJump = false;
}
leave() {}
beforeUpdatingObstacles(timeDelta: float) {}
checkTransitionBeforeX() {}
beforeMovingX() {}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
//Go on a ladder
behavior._checkTransitionOnLadder();
//Jumping
behavior._checkTransitionJumping();
//Grabbing a platform
if (behavior._canGrabPlatforms && behavior._requestedDeltaX !== 0) {
behavior._checkGrabPlatform();
}
}
beforeMovingY(timeDelta: float, oldX: float) {
//Fall
this._behavior._fall(timeDelta);
}
toString(): String {
return 'Falling';
}
}
/**
* The object is on the ascending and descending part of the jump.
* The object is considered falling when the jump continue to a lower position than the initial one.
*/
class Jumping implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
private _currentJumpSpeed: number = 0;
private _timeSinceCurrentJumpStart: number = 0;
private _jumpKeyHeldSinceJumpStart: boolean = false;
private _jumpingFirstDelta: boolean = false;
constructor(behavior: PlatformerObjectRuntimeBehavior) {
this._behavior = behavior;
}
getCurrentJumpSpeed() {
return this._currentJumpSpeed;
}
enter(from: State) {
const behavior = this._behavior;
this._timeSinceCurrentJumpStart = 0;
this._jumpKeyHeldSinceJumpStart = true;
if (from !== behavior._jumping && from !== behavior._falling) {
this._jumpingFirstDelta = true;
}
behavior._canJump = false;
this._currentJumpSpeed = behavior._jumpSpeed;
behavior._currentFallSpeed = 0;
}
leave() {
this._currentJumpSpeed = 0;
}
beforeUpdatingObstacles(timeDelta: float) {}
checkTransitionBeforeX() {}
beforeMovingX() {}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
//Go on a ladder
behavior._checkTransitionOnLadder();
//Jumping
behavior._checkTransitionJumping();
//Grabbing a platform
if (
behavior._canGrabPlatforms &&
behavior._requestedDeltaX !== 0 &&
behavior._lastDeltaY >= 0
) {
behavior._checkGrabPlatform();
}
}
beforeMovingY(timeDelta: float, oldX: float) {
const behavior = this._behavior;
//Fall
if (!this._jumpingFirstDelta) {
behavior._fall(timeDelta);
}
this._jumpingFirstDelta = false;
// Check if the jump key is continuously held since
// the beginning of the jump.
if (!behavior._jumpKey) {
this._jumpKeyHeldSinceJumpStart = false;
}
this._timeSinceCurrentJumpStart += timeDelta;
behavior._requestedDeltaY -= this._currentJumpSpeed * timeDelta;
// Decrease jump speed after the (optional) jump sustain time is over.
const sustainJumpSpeed =
this._jumpKeyHeldSinceJumpStart &&
this._timeSinceCurrentJumpStart < behavior._jumpSustainTime;
if (!sustainJumpSpeed) {
this._currentJumpSpeed -= behavior._gravity * timeDelta;
}
if (this._currentJumpSpeed < 0) {
behavior._setFalling();
}
}
toString(): String {
return 'Jumping';
}
}
/**
* The object grabbed the edge of a platform and is standing there.
*/
class GrabbingPlatform implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
private _grabbedPlatform: any = null;
private _grabbedPlatformLastX: any;
private _grabbedPlatformLastY: any;
constructor(behavior: PlatformerObjectRuntimeBehavior) {
this._behavior = behavior;
}
enter(grabbedPlatform: PlatformRuntimeBehavior) {
this._grabbedPlatform = grabbedPlatform;
this._behavior._canJump = true;
this._behavior._currentFallSpeed = 0;
}
leave() {
this._grabbedPlatform = null;
}
beforeUpdatingObstacles(timeDelta: float) {}
checkTransitionBeforeX() {
const behavior = this._behavior;
//Check that the grabbed platform object still exists and is near the object.
if (
!behavior._isIn(
behavior._potentialCollidingObjects,
this._grabbedPlatform.owner.id
)
) {
behavior._releaseGrabbedPlatform();
}
}
beforeMovingX() {
const behavior = this._behavior;
//Shift the object according to the grabbed platform movement.
// this erases any other movement
behavior._requestedDeltaX =
this._grabbedPlatform.owner.getX() - this._grabbedPlatformLastX;
behavior._requestedDeltaY =
this._grabbedPlatform.owner.getY() - this._grabbedPlatformLastY;
}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
//Go on a ladder
behavior._checkTransitionOnLadder();
//Release the platform
if (behavior._releasePlatformKey) {
behavior._releaseGrabbedPlatform();
}
//Jumping
behavior._checkTransitionJumping();
}
beforeMovingY(timeDelta: float, oldX: float) {
this._grabbedPlatformLastX = this._grabbedPlatform.owner.getX();
this._grabbedPlatformLastY = this._grabbedPlatform.owner.getY();
}
toString(): String {
return 'GrabbingPlatform';
}
}
/**
* The object grabbed a ladder. It can stand or move in 8 directions.
*/
class OnLadder implements State {
private _behavior: PlatformerObjectRuntimeBehavior;
constructor(behavior: PlatformerObjectRuntimeBehavior) {
this._behavior = behavior;
}
enter() {
this._behavior._canJump = true;
this._behavior._currentFallSpeed = 0;
}
leave() {}
beforeUpdatingObstacles(timeDelta: float) {}
checkTransitionBeforeX() {}
beforeMovingX() {}
checkTransitionBeforeY(timeDelta: float) {
const behavior = this._behavior;
//Coming to an extremity of a ladder
if (!behavior._isOverlappingLadder()) {
behavior._setFalling();
}
//Jumping
behavior._checkTransitionJumping();
//Release the ladder
if (behavior._releaseLadderKey) {
behavior._releaseLadder();
}
}
beforeMovingY(timeDelta: float, oldX: float) {
const behavior = this._behavior;
// TODO: we could consider supporting acceleration for ladder climbing in the future.
if (behavior._upKey) {
behavior._requestedDeltaY -= behavior._ladderClimbingSpeed * timeDelta;
}
if (behavior._downKey) {
behavior._requestedDeltaY += behavior._ladderClimbingSpeed * timeDelta;
}
}
toString(): String {
return 'OnLadder';
}
}
gdjs.registerBehavior(
'PlatformBehavior::PlatformerObjectBehavior',
gdjs.PlatformerObjectRuntimeBehavior
);
}