mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00

* New actions are available to save & load the game state, making Saving & Loading as easy as adding 1 action to your game! * While it will work in most cases, it has a few limitations and hasn't been thoroughly tested on all types of objects/behaviors and games, so it is for the moment tagged as **Experimental** while we gather feedback and improve it * Check out the wiki for more info: https://wiki.gdevelop.io/gdevelop5/all-features/save-state
2787 lines
91 KiB
TypeScript
2787 lines
91 KiB
TypeScript
/*
|
|
GDevelop - Platform Behavior Extension
|
|
Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
|
|
*/
|
|
namespace gdjs {
|
|
/**
|
|
* Returned by _findHighestFloorAndMoveOnTop
|
|
*/
|
|
type PlatformSearchResult = {
|
|
highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null;
|
|
highestGroundPolygon: gdjs.Polygon | null;
|
|
isCollidingAnyPlatform: boolean;
|
|
};
|
|
|
|
interface OnFloorStateNetworkSyncData {
|
|
flx: number;
|
|
fly: number;
|
|
oh: number;
|
|
}
|
|
|
|
interface FallingStateNetworkSyncData {}
|
|
|
|
interface JumpingStateNetworkSyncData {
|
|
cjs: number;
|
|
tscjs: number;
|
|
jfd: boolean;
|
|
}
|
|
|
|
interface GrabbingPlatformStateNetworkSyncData {
|
|
gplx: float;
|
|
gply: float;
|
|
}
|
|
|
|
interface OnLadderStateNetworkSyncData {}
|
|
|
|
type StateNetworkSyncData =
|
|
| OnFloorStateNetworkSyncData
|
|
| FallingStateNetworkSyncData
|
|
| JumpingStateNetworkSyncData
|
|
| GrabbingPlatformStateNetworkSyncData
|
|
| OnLadderStateNetworkSyncData;
|
|
|
|
interface PlatformerObjectNetworkSyncDataType {
|
|
cs: float;
|
|
rdx: float;
|
|
rdy: float;
|
|
ldy: float;
|
|
cfs: float;
|
|
cj: boolean;
|
|
ldl: boolean;
|
|
lek: boolean;
|
|
rik: boolean;
|
|
lak: boolean;
|
|
upk: boolean;
|
|
dok: boolean;
|
|
juk: boolean;
|
|
rpk: boolean;
|
|
rlk: boolean;
|
|
jkhsjs: boolean;
|
|
sn: string;
|
|
ssd: StateNetworkSyncData;
|
|
}
|
|
|
|
export interface PlatformerObjectNetworkSyncData
|
|
extends BehaviorNetworkSyncData {
|
|
props: PlatformerObjectNetworkSyncDataType;
|
|
}
|
|
|
|
/**
|
|
* PlatformerObjectRuntimeBehavior represents a behavior allowing objects to be
|
|
* considered as a platform by objects having PlatformerObject Behavior.
|
|
*/
|
|
export class PlatformerObjectRuntimeBehavior extends gdjs.RuntimeBehavior {
|
|
/**
|
|
* Returned by _findHighestFloorAndMoveOnTop
|
|
*/
|
|
private static readonly _platformSearchResult: PlatformSearchResult = {
|
|
highestGroundPlatform: null,
|
|
highestGroundPolygon: null,
|
|
isCollidingAnyPlatform: false,
|
|
};
|
|
|
|
/**
|
|
* A very small value compare to 1 pixel, yet very huge compare to rounding errors.
|
|
*/
|
|
private static readonly epsilon = 2 ** -20;
|
|
|
|
// Behavior configuration
|
|
|
|
/** 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;
|
|
|
|
private _acceleration: float;
|
|
private _deceleration: float;
|
|
private _maxSpeed: float;
|
|
private _slopeMaxAngle: float;
|
|
_slopeClimbingFactor: float = 1;
|
|
|
|
_gravity: float;
|
|
_maxFallingSpeed: float;
|
|
_jumpSpeed: float;
|
|
_jumpSustainTime: float;
|
|
|
|
_ladderClimbingSpeed: float;
|
|
|
|
_canGrabPlatforms: boolean;
|
|
_canGrabWithoutMoving: boolean;
|
|
private _yGrabOffset: any;
|
|
private _xGrabTolerance: any;
|
|
|
|
_useLegacyTrajectory: boolean;
|
|
_useRepeatedJump: boolean;
|
|
|
|
_canGoDownFromJumpthru: boolean = false;
|
|
|
|
// Behavior state
|
|
|
|
_currentSpeed: float = 0;
|
|
_requestedDeltaX: float = 0;
|
|
_requestedDeltaY: float = 0;
|
|
_lastDeltaY: float = 0;
|
|
_currentFallSpeed: float = 0;
|
|
_canJump: boolean = false;
|
|
_lastDirectionIsLeft: 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;
|
|
_jumpKeyHeldSinceJumpStart: boolean = false;
|
|
_releasePlatformKey: boolean = false;
|
|
_releaseLadderKey: boolean = false;
|
|
|
|
// This is useful when the object is synchronized by an external source
|
|
// like in a multiplayer game, and we want to be able to predict the
|
|
// movement of the object, even if the inputs are not updated every frame.
|
|
private _clearInputsBetweenFrames: boolean = true;
|
|
// This is useful when the object is synchronized over the network,
|
|
// object is controlled by the network and we want to ensure the current player
|
|
// cannot control it.
|
|
_ignoreDefaultControlsAsSyncedByNetwork: boolean = false;
|
|
|
|
// This is useful for extensions that need to know
|
|
// which keys were pressed and doesn't know the mapping
|
|
// done by the scene events.
|
|
private _wasLeftKeyPressed: boolean = false;
|
|
private _wasRightKeyPressed: boolean = false;
|
|
private _wasLadderKeyPressed: boolean = false;
|
|
private _wasUpKeyPressed: boolean = false;
|
|
private _wasDownKeyPressed: boolean = false;
|
|
private _wasJumpKeyPressed: boolean = false;
|
|
private _wasReleasePlatformKeyPressed: boolean = false;
|
|
private _wasReleaseLadderKeyPressed: 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`. */
|
|
_overlappedJumpThru: Array<gdjs.PlatformRuntimeBehavior>;
|
|
|
|
private _hasReallyMoved: boolean = false;
|
|
/** @deprecated use _hasReallyMoved instead */
|
|
private _hasMovedAtLeastOnePixel: boolean = false;
|
|
private _manager: gdjs.PlatformObjectsManager;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
behaviorData,
|
|
owner: gdjs.RuntimeObject
|
|
) {
|
|
super(instanceContainer, behaviorData, owner);
|
|
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._canGrabWithoutMoving = behaviorData.canGrabWithoutMoving;
|
|
this._yGrabOffset = behaviorData.yGrabOffset || 0;
|
|
this._xGrabTolerance = behaviorData.xGrabTolerance || 10;
|
|
this._jumpSustainTime = behaviorData.jumpSustainTime || 0;
|
|
this._ignoreDefaultControls = behaviorData.ignoreDefaultControls;
|
|
this._useLegacyTrajectory =
|
|
behaviorData.useLegacyTrajectory === undefined
|
|
? true
|
|
: behaviorData.useLegacyTrajectory;
|
|
this._useRepeatedJump =
|
|
behaviorData.useRepeatedJump === undefined
|
|
? true
|
|
: behaviorData.useRepeatedJump;
|
|
this._canGoDownFromJumpthru = behaviorData.canGoDownFromJumpthru;
|
|
this._slopeMaxAngle = 0;
|
|
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
|
|
|
|
this._potentialCollidingObjects = [];
|
|
this._overlappedJumpThru = [];
|
|
|
|
this._manager = gdjs.PlatformObjectsManager.getManager(instanceContainer);
|
|
|
|
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;
|
|
}
|
|
|
|
getNetworkSyncData(
|
|
syncOptions: GetNetworkSyncDataOptions
|
|
): PlatformerObjectNetworkSyncData {
|
|
// This method is called, so we are synchronizing this object.
|
|
// Let's clear the inputs between frames as we control it.
|
|
this._clearInputsBetweenFrames = true;
|
|
this._ignoreDefaultControlsAsSyncedByNetwork = false;
|
|
|
|
return {
|
|
...super.getNetworkSyncData(syncOptions),
|
|
props: {
|
|
cs: this._currentSpeed,
|
|
|
|
// TODO Try to remove these 3 fields from the synch
|
|
// They are reset every frame and are not part of the state.
|
|
rdx: this._requestedDeltaX,
|
|
rdy: this._requestedDeltaY,
|
|
ldy: this._lastDeltaY,
|
|
|
|
cfs: this._currentFallSpeed,
|
|
cj: this._canJump,
|
|
ldl: this._lastDirectionIsLeft,
|
|
lek: this._wasLeftKeyPressed,
|
|
rik: this._wasRightKeyPressed,
|
|
lak: this._wasLadderKeyPressed,
|
|
upk: this._wasUpKeyPressed,
|
|
dok: this._wasDownKeyPressed,
|
|
juk: this._wasJumpKeyPressed,
|
|
rpk: this._wasReleasePlatformKeyPressed,
|
|
rlk: this._wasReleaseLadderKeyPressed,
|
|
jkhsjs: this._jumpKeyHeldSinceJumpStart,
|
|
sn: this._state.toString(),
|
|
ssd: this._state.getNetworkSyncData(),
|
|
},
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(
|
|
networkSyncData: PlatformerObjectNetworkSyncData,
|
|
options: UpdateFromNetworkSyncDataOptions
|
|
) {
|
|
super.updateFromNetworkSyncData(networkSyncData, options);
|
|
|
|
const behaviorSpecificProps = networkSyncData.props;
|
|
|
|
switch (behaviorSpecificProps.sn) {
|
|
case 'Falling':
|
|
if (behaviorSpecificProps.sn !== this._state.toString()) {
|
|
this._setFalling();
|
|
}
|
|
this._falling.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
|
|
break;
|
|
case 'OnFloor':
|
|
// Let it handle automatically as we don't know which platform to land on.
|
|
// @ts-ignore - we assume it's OnFloorStateNetworkSyncData
|
|
this._onFloor.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
|
|
break;
|
|
case 'Jumping':
|
|
if (behaviorSpecificProps.sn !== this._state.toString()) {
|
|
this._setJumping();
|
|
}
|
|
// @ts-ignore - we assume it's JumpingStateNetworkSyncData
|
|
this._jumping.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
|
|
break;
|
|
case 'GrabbingPlatform':
|
|
// Let it handle automatically as we don't know which platform to grab.
|
|
this._grabbingPlatform.updateFromNetworkSyncData(
|
|
// @ts-ignore - we assume it's GrabbingPlatformStateNetworkSyncData
|
|
behaviorSpecificProps.ssd
|
|
);
|
|
break;
|
|
case 'OnLadder':
|
|
if (behaviorSpecificProps.sn !== this._state.toString()) {
|
|
this._setOnLadder();
|
|
}
|
|
this._onLadder.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
|
|
break;
|
|
default:
|
|
console.error(
|
|
'Unknown state name: ' + behaviorSpecificProps.sn + '.'
|
|
);
|
|
break;
|
|
}
|
|
|
|
if (behaviorSpecificProps.cs !== this._currentSpeed) {
|
|
this._currentSpeed = behaviorSpecificProps.cs;
|
|
}
|
|
if (behaviorSpecificProps.rdx !== this._requestedDeltaX) {
|
|
this._requestedDeltaX = behaviorSpecificProps.rdx;
|
|
}
|
|
if (behaviorSpecificProps.rdy !== this._requestedDeltaY) {
|
|
this._requestedDeltaY = behaviorSpecificProps.rdy;
|
|
}
|
|
if (behaviorSpecificProps.ldy !== this._lastDeltaY) {
|
|
this._lastDeltaY = behaviorSpecificProps.ldy;
|
|
}
|
|
if (behaviorSpecificProps.cfs !== this._currentFallSpeed) {
|
|
this._currentFallSpeed = behaviorSpecificProps.cfs;
|
|
}
|
|
if (behaviorSpecificProps.cj !== this._canJump) {
|
|
this._canJump = behaviorSpecificProps.cj;
|
|
}
|
|
if (behaviorSpecificProps.ldl !== this._lastDirectionIsLeft) {
|
|
this._lastDirectionIsLeft = behaviorSpecificProps.ldl;
|
|
}
|
|
if (behaviorSpecificProps.lek !== this._leftKey) {
|
|
this._leftKey = behaviorSpecificProps.lek;
|
|
}
|
|
if (behaviorSpecificProps.rik !== this._rightKey) {
|
|
this._rightKey = behaviorSpecificProps.rik;
|
|
}
|
|
if (behaviorSpecificProps.lak !== this._ladderKey) {
|
|
this._ladderKey = behaviorSpecificProps.lak;
|
|
}
|
|
if (behaviorSpecificProps.upk !== this._upKey) {
|
|
this._upKey = behaviorSpecificProps.upk;
|
|
}
|
|
if (behaviorSpecificProps.dok !== this._downKey) {
|
|
this._downKey = behaviorSpecificProps.dok;
|
|
}
|
|
if (behaviorSpecificProps.juk !== this._jumpKey) {
|
|
this._jumpKey = behaviorSpecificProps.juk;
|
|
}
|
|
if (behaviorSpecificProps.rpk !== this._releasePlatformKey) {
|
|
this._releasePlatformKey = behaviorSpecificProps.rpk;
|
|
}
|
|
if (behaviorSpecificProps.rlk !== this._releaseLadderKey) {
|
|
this._releaseLadderKey = behaviorSpecificProps.rlk;
|
|
}
|
|
if (behaviorSpecificProps.jkhsjs !== this._jumpKeyHeldSinceJumpStart) {
|
|
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
|
|
}
|
|
|
|
// Clear user inputs between frames only if requested.
|
|
this._clearInputsBetweenFrames = !!options.clearInputs;
|
|
// And we are not using the default controls.
|
|
this._ignoreDefaultControlsAsSyncedByNetwork = !options.keepControl;
|
|
}
|
|
|
|
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
|
|
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.canGrabWithoutMoving !==
|
|
newBehaviorData.canGrabWithoutMoving
|
|
) {
|
|
this._canGrabWithoutMoving = newBehaviorData.canGrabWithoutMoving;
|
|
}
|
|
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);
|
|
}
|
|
if (
|
|
oldBehaviorData.useLegacyTrajectory !==
|
|
newBehaviorData.useLegacyTrajectory
|
|
) {
|
|
this._useLegacyTrajectory = newBehaviorData.useLegacyTrajectory;
|
|
}
|
|
if (
|
|
oldBehaviorData.canGoDownFromJumpthru !==
|
|
newBehaviorData.canGoDownFromJumpthru
|
|
) {
|
|
this._canGoDownFromJumpthru = newBehaviorData.canGoDownFromJumpthru;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
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() / 1000;
|
|
|
|
//0.1) Get the player input:
|
|
this._requestedDeltaX = 0;
|
|
this._requestedDeltaY = 0;
|
|
|
|
const inputManager = instanceContainer.getGame().getInputManager();
|
|
this._leftKey ||
|
|
(this._leftKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(LEFTKEY));
|
|
this._rightKey ||
|
|
(this._rightKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(RIGHTKEY));
|
|
|
|
this._jumpKey ||
|
|
(this._jumpKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
(inputManager.isKeyPressed(LSHIFTKEY) ||
|
|
inputManager.isKeyPressed(RSHIFTKEY) ||
|
|
inputManager.isKeyPressed(SPACEKEY)));
|
|
// Check if the jump key is continuously held since
|
|
// the beginning of the jump.
|
|
if (!this._jumpKey) {
|
|
this._jumpKeyHeldSinceJumpStart = false;
|
|
}
|
|
|
|
this._ladderKey ||
|
|
(this._ladderKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(UPKEY));
|
|
|
|
this._upKey ||
|
|
(this._upKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(UPKEY));
|
|
this._downKey ||
|
|
(this._downKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(DOWNKEY));
|
|
|
|
this._releasePlatformKey ||
|
|
(this._releasePlatformKey =
|
|
!this.shouldIgnoreDefaultControls() &&
|
|
inputManager.isKeyPressed(DOWNKEY));
|
|
|
|
this._requestedDeltaX += this._updateSpeed(timeDelta);
|
|
|
|
if (this._leftKey !== this._rightKey) {
|
|
this._lastDirectionIsLeft = this._leftKey;
|
|
}
|
|
|
|
//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:
|
|
const beforeMovingXState = this._state;
|
|
this._state.checkTransitionBeforeX();
|
|
this._state.beforeMovingX();
|
|
|
|
//Ensure the object is not stuck
|
|
const hasPopOutOfPlatform = this._separateFromPlatforms(
|
|
this._potentialCollidingObjects,
|
|
true
|
|
);
|
|
if (hasPopOutOfPlatform && !this._jumpKey) {
|
|
// TODO This is probably unnecessary because `_canJump` is already set
|
|
// to true when entering the `OnFloor` state.
|
|
// This is wrongly allowing double jumps when characters are flipped
|
|
// with an offset center.
|
|
|
|
//After being unstuck, the object must be able to jump again.
|
|
this._canJump = true;
|
|
}
|
|
|
|
const oldX = object.getX();
|
|
this._moveX();
|
|
const mayCollideWall = object.getX() !== oldX + this._requestedDeltaX;
|
|
|
|
//2) Y axis:
|
|
const beforeMovingYState = this._state;
|
|
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:
|
|
const beforeLastTransitionYState = this._state;
|
|
//TODO what about a moving platforms, remove this condition to do the same as for grabbing?
|
|
if (this._state !== this._onLadder) {
|
|
this._checkTransitionOnFloorOrFalling();
|
|
}
|
|
|
|
if (
|
|
// When the character is against a wall and the player hold left or
|
|
// right, the speed shouldn't stack because starting at full speed when
|
|
// jumping over the wall would look strange.
|
|
mayCollideWall &&
|
|
// Whereas, when the state has change, the collision is probably a
|
|
// landing or a collision from the floor when stating to jump. The
|
|
// speed must not be lost in these cases.
|
|
this._state === beforeMovingXState &&
|
|
this._state === beforeMovingYState &&
|
|
this._state === beforeLastTransitionYState &&
|
|
// When the character is on the floor, it will try to walk on the
|
|
// obstacles and already stop if necessary.
|
|
this._state !== this._onFloor
|
|
) {
|
|
this._currentSpeed = 0;
|
|
}
|
|
|
|
this._wasLeftKeyPressed = this._leftKey;
|
|
this._wasRightKeyPressed = this._rightKey;
|
|
this._wasLadderKeyPressed = this._ladderKey;
|
|
this._wasUpKeyPressed = this._upKey;
|
|
this._wasDownKeyPressed = this._downKey;
|
|
this._wasJumpKeyPressed = this._jumpKey;
|
|
this._wasReleasePlatformKeyPressed = this._releasePlatformKey;
|
|
this._wasReleaseLadderKeyPressed = this._releaseLadderKey;
|
|
|
|
//4) Do not forget to reset pressed keys
|
|
if (this._clearInputsBetweenFrames) {
|
|
// Reset the keys only if the inputs are not supposed to survive between frames.
|
|
// (Most of the time, except if this object is synchronized by an external source)
|
|
this._leftKey = false;
|
|
this._rightKey = false;
|
|
this._ladderKey = false;
|
|
this._upKey = false;
|
|
this._downKey = false;
|
|
this._jumpKey = false;
|
|
this._releasePlatformKey = false;
|
|
this._releaseLadderKey = false;
|
|
}
|
|
|
|
//5) Track the movement
|
|
this._hasReallyMoved =
|
|
Math.abs(object.getX() - oldX) >
|
|
PlatformerObjectRuntimeBehavior.epsilon ||
|
|
Math.abs(object.getY() - oldY) >
|
|
PlatformerObjectRuntimeBehavior.epsilon;
|
|
this._hasMovedAtLeastOnePixel =
|
|
Math.abs(object.getX() - oldX) >= 1 ||
|
|
Math.abs(object.getY() - oldY) >= 1;
|
|
this._lastDeltaY = object.getY() - oldY;
|
|
}
|
|
|
|
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}
|
|
|
|
private _updateSpeed(timeDelta: float): float {
|
|
const previousSpeed = this._currentSpeed;
|
|
// Change the speed according to the player's input.
|
|
// TODO Give priority to the last key for faster reaction time.
|
|
if (this._leftKey !== this._rightKey) {
|
|
if (this._leftKey) {
|
|
if (this._currentSpeed <= 0) {
|
|
this._currentSpeed -= this._acceleration * timeDelta;
|
|
} else {
|
|
// Turn back at least as fast as it would stop.
|
|
this._currentSpeed -=
|
|
Math.max(this._acceleration, this._deceleration) * timeDelta;
|
|
}
|
|
} else if (this._rightKey) {
|
|
if (this._currentSpeed >= 0) {
|
|
this._currentSpeed += this._acceleration * timeDelta;
|
|
} else {
|
|
this._currentSpeed +=
|
|
Math.max(this._acceleration, this._deceleration) * 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;
|
|
}
|
|
// Use Verlet integration.
|
|
return ((this._currentSpeed + previousSpeed) * timeDelta) / 2;
|
|
}
|
|
|
|
/**
|
|
* Also see {@link ./README.md}
|
|
*/
|
|
private _moveX() {
|
|
const object = this.owner;
|
|
//Move the object on x axis.
|
|
const oldX = object.getX();
|
|
if (this._requestedDeltaX !== 0) {
|
|
let 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,
|
|
this._onFloor.getFloorPolygon()
|
|
)
|
|
) {
|
|
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 (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)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _moveY() {
|
|
const object = this.owner;
|
|
//Move the object on Y axis
|
|
if (this._requestedDeltaY !== 0) {
|
|
if (this._requestedDeltaY > 0) {
|
|
// Use the same method as for following the floor.
|
|
// This is to be consistent on all floor collision.
|
|
// The object will land right on floor.
|
|
|
|
const { highestGroundPlatform } = this._findHighestFloorAndMoveOnTop(
|
|
this._potentialCollidingObjects,
|
|
0,
|
|
this._requestedDeltaY
|
|
);
|
|
if (!highestGroundPlatform) {
|
|
object.setY(object.getY() + this._requestedDeltaY);
|
|
}
|
|
} else {
|
|
// The same logic could be applied going up one day.
|
|
let oldY = object.getY();
|
|
object.setY(object.getY() + this._requestedDeltaY);
|
|
|
|
// Stop when colliding with an obstacle.
|
|
while (
|
|
// Jumpthru == obstacle <=> Never when going up.
|
|
(this._requestedDeltaY < 0 &&
|
|
this._isCollidingWithOneOf(
|
|
this._potentialCollidingObjects,
|
|
null,
|
|
/*excludeJumpThrus=*/
|
|
true
|
|
)) ||
|
|
// Jumpthru == obstacle <=> Only if not already overlapped when going down.
|
|
(this._requestedDeltaY > 0 &&
|
|
this._isCollidingWithOneOfExcluding(
|
|
this._potentialCollidingObjects,
|
|
this._overlappedJumpThru
|
|
))
|
|
) {
|
|
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();
|
|
const from = this._state;
|
|
this._state = this._falling;
|
|
this._falling.enter(from);
|
|
}
|
|
|
|
_setOnFloor(
|
|
collidingPlatform: gdjs.PlatformRuntimeBehavior,
|
|
floorPolygon: gdjs.Polygon
|
|
) {
|
|
this._state.leave();
|
|
this._state = this._onFloor;
|
|
this._onFloor.enter(collidingPlatform, floorPolygon);
|
|
}
|
|
|
|
private _setJumping() {
|
|
this._state.leave();
|
|
const from = this._state;
|
|
this._state = this._jumping;
|
|
this._jumping.enter(from);
|
|
}
|
|
|
|
private _setGrabbingPlatform(
|
|
grabbedPlatform: gdjs.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._jumpKeyHeldSinceJumpStart || this._useRepeatedJump)
|
|
) {
|
|
this._setJumping();
|
|
}
|
|
}
|
|
|
|
_checkGrabPlatform() {
|
|
const object = this.owner;
|
|
|
|
let oldX = object.getX();
|
|
object.setX(
|
|
object.getX() +
|
|
(this._requestedDeltaX < 0 ||
|
|
(this._requestedDeltaX === 0 && this._lastDirectionIsLeft)
|
|
? -this._xGrabTolerance
|
|
: this._xGrabTolerance)
|
|
);
|
|
const collidingPlatforms: gdjs.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;
|
|
const oldY = object.getY();
|
|
// 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 for this extra check).
|
|
const canLand = this._requestedDeltaY >= 0;
|
|
|
|
// The interval could be smaller.
|
|
// It's just for rounding errors.
|
|
const { highestGroundPlatform, highestGroundPolygon } =
|
|
this._findHighestFloorAndMoveOnTop(
|
|
this._potentialCollidingObjects,
|
|
-1,
|
|
1
|
|
);
|
|
// don't fall if GrabbingPlatform or OnLadder
|
|
if (this._state === this._onFloor) {
|
|
if (!highestGroundPlatform || !highestGroundPolygon) {
|
|
this._setFalling();
|
|
} else if (
|
|
highestGroundPlatform === this._onFloor.getFloorPlatform() &&
|
|
highestGroundPolygon === this._onFloor.getFloorPolygon()
|
|
) {
|
|
this._onFloor.updateFloorPosition();
|
|
} else {
|
|
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
|
}
|
|
} else if (highestGroundPlatform && highestGroundPolygon && canLand) {
|
|
this._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
|
} else {
|
|
// The object can't land.
|
|
object.setY(oldY);
|
|
}
|
|
}
|
|
|
|
_fall(timeDelta: float) {
|
|
const previousFallSpeed = this._currentFallSpeed;
|
|
this._currentFallSpeed += this._gravity * timeDelta;
|
|
if (this._currentFallSpeed > this._maxFallingSpeed) {
|
|
this._currentFallSpeed = this._maxFallingSpeed;
|
|
}
|
|
if (this._useLegacyTrajectory) {
|
|
this._requestedDeltaY += this._currentFallSpeed * timeDelta;
|
|
} else {
|
|
// Use Verlet integration.
|
|
this._requestedDeltaY +=
|
|
((this._currentFallSpeed + previousFallSpeed) / 2) * 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();
|
|
// This must be inclusive for at least one position.
|
|
// Otherwise, if the character is at the exact position,
|
|
// it could not be able to grab the platform at any frame.
|
|
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.
|
|
* @returns true if the object was moved
|
|
*/
|
|
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.
|
|
* @returns true if the object collides any platform
|
|
*/
|
|
_isCollidingWithOneOf(
|
|
candidates: gdjs.PlatformRuntimeBehavior[],
|
|
ignoredPlatformId?: number | null,
|
|
excludeJumpThrus?: boolean,
|
|
ignoredPolygon?: gdjs.Polygon | null
|
|
) {
|
|
excludeJumpThrus = !!excludeJumpThrus;
|
|
for (let i = 0; i < candidates.length; ++i) {
|
|
const platform = candidates[i];
|
|
const isPlatformIgnored = platform.owner.id === ignoredPlatformId;
|
|
if (isPlatformIgnored && !ignoredPolygon) {
|
|
continue;
|
|
}
|
|
if (
|
|
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
excludeJumpThrus &&
|
|
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.JUMPTHRU
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
gdjs.RuntimeObject.collisionTest(
|
|
platform.owner,
|
|
this.owner,
|
|
this._ignoreTouchingEdges,
|
|
isPlatformIgnored ? ignoredPolygon : null
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find the highest floor reachable and move the owner on top of it.
|
|
*
|
|
* Also see {@link ./README.md}
|
|
*
|
|
* @param candidates The platform to be tested for collision
|
|
* @param upwardDeltaY The owner won't move upward more than this value.
|
|
* @param downwardDeltaY The owner won't move downward more than this value.
|
|
* @returns the platform where to walk or if an obstacle was found
|
|
*/
|
|
_findHighestFloorAndMoveOnTop(
|
|
candidates: gdjs.PlatformRuntimeBehavior[],
|
|
upwardDeltaY: float,
|
|
downwardDeltaY: float
|
|
): PlatformSearchResult {
|
|
const context = FollowConstraintContext.instance;
|
|
context.initializeBeforeSearch(this, upwardDeltaY, downwardDeltaY);
|
|
|
|
let totalHighestY = Number.MAX_VALUE;
|
|
let highestGroundPlatform: gdjs.PlatformRuntimeBehavior | null = null;
|
|
let highestGroundPolygon: gdjs.Polygon | null = null;
|
|
let isCollidingAnyPlatform = false;
|
|
for (const platform of candidates) {
|
|
if (
|
|
platform.getPlatformType() === gdjs.PlatformRuntimeBehavior.LADDER ||
|
|
// Jump through platforms are obstacles only when the character comes from the top.
|
|
(platform.getPlatformType() ===
|
|
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
|
// When following the floor, jumpthrus that are higher than the character are ignored.
|
|
// If we only look above the character bottom, every jumpthrus can be discarded
|
|
// without doing any collision check.
|
|
((this._state === this._onFloor &&
|
|
platform !== this._onFloor.getFloorPlatform() &&
|
|
downwardDeltaY < 0) ||
|
|
// When trying to land on a platform, exclude jumpthrus that were already overlapped.
|
|
(this._state !== this._onFloor &&
|
|
this._isIn(this._overlappedJumpThru, platform.owner.id))))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const previousAllowedMinDeltaY = context.allowedMinDeltaY;
|
|
const previousAllowedMaxDeltaY = context.allowedMaxDeltaY;
|
|
this._findPlatformHighestRelativeYUnderObject(platform, context);
|
|
let highestRelativeY = context.getFloorDeltaY();
|
|
if (
|
|
platform.getPlatformType() ===
|
|
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
|
// When following the floor, ignore jumpthrus that are higher than the character bottom.
|
|
((this._state === this._onFloor &&
|
|
platform !== this._onFloor.getFloorPlatform() &&
|
|
highestRelativeY < 0) ||
|
|
// A jumpthrus should never constrain a character to go below.
|
|
// Jumpthrus are considered as obstacles at the 1st frame they are overlapping the character
|
|
// because it allows it to land on them, but they shouldn't push on its head.
|
|
context.allowedMinDeltaY !== previousAllowedMinDeltaY)
|
|
) {
|
|
// Don't follow jumpthrus that are higher than the character bottom.
|
|
// Revert side effect on the search context.
|
|
context.revertTo(previousAllowedMinDeltaY, previousAllowedMaxDeltaY);
|
|
continue;
|
|
}
|
|
if (context.isCollidingAnyPlatform()) {
|
|
isCollidingAnyPlatform = true;
|
|
}
|
|
if (context.floorIsTooHigh()) {
|
|
// One platform is colliding the character
|
|
// and is too high for the character to walk on.
|
|
// This will still be an obstacle even if there
|
|
// are other platforms that fit the requirements.
|
|
highestGroundPlatform = null;
|
|
highestGroundPolygon = null;
|
|
break;
|
|
}
|
|
|
|
if (
|
|
context.isCollidingAnyPlatform() &&
|
|
highestRelativeY < totalHighestY
|
|
) {
|
|
totalHighestY = highestRelativeY;
|
|
highestGroundPlatform = platform;
|
|
highestGroundPolygon = context.highestFloorPolygon;
|
|
}
|
|
}
|
|
if (highestGroundPlatform) {
|
|
const object = this.owner;
|
|
object.setY(object.getY() + totalHighestY);
|
|
}
|
|
const returnValue =
|
|
gdjs.PlatformerObjectRuntimeBehavior._platformSearchResult;
|
|
returnValue.highestGroundPlatform = highestGroundPlatform;
|
|
returnValue.highestGroundPolygon = highestGroundPolygon;
|
|
returnValue.isCollidingAnyPlatform = isCollidingAnyPlatform;
|
|
return returnValue;
|
|
}
|
|
|
|
/**
|
|
* Find the highest Y relative to the owner bottom of the floor reachable by the owner.
|
|
* @param platform The platform to be tested for collision.
|
|
* @param upwardDeltaY The owner won't move upward more than this value.
|
|
* @param downwardDeltaY The owner won't move downward more than this value.
|
|
* @return the search context
|
|
*/
|
|
private _findPlatformHighestRelativeYUnderObject(
|
|
platform: gdjs.PlatformRuntimeBehavior,
|
|
context: FollowConstraintContext
|
|
): FollowConstraintContext {
|
|
const platformObject = platform.owner;
|
|
const platformAABB = platformObject.getAABB();
|
|
if (
|
|
platformAABB.max[0] <= context.ownerMinX ||
|
|
platformAABB.min[0] >= context.ownerMaxX ||
|
|
platformAABB.max[1] <= context.headMinY ||
|
|
platformAABB.min[1] > context.floorMaxY
|
|
) {
|
|
// No collision
|
|
return context;
|
|
}
|
|
|
|
for (const hitbox of platformObject.getHitBoxesAround(
|
|
context.ownerMinX,
|
|
context.headMinY,
|
|
context.ownerMaxX,
|
|
context.floorMaxY
|
|
)) {
|
|
if (hitbox.vertices.length < 3) {
|
|
continue;
|
|
}
|
|
|
|
// Edges over the character head might not result to a collision,
|
|
// but if there is also an edge under its head then there is a collision.
|
|
// The platform hitbox could be in several parts.
|
|
// So, the object could walk on one part
|
|
// and have another part over its head.
|
|
// This is why flags are reset between each hitbox.
|
|
context.initializeBeforeHitboxCheck();
|
|
|
|
let previousVertex = hitbox.vertices[hitbox.vertices.length - 2];
|
|
let vertex = hitbox.vertices[hitbox.vertices.length - 1];
|
|
for (const nextVertex of hitbox.vertices) {
|
|
// When the character is side by side to a wall,
|
|
// no collision should be detected.
|
|
// Indeed, it only shares an edge so the intersection has no area.
|
|
// But, the character can share a vertex X with a platform
|
|
// when one of them is encompassing the other.
|
|
// This is why the edge direction is checked in this case.
|
|
if (
|
|
// The vertex is strictly into the interval...
|
|
(context.ownerMinX < vertex[0] && vertex[0] < context.ownerMaxX) ||
|
|
// ...or is on a bound but at least one of its edges is from the inside.
|
|
// Note: this needs strict convex hitbox to work.
|
|
(vertex[0] === context.ownerMinX &&
|
|
(previousVertex[0] > vertex[0] || nextVertex[0] > vertex[0])) ||
|
|
(vertex[0] === context.ownerMaxX &&
|
|
(previousVertex[0] < vertex[0] || nextVertex[0] < vertex[0]))
|
|
) {
|
|
context.addPointConstraint(vertex[1], hitbox);
|
|
}
|
|
|
|
const deltaX = vertex[0] - previousVertex[0];
|
|
// Vertical edges doesn't matter
|
|
if (deltaX !== 0) {
|
|
// Check intersection on the left side of owner
|
|
if (
|
|
(vertex[0] < context.ownerMinX &&
|
|
context.ownerMinX < previousVertex[0]) ||
|
|
(previousVertex[0] < context.ownerMinX &&
|
|
context.ownerMinX < vertex[0])
|
|
) {
|
|
const deltaY = vertex[1] - previousVertex[1];
|
|
const intersectionY =
|
|
previousVertex[1] +
|
|
((context.ownerMinX - previousVertex[0]) * deltaY) / deltaX;
|
|
|
|
context.addPointConstraint(intersectionY, hitbox);
|
|
}
|
|
// Check intersection on the right side of owner
|
|
if (
|
|
(vertex[0] < context.ownerMaxX &&
|
|
context.ownerMaxX < previousVertex[0]) ||
|
|
(previousVertex[0] < context.ownerMaxX &&
|
|
context.ownerMaxX < vertex[0])
|
|
) {
|
|
const deltaY = vertex[1] - previousVertex[1];
|
|
const intersectionY =
|
|
previousVertex[1] +
|
|
((context.ownerMaxX - previousVertex[0]) * deltaY) / deltaX;
|
|
|
|
context.addPointConstraint(intersectionY, hitbox);
|
|
}
|
|
}
|
|
if (context.floorIsTooHigh()) {
|
|
// The character can't follow the platforms.
|
|
// No need to continue the search.
|
|
return context;
|
|
}
|
|
previousVertex = vertex;
|
|
vertex = nextVertex;
|
|
}
|
|
}
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* 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 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) {
|
|
const object = this.owner;
|
|
|
|
this._manager.getAllPlatformsAround(
|
|
object,
|
|
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 === object) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**.
|
|
* @param input The control to be tested [Left,Right,Up,Down,Ladder,Jump,Release,Release Ladder].
|
|
* @returns true if the key was used since the last `doStepPreEvents` call.
|
|
*/
|
|
isUsingControl(input: string): boolean {
|
|
if (input === 'Left') {
|
|
return this._wasLeftKeyPressed;
|
|
}
|
|
if (input === 'Right') {
|
|
return this._wasRightKeyPressed;
|
|
}
|
|
if (input === 'Up') {
|
|
return this._wasUpKeyPressed;
|
|
}
|
|
if (input === 'Down') {
|
|
return this._wasDownKeyPressed;
|
|
}
|
|
if (input === 'Ladder') {
|
|
return this._wasLadderKeyPressed;
|
|
}
|
|
if (input === 'Jump') {
|
|
return this._wasJumpKeyPressed;
|
|
}
|
|
if (input === 'Release') {
|
|
return this._wasReleasePlatformKeyPressed;
|
|
}
|
|
if (input === 'Release Ladder') {
|
|
return this._wasReleaseLadderKeyPressed;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the gravity of the Platformer Object.
|
|
* @returns The current gravity.
|
|
*/
|
|
getGravity(): float {
|
|
return this._gravity;
|
|
}
|
|
|
|
/**
|
|
* Get maximum angle of a slope for the Platformer Object to run on it as a floor.
|
|
* @returns the slope maximum angle, in degrees.
|
|
*/
|
|
getSlopeMaxAngle(): float {
|
|
return this._slopeMaxAngle;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Set the current speed of the Platformer Object.
|
|
* @param currentSpeed The current speed.
|
|
*/
|
|
setCurrentSpeed(currentSpeed: float): void {
|
|
this._currentSpeed = gdjs.evtTools.common.clamp(
|
|
currentSpeed,
|
|
-this._maxSpeed,
|
|
this._maxSpeed
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param tryToPreserveAirSpeed If true and if jumping, tune the current jump speed to preserve the overall speed in the air.
|
|
*/
|
|
setMaxFallingSpeed(
|
|
maxFallingSpeed: float,
|
|
tryToPreserveAirSpeed: boolean = false
|
|
): void {
|
|
if (tryToPreserveAirSpeed && this._state === this._jumping) {
|
|
// If the falling speed is too high compared to the new max falling speed,
|
|
// reduce it and adapt the jump speed to preserve the overall vertical speed.
|
|
const fallingSpeedOverflow = this._currentFallSpeed - maxFallingSpeed;
|
|
if (fallingSpeedOverflow > 0) {
|
|
this._currentFallSpeed -= fallingSpeedOverflow;
|
|
this._jumping.setCurrentJumpSpeed(
|
|
Math.max(
|
|
0,
|
|
this._jumping.getCurrentJumpSpeed() - fallingSpeedOverflow
|
|
)
|
|
);
|
|
}
|
|
}
|
|
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 jumpSustainTime 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
|
|
);
|
|
}
|
|
|
|
// Avoid a `_slopeClimbingFactor` set to exactly 0.
|
|
// Otherwise, this can lead the floor finding functions to consider
|
|
// a floor to be "too high" to reach, even if the object is very slightly
|
|
// inside it, which can happen because of rounding errors.
|
|
// See "Floating-point error mitigations" tests.
|
|
if (this._slopeClimbingFactor < 1 / 1024) {
|
|
this._slopeClimbingFactor = 1 / 1024;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort the current jump.
|
|
*
|
|
* When the character is not in the jumping state this method has no effect.
|
|
*/
|
|
abortJump(): void {
|
|
if (this._state === this._jumping) {
|
|
this._currentFallSpeed = 0;
|
|
this._setFalling();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the current fall speed.
|
|
*
|
|
* When the character is not in the falling state this method has no effect.
|
|
*/
|
|
setCurrentFallSpeed(currentFallSpeed: float) {
|
|
if (this._state === this._falling) {
|
|
this._currentFallSpeed = gdjs.evtTools.common.clamp(
|
|
currentFallSpeed,
|
|
0,
|
|
this._maxFallingSpeed
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Check if the default controls of the Platformer Object are ignored.
|
|
* @returns true if the default controls are ignored.
|
|
*/
|
|
shouldIgnoreDefaultControls() {
|
|
return (
|
|
this._ignoreDefaultControls ||
|
|
this._ignoreDefaultControlsAsSyncedByNetwork
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 a floor.
|
|
* @returns Returns true if on a floor and false if not.
|
|
*/
|
|
isOnFloor(): boolean {
|
|
return this._state === this._onFloor;
|
|
}
|
|
|
|
/**
|
|
* Check if the Platformer Object is on the given object.
|
|
* @returns Returns true if on the object and false if not.
|
|
*/
|
|
isOnFloorObject(object: gdjs.RuntimeObject): boolean {
|
|
if (this.isOnFloor()) {
|
|
const floorPlatform = this._onFloor.getFloorPlatform();
|
|
return !!floorPlatform && floorPlatform.owner.id === object.id;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* When walking or climbing on a ladder,
|
|
* a speed of less than one pixel per frame won't be detected.
|
|
*
|
|
* @returns Returns true if it is moving and false if not.
|
|
* @deprecated use isMovingEvenALittle instead
|
|
*/
|
|
isMoving(): boolean {
|
|
return (
|
|
(this._hasMovedAtLeastOnePixel &&
|
|
(this._currentSpeed !== 0 || this._state === this._onLadder)) ||
|
|
this._jumping.getCurrentJumpSpeed() !== 0 ||
|
|
this._currentFallSpeed !== 0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the Platformer Object is moving.
|
|
* @returns Returns true if it is moving and false if not.
|
|
*/
|
|
isMovingEvenALittle(): 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;
|
|
|
|
getNetworkSyncData(): StateNetworkSyncData;
|
|
|
|
updateFromNetworkSyncData(syncData: StateNetworkSyncData): void;
|
|
}
|
|
|
|
/**
|
|
* The object is on the floor standing or walking.
|
|
*
|
|
* Also see {@link ./README.md}
|
|
*/
|
|
class OnFloor implements State {
|
|
private _behavior: PlatformerObjectRuntimeBehavior;
|
|
private _floorPlatform: gdjs.PlatformRuntimeBehavior | null = null;
|
|
private _floorPolygon: gdjs.Polygon | null = null;
|
|
private _floorLastX: float = 0;
|
|
private _floorLastY: float = 0;
|
|
_oldHeight: float = 0;
|
|
|
|
constructor(behavior: PlatformerObjectRuntimeBehavior) {
|
|
this._behavior = behavior;
|
|
}
|
|
|
|
getFloorPlatform() {
|
|
return this._floorPlatform;
|
|
}
|
|
|
|
getFloorPolygon() {
|
|
return this._floorPolygon;
|
|
}
|
|
|
|
enter(
|
|
floorPlatform: gdjs.PlatformRuntimeBehavior,
|
|
floorPolygon: gdjs.Polygon
|
|
) {
|
|
this._floorPlatform = floorPlatform;
|
|
this._floorPolygon = floorPolygon;
|
|
this.updateFloorPosition();
|
|
this._behavior._canJump = true;
|
|
this._behavior._currentFallSpeed = 0;
|
|
}
|
|
|
|
leave() {
|
|
this._floorPlatform = null;
|
|
this._floorPolygon = 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()) {
|
|
// TODO This should probably be done after the events because
|
|
// the character stays at the wrong place during 1 frame.
|
|
const deltaY =
|
|
((this._oldHeight - object.getHeight()) *
|
|
(object.getHeight() + object.getDrawableY() - object.getY())) /
|
|
object.getHeight();
|
|
object.setY(object.getY() + deltaY);
|
|
}
|
|
// Directly follow the floor movement on the Y axis by moving the character.
|
|
// For the X axis, we follow the floor movement using `_requestedDeltaX`
|
|
// (see `beforeMovingX`).
|
|
// We don't use `_requestedDeltaY` to follow the floor on the Y axis
|
|
// to avoid a transition loop with the Falling state.
|
|
// Indeed, if we used it, then:
|
|
// - going down, the character could no longer be on a platform and start falling.
|
|
// - going up, the character will already be pushed on top on the platform
|
|
// by `beforeMovingY` that handle slopes or by `_separateFromPlatforms` that
|
|
// avoid characters being stuck. So using `_requestedDeltaY`, the character
|
|
// would be going too much higher and fall at the next frame.
|
|
//
|
|
// We could make the character follow a platform moving up
|
|
// at a greater speed as it's coherent from a physics point of view.
|
|
// But, when the character is put on top of the platform to follow it up,
|
|
// the platform AABB may not be updated in RBush yet
|
|
// and the platform can go out of the spatial search rectangle
|
|
// even though they are next to each other, which means
|
|
// that the character will fall.
|
|
const deltaY = this._floorPlatform!.owner.getY() - this._floorLastY;
|
|
if (
|
|
deltaY !== 0 &&
|
|
Math.abs(deltaY) <=
|
|
Math.abs(this._behavior._maxFallingSpeed * timeDelta)
|
|
) {
|
|
object.setY(object.getY() + deltaY);
|
|
}
|
|
}
|
|
|
|
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();
|
|
} else if (
|
|
this._behavior._downKey &&
|
|
this._floorPlatform!._platformType ===
|
|
gdjs.PlatformRuntimeBehavior.JUMPTHRU &&
|
|
behavior._canGoDownFromJumpthru
|
|
) {
|
|
behavior._overlappedJumpThru.push(this._floorPlatform!);
|
|
behavior._setFalling();
|
|
}
|
|
|
|
// It was originally in checkTransitionBeforeY.
|
|
// The character is ignoring the floor when moving on X to be able to
|
|
// follow up a slope when moving Y (it enter inside it).
|
|
// When the current floor and the wall the character is facing is part of
|
|
// the same instance, the wall is also ignored when moving on X, but the
|
|
// wall is too high to follow and it is seen as colliding an obstacle
|
|
// from behind.
|
|
// Moving against a wall before jumping in this configuration was making
|
|
// jumps being aborted.
|
|
behavior._checkTransitionJumping();
|
|
}
|
|
|
|
beforeMovingX() {
|
|
const behavior = this._behavior;
|
|
// Shift the object according to the floor movement.
|
|
behavior._requestedDeltaX +=
|
|
this._floorPlatform!.owner.getX() - this._floorLastX;
|
|
// See `beforeUpdatingObstacles` for the logic for the Y axis.
|
|
}
|
|
|
|
checkTransitionBeforeY(timeDelta: float) {
|
|
const behavior = this._behavior;
|
|
// Go on a ladder
|
|
behavior._checkTransitionOnLadder();
|
|
}
|
|
|
|
beforeMovingY(timeDelta: float, oldX: float) {
|
|
const behavior = this._behavior;
|
|
const object = behavior.owner;
|
|
|
|
if (object.getX() === oldX + behavior._requestedDeltaX) {
|
|
// The character didn't encounter any obstacles on the X axis.
|
|
// It follows the floor.
|
|
|
|
// In theory, this max delta on the Y axis could be 0. In practice,
|
|
// `behavior._slopeClimbingFactor` has a lower bound of 1 / 1024.
|
|
// This avoids this max delta Y to be strictly 0, which would then risk
|
|
// considering a floor "too high", even if the object is inside it because
|
|
// of a very small rounding error.
|
|
// See "Floating-point error mitigations" tests.
|
|
const deltaMaxY = Math.abs(
|
|
behavior._requestedDeltaX * behavior._slopeClimbingFactor
|
|
);
|
|
const {
|
|
highestGroundPlatform,
|
|
highestGroundPolygon,
|
|
isCollidingAnyPlatform,
|
|
} = behavior._findHighestFloorAndMoveOnTop(
|
|
behavior._potentialCollidingObjects,
|
|
-deltaMaxY,
|
|
deltaMaxY
|
|
);
|
|
if (
|
|
highestGroundPlatform &&
|
|
highestGroundPolygon &&
|
|
(highestGroundPlatform !== this._floorPlatform ||
|
|
highestGroundPolygon !== this._floorPolygon)
|
|
) {
|
|
behavior._setOnFloor(highestGroundPlatform, highestGroundPolygon);
|
|
}
|
|
if (highestGroundPlatform === null && isCollidingAnyPlatform) {
|
|
// Unable to follow the floor (too steep): go back to the original position.
|
|
behavior.owner.setX(oldX);
|
|
}
|
|
} else {
|
|
// The character encountered an obstacle on the X axis.
|
|
// Try to walk on it or stop before it.
|
|
|
|
// Try to follow the platform until the obstacle.
|
|
const {
|
|
highestGroundPlatform: highestGroundOnPlatform,
|
|
isCollidingAnyPlatform,
|
|
} = behavior._findHighestFloorAndMoveOnTop(
|
|
behavior._potentialCollidingObjects,
|
|
Math.min(
|
|
0,
|
|
-Math.abs(object.getX() - oldX) * behavior._slopeClimbingFactor
|
|
),
|
|
0
|
|
);
|
|
if (highestGroundOnPlatform === null && isCollidingAnyPlatform) {
|
|
// Unable to follow the floor (too steep): go back to the original position.
|
|
behavior.owner.setX(oldX);
|
|
} else {
|
|
const requestedDeltaX = behavior._requestedDeltaX;
|
|
// The current platform is climbed.
|
|
// Can the obstacle be climbed too from here?
|
|
// We do a look-up in 2 steps:
|
|
// 1. Try to move 1 pixel on X to climb the junction
|
|
// (because the obstacle detection is done 1 pixel by 1 pixel).
|
|
// 2. Try to follow the obstacle slope by at least 1 pixel on X axis
|
|
// (it can only be done after the junction because otherwise
|
|
// the slope angle would be a mean between the current platform and
|
|
// the obstacles).
|
|
//
|
|
// The 2nd step is done using a 1 pixel width at least, when remainingDeltaX
|
|
// is less than 2 pixels: this will be a "lookahead". This is to ensure
|
|
// the character doesn't start to climb a slope it actually can't.
|
|
const remainingDeltaX = requestedDeltaX - (object.getX() - oldX);
|
|
const beforeObstacleY = object.getY();
|
|
const beforeObstacleX = object.getX();
|
|
|
|
// 1. Try to move 1 pixel on the X axis to climb the junction.
|
|
object.setX(object.getX() + Math.sign(requestedDeltaX));
|
|
const { highestGroundPlatform: highestGroundAtJunction } =
|
|
behavior._findHighestFloorAndMoveOnTop(
|
|
behavior._potentialCollidingObjects,
|
|
// Look up from at least 1 pixel to bypass not perfectly aligned floors.
|
|
Math.min(-1, -1 * behavior._slopeClimbingFactor),
|
|
0
|
|
);
|
|
if (highestGroundAtJunction) {
|
|
// The obstacle 1st pixel can be climbed.
|
|
// Now that the character is on the obstacle,
|
|
// try to follow the slope for at least 1 pixel.
|
|
const deltaX =
|
|
Math.sign(requestedDeltaX) *
|
|
Math.max(
|
|
1,
|
|
// - 1, because the owner moved from 1 pixel at the junction.
|
|
Math.abs(remainingDeltaX) - 1
|
|
);
|
|
object.setX(object.getX() + deltaX);
|
|
const {
|
|
highestGroundPlatform: highestGroundOnObstacle,
|
|
highestGroundPolygon,
|
|
} = behavior._findHighestFloorAndMoveOnTop(
|
|
behavior._potentialCollidingObjects,
|
|
// Do an exact slope angle check.
|
|
-Math.abs(deltaX) * behavior._slopeClimbingFactor,
|
|
0
|
|
);
|
|
if (highestGroundOnObstacle && highestGroundPolygon) {
|
|
// The obstacle slope can be climbed.
|
|
if (Math.abs(remainingDeltaX) >= 2) {
|
|
behavior._setOnFloor(
|
|
highestGroundOnObstacle,
|
|
highestGroundPolygon
|
|
);
|
|
} else {
|
|
// We went too far in order to check that.
|
|
// Now, find the right position on the obstacles.
|
|
object.setPosition(oldX + requestedDeltaX, beforeObstacleY);
|
|
const { highestGroundPlatform: highestGroundOnObstacle } =
|
|
behavior._findHighestFloorAndMoveOnTop(
|
|
behavior._potentialCollidingObjects,
|
|
// requestedDeltaX can be small when the object start moving.
|
|
// So, look up from at least 1 pixel to bypass not perfectly aligned floors.
|
|
Math.min(
|
|
-1,
|
|
-Math.abs(remainingDeltaX) * behavior._slopeClimbingFactor
|
|
),
|
|
0
|
|
);
|
|
// Should always be true
|
|
if (highestGroundOnObstacle && highestGroundPolygon) {
|
|
behavior._setOnFloor(
|
|
highestGroundOnObstacle,
|
|
highestGroundPolygon
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Don't climb on the obstacle
|
|
// because the obstacle slope is too steep.
|
|
if (
|
|
Math.sign(beforeObstacleX - oldX) === Math.sign(requestedDeltaX)
|
|
) {
|
|
object.setPosition(beforeObstacleX, beforeObstacleY);
|
|
} else {
|
|
// Avoid to go backward
|
|
object.setPosition(oldX, beforeObstacleY);
|
|
}
|
|
behavior._currentSpeed = 0;
|
|
}
|
|
} else {
|
|
// Don't climb on the obstacle
|
|
// because the obstacle 1st pixel is more than 1 pixel high (or too steep).
|
|
if (
|
|
Math.sign(beforeObstacleX - oldX) === Math.sign(requestedDeltaX)
|
|
) {
|
|
object.setPosition(beforeObstacleX, beforeObstacleY);
|
|
} else {
|
|
// Avoid to go backward
|
|
object.setPosition(oldX, beforeObstacleY);
|
|
}
|
|
behavior._currentSpeed = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
getNetworkSyncData(): OnFloorStateNetworkSyncData {
|
|
return {
|
|
flx: this._floorLastX,
|
|
fly: this._floorLastY,
|
|
oh: this._oldHeight,
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(data: OnFloorStateNetworkSyncData) {
|
|
this._floorLastX = data.flx;
|
|
this._floorLastY = data.fly;
|
|
this._oldHeight = data.oh;
|
|
}
|
|
|
|
toString(): String {
|
|
return 'OnFloor';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The object is falling.
|
|
*/
|
|
class Falling implements State {
|
|
private _behavior: PlatformerObjectRuntimeBehavior;
|
|
|
|
constructor(behavior: PlatformerObjectRuntimeBehavior) {
|
|
this._behavior = behavior;
|
|
}
|
|
|
|
enter(from: State) {
|
|
// Only forbid jumping when starting to fall from a platform,
|
|
// not when falling during a jump. This is because the Jumping
|
|
// state has already set `_canJump` to false and we don't want to reset
|
|
// it again because it could have been set back to `true` to allow
|
|
// for an "air jump".
|
|
// Transition from Falling to Falling state should not happen,
|
|
// but don't change anything if this ever happen.
|
|
if (from !== this._behavior._jumping && from !== this) {
|
|
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._canGrabWithoutMoving)
|
|
) {
|
|
behavior._checkGrabPlatform();
|
|
}
|
|
}
|
|
|
|
beforeMovingY(timeDelta: float, oldX: float) {
|
|
//Fall
|
|
this._behavior._fall(timeDelta);
|
|
}
|
|
|
|
getNetworkSyncData(): FallingStateNetworkSyncData {
|
|
return {};
|
|
}
|
|
|
|
updateFromNetworkSyncData(data: FallingStateNetworkSyncData) {}
|
|
|
|
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 _jumpingFirstDelta: boolean = false;
|
|
|
|
constructor(behavior: PlatformerObjectRuntimeBehavior) {
|
|
this._behavior = behavior;
|
|
}
|
|
|
|
getCurrentJumpSpeed() {
|
|
return this._currentJumpSpeed;
|
|
}
|
|
|
|
setCurrentJumpSpeed(currentJumpSpeed: number) {
|
|
this._currentJumpSpeed = currentJumpSpeed;
|
|
}
|
|
|
|
enter(from: State) {
|
|
const behavior = this._behavior;
|
|
this._timeSinceCurrentJumpStart = 0;
|
|
behavior._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._canGrabWithoutMoving) &&
|
|
behavior._lastDeltaY >= 0
|
|
) {
|
|
behavior._checkGrabPlatform();
|
|
}
|
|
}
|
|
|
|
beforeMovingY(timeDelta: float, oldX: float) {
|
|
const behavior = this._behavior;
|
|
|
|
this._timeSinceCurrentJumpStart += timeDelta;
|
|
|
|
const previousJumpSpeed = this._currentJumpSpeed;
|
|
// Decrease jump speed after the (optional) jump sustain time is over.
|
|
const sustainJumpSpeed =
|
|
behavior._jumpKeyHeldSinceJumpStart &&
|
|
this._timeSinceCurrentJumpStart < behavior._jumpSustainTime;
|
|
if (!sustainJumpSpeed) {
|
|
this._currentJumpSpeed -= behavior._gravity * timeDelta;
|
|
}
|
|
|
|
if (this._behavior._useLegacyTrajectory) {
|
|
behavior._requestedDeltaY -= previousJumpSpeed * timeDelta;
|
|
|
|
// Fall
|
|
// The condition is a legacy thing.
|
|
// There is no actual reason not to fall at 1st frame.
|
|
// Before a refactoring, it used to not be this obvious.
|
|
if (!this._jumpingFirstDelta) {
|
|
behavior._fall(timeDelta);
|
|
}
|
|
} else {
|
|
// Use Verlet integration.
|
|
behavior._requestedDeltaY +=
|
|
((-previousJumpSpeed - this._currentJumpSpeed) / 2) * timeDelta;
|
|
|
|
// Fall
|
|
behavior._fall(timeDelta);
|
|
}
|
|
this._jumpingFirstDelta = false;
|
|
|
|
if (this._currentJumpSpeed < 0) {
|
|
behavior._setFalling();
|
|
}
|
|
}
|
|
|
|
getNetworkSyncData(): JumpingStateNetworkSyncData {
|
|
return {
|
|
cjs: this._currentJumpSpeed,
|
|
tscjs: this._timeSinceCurrentJumpStart,
|
|
jfd: this._jumpingFirstDelta,
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(data: JumpingStateNetworkSyncData) {
|
|
this._currentJumpSpeed = data.cjs;
|
|
this._timeSinceCurrentJumpStart = data.tscjs;
|
|
this._jumpingFirstDelta = data.jfd;
|
|
}
|
|
|
|
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: gdjs.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();
|
|
}
|
|
|
|
getNetworkSyncData(): GrabbingPlatformStateNetworkSyncData {
|
|
return {
|
|
gplx: this._grabbedPlatformLastX,
|
|
gply: this._grabbedPlatformLastY,
|
|
};
|
|
}
|
|
|
|
updateFromNetworkSyncData(data: GrabbingPlatformStateNetworkSyncData) {
|
|
this._grabbedPlatformLastX = data.gplx;
|
|
this._grabbedPlatformLastY = data.gply;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
getNetworkSyncData(): OnLadderStateNetworkSyncData {
|
|
return {};
|
|
}
|
|
|
|
updateFromNetworkSyncData(data: OnLadderStateNetworkSyncData) {}
|
|
|
|
toString(): String {
|
|
return 'OnLadder';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A context used to search for a floor.
|
|
*/
|
|
class FollowConstraintContext {
|
|
static readonly instance: FollowConstraintContext =
|
|
new FollowConstraintContext();
|
|
/**
|
|
* Character right side
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
ownerMinX: float = 0;
|
|
/**
|
|
* Character left side
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
ownerMaxX: float = 0;
|
|
/**
|
|
* The maximum top position the character top can go.
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
headMinY: float = 0;
|
|
/**
|
|
* Character top
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
ownerMinY: float = 0;
|
|
/**
|
|
* The maximum bottom position the character top can go.
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
headMaxY: float = 0;
|
|
/**
|
|
* The maximum top position the character bottom can go.
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
floorMinY: float = 0;
|
|
/**
|
|
* Character bottom
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
ownerMaxY: float = 0;
|
|
/**
|
|
* The maximum bottom position the character bottom can go.
|
|
*
|
|
* (constant to a search)
|
|
*/
|
|
floorMaxY: float = 0;
|
|
|
|
/**
|
|
* The minimum upward delta according to already checked platforms.
|
|
*
|
|
* (a result of the search)
|
|
*/
|
|
allowedMinDeltaY: float = 0;
|
|
/**
|
|
* The maximum downward delta according to already checked platforms.
|
|
*
|
|
* (a result of the search)
|
|
*/
|
|
allowedMaxDeltaY: float = 0;
|
|
|
|
/**
|
|
* True if any edge has been found over where the character top can go (downward).
|
|
*
|
|
* It allows to check for encompassing platforms.
|
|
*
|
|
* (local to one hitbox check)
|
|
*/
|
|
foundOverHead: boolean = false;
|
|
/**
|
|
* True if any edge has been found under where the character bottom can go (upward).
|
|
*
|
|
* It allows to check for encompassing platforms.
|
|
*
|
|
* (local to one hitbox check)
|
|
*/
|
|
foundUnderBottom: boolean = false;
|
|
|
|
highestFloorPolygon: gdjs.Polygon | null = null;
|
|
|
|
initializeBeforeSearch(
|
|
behavior: PlatformerObjectRuntimeBehavior,
|
|
upwardDeltaY: float,
|
|
downwardDeltaY: float
|
|
) {
|
|
let ownerMinX = Number.MAX_VALUE;
|
|
let ownerMaxX = -Number.MAX_VALUE;
|
|
let ownerMinY = Number.MAX_VALUE;
|
|
let ownerMaxY = -Number.MAX_VALUE;
|
|
for (const hitBox of behavior.owner.getHitBoxes()) {
|
|
for (const vertex of hitBox.vertices) {
|
|
ownerMinX = Math.min(ownerMinX, vertex[0]);
|
|
ownerMaxX = Math.max(ownerMaxX, vertex[0]);
|
|
ownerMinY = Math.min(ownerMinY, vertex[1]);
|
|
ownerMaxY = Math.max(ownerMaxY, vertex[1]);
|
|
}
|
|
}
|
|
|
|
this.ownerMinX = ownerMinX;
|
|
this.ownerMaxX = ownerMaxX;
|
|
this.headMinY = ownerMinY + upwardDeltaY;
|
|
this.ownerMinY = ownerMinY;
|
|
this.headMaxY = ownerMinY + downwardDeltaY;
|
|
this.floorMinY = ownerMaxY + upwardDeltaY;
|
|
this.ownerMaxY = ownerMaxY;
|
|
this.floorMaxY = ownerMaxY + downwardDeltaY;
|
|
|
|
this.allowedMinDeltaY = upwardDeltaY;
|
|
// Number.MAX_VALUE and not downwardDeltaY
|
|
// because it would means that a platform was found.
|
|
// see isCollidingAnyPlatform()
|
|
this.allowedMaxDeltaY = Number.MAX_VALUE;
|
|
}
|
|
|
|
initializeBeforeHitboxCheck() {
|
|
this.foundOverHead = false;
|
|
this.foundUnderBottom = false;
|
|
}
|
|
|
|
/**
|
|
* Revert the search variables to a given state.
|
|
*
|
|
* This is used to revert side effect of jumpthru check.
|
|
* @param previousAllowedMinDeltaY
|
|
* @param previousAllowedMaxDeltaY
|
|
*/
|
|
revertTo(previousAllowedMinDeltaY: float, previousAllowedMaxDeltaY: float) {
|
|
// Other members are either constants or local to an hitbox search.
|
|
this.allowedMinDeltaY = previousAllowedMinDeltaY;
|
|
this.allowedMaxDeltaY = previousAllowedMaxDeltaY;
|
|
}
|
|
|
|
setFloorIsTooHigh() {
|
|
this.allowedMinDeltaY = Number.MAX_VALUE;
|
|
this.allowedMaxDeltaY = -Number.MAX_VALUE;
|
|
}
|
|
|
|
floorIsTooHigh(): boolean {
|
|
// Return true when the 2 constraints are incompatible.
|
|
return this.allowedMinDeltaY > this.allowedMaxDeltaY;
|
|
}
|
|
|
|
isCollidingAnyPlatform(): boolean {
|
|
return this.ownerMaxY + this.allowedMaxDeltaY <= this.floorMaxY;
|
|
}
|
|
|
|
getFloorDeltaY(): float {
|
|
return this.allowedMaxDeltaY;
|
|
}
|
|
|
|
/**
|
|
* Check if the character can follow a given Y or move not to touch it
|
|
* and update the context with this new constraint.
|
|
* @param y
|
|
*/
|
|
addPointConstraint(y: float, sourcePolygon: gdjs.Polygon): void {
|
|
if (y < this.floorMinY) {
|
|
// The platform is too high to walk on...
|
|
if (y > this.headMaxY) {
|
|
// ...but not over the object.
|
|
this.setFloorIsTooHigh();
|
|
return;
|
|
}
|
|
// ...but over the object.
|
|
this.foundOverHead = true;
|
|
if (this.foundUnderBottom) {
|
|
// The current hitbox is below and above at the same time.
|
|
// As hitboxes are convex, the platform overlaps the character.
|
|
this.setFloorIsTooHigh();
|
|
return;
|
|
}
|
|
// When there is a platform on the top,
|
|
// the character is constraint on how high
|
|
// he can follow a floor.
|
|
this.allowedMinDeltaY = Math.max(
|
|
this.allowedMinDeltaY,
|
|
y - this.ownerMinY
|
|
);
|
|
} else {
|
|
// The platform can be walked on.
|
|
this.foundUnderBottom = true;
|
|
if (this.foundOverHead) {
|
|
// The current hitbox is below and above at the same time.
|
|
// As hitboxes are convex, the platform overlaps the character.
|
|
this.setFloorIsTooHigh();
|
|
return;
|
|
}
|
|
// Add the vertex to the constraints.
|
|
// When there is a platform on the bottom,
|
|
// the character is constraint on how low
|
|
// he can follow a floor.
|
|
this.allowedMaxDeltaY = Math.min(
|
|
this.allowedMaxDeltaY,
|
|
y - this.ownerMaxY
|
|
);
|
|
this.highestFloorPolygon = sourcePolygon;
|
|
}
|
|
}
|
|
}
|
|
|
|
gdjs.registerBehavior(
|
|
'PlatformBehavior::PlatformerObjectBehavior',
|
|
gdjs.PlatformerObjectRuntimeBehavior
|
|
);
|
|
}
|