/* 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; /** Overlapped jump-thru platforms, updated with `_updateOverlappedJumpThru`. */ private _overlappedJumpThru: Array; 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 ); }