if (typeof gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown !== "undefined") { gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.registeredGdjsCallbacks.forEach(callback => gdjs._unregisterCallback(callback) ); } gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown = {}; gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.idToCallbackMap = new Map(); gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.userFunc0xa43328 = function GDJSInlineCode(runtimeScene, eventsFunctionContext) { "use strict"; if (gdjs.__pixelPerfectExtension) { return; } const epsilon = 1 / (1 << 20); class PixelPerfectTopDownMovement { /** * @param {gdjs.RuntimeBehavior} pixelPerfectBehavior * @param {gdjs.TopDownMovementRuntimeBehavior} topDownBehavior */ constructor(pixelPerfectBehavior, topDownBehavior) { /** @type {gdjs.RuntimeBehavior} */ this.pixelPerfectBehavior = pixelPerfectBehavior; /** @type {gdjs.TopDownMovementRuntimeBehavior} */ this.topDownBehavior = topDownBehavior; topDownBehavior.registerHook(this); /** @type {number | null} */ this.targetX = null; /** @type {number | null} */ this.targetY = null; this.targetDirectionX = 0; this.targetDirectionY = 0; this.lastDirection = -1; this.isVelocityAdjusted = false; this.isVelocityAdjustedOnX = false; this.isVelocityAdjustedOnY = false; } /** * Return the direction to use instead of the direction given in * parameter. * @param {gdjs.TopDownMovementRuntimeBehavior.TopDownMovementHookContext} context * @return {number} */ overrideDirection(context) { let direction = context.getDirection(); if (!this.pixelPerfectBehavior.activated()) { return direction; } /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); const that = this.topDownBehavior; const object = that.owner; if (cellSize <= 0 || that._allowDiagonals) { return direction; } // Forbid to turn before being aligned on the grid. const timeDelta = object.getElapsedTime() / 1000; const deltaX = Math.abs(that._xVelocity * timeDelta); const deltaY = Math.abs(that._yVelocity * timeDelta); const isTryingToMoveOnX = direction === 4 || direction === 0; const isTryingToMoveOnY = direction === 6 || direction === 2; if (isTryingToMoveOnX) { if (that._yVelocity < 0) { if (Math.abs(this.ceilToCellY(object.y) - object.y) > deltaY) { direction = 6; } else { object.y = this.ceilToCellY(object.y); } } if (that._yVelocity > 0) { if (Math.abs(this.floorToCellY(object.y) - object.y) > deltaY) { direction = 2; } else { object.y = this.floorToCellY(object.y); } } } else if (isTryingToMoveOnY) { if (that._xVelocity < 0) { if (Math.abs(this.ceilToCellX(object.x) - object.x) > deltaX) { direction = 4; } else { object.x = this.ceilToCellX(object.x); } } if (that._xVelocity > 0) { if (Math.abs(this.floorToCellX(object.x) - object.x) > deltaX) { direction = 0; } else { object.x = this.floorToCellX(object.x); } } } // Ensure sharp turn even with Verlet integrations. const speed = Math.abs(that._xVelocity + that._yVelocity); if (direction === 0) { that._xVelocity = speed; that._yVelocity = 0; } else if (direction === 4) { that._xVelocity = -speed; that._yVelocity = 0; } else if (direction === 2) { that._yVelocity = speed; that._xVelocity = 0; } else if (direction === 6) { that._yVelocity = -speed; that._xVelocity = 0; } this.lastDirection = direction; return direction; } /** * Called before the acceleration and new direction is applied to the * velocity. * @param {gdjs.TopDownMovementRuntimeBehavior.TopDownMovementHookContext} context */ beforeSpeedUpdate(context) { if (!this.pixelPerfectBehavior.activated()) { return; } const direction = context.getDirection(); /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); const that = this.topDownBehavior; const object = that.owner; if (cellSize <= 0) { return; } const isMovingOnX = direction !== -1 && direction !== 2 && direction !== 6; if (isMovingOnX) { this.targetX = null; } else if (this.targetX === null) { // Find where the deceleration should stop the object. if (that._xVelocity > 0) { this.targetX = this.ceilToCellX( object.x + this.getBreakingDistanceX() ); this.targetDirectionX = 1; } if (that._xVelocity < 0) { this.targetX = this.floorToCellX( object.x - this.getBreakingDistanceX() ); this.targetDirectionX = -1; } } const isMovingOnY = direction !== -1 && direction !== 0 && direction !== 4; if (isMovingOnY) { this.targetY = null; } else if (this.targetY === null) { // Find where the deceleration should stop the object. if (that._yVelocity > 0) { this.targetY = this.ceilToCellY( object.y + this.getBreakingDistanceY() ); this.targetDirectionY = 1; } if (that._yVelocity < 0) { this.targetY = this.floorToCellY( object.y - this.getBreakingDistanceY() ); this.targetDirectionY = -1; } } // Make as if the player had press button a bit longer to reach exactly // the next cell. this.isVelocityAdjustedOnX = this.targetX !== null; if (this.isVelocityAdjustedOnX) { if (this.targetDirectionX > 0) { if (this.targetX > object.getX()) { that._xVelocity = Math.min( that._xVelocity + that._acceleration, that._maxSpeed, this.getSpeedToReach(this.targetX - object.getX()) ); } const nextFrameX = object.getX() + that._xVelocity * object.getElapsedTime() / 1000; if (this.targetX <= nextFrameX) { that._xVelocity = 0; object.setX(this.roundToCellX(object.getX())); this.targetX = null; } } if (this.targetDirectionX < 0) { if (this.targetX < object.getX()) { that._xVelocity = Math.max( that._xVelocity - that._acceleration, -that._maxSpeed, this.getSpeedToReach(this.targetX - object.getX()) ); } const nextFrameX = object.getX() + that._xVelocity * object.getElapsedTime() / 1000; if (this.targetX >= nextFrameX) { that._xVelocity = 0; object.setX(this.roundToCellX(object.getX())); this.targetX = null; } } // The velocity is exact. There no need for Verlet integration. this.previousVelocityX = that._xVelocity; } this.isVelocityAdjustedOnY = this.targetY !== null; if (this.isVelocityAdjustedOnY) { if (this.targetDirectionY > 0) { if (this.targetY > object.getY()) { that._yVelocity = Math.min( that._yVelocity + that._acceleration, that._maxSpeed, this.getSpeedToReach(this.targetY - object.getY()) ); } const nextFrameY = object.getY() + that._yVelocity * object.getElapsedTime() / 1000; if (this.targetY <= nextFrameY) { that._yVelocity = 0; object.setY(this.roundToCellY(object.getY())); this.targetY = null; } } if (this.targetDirectionY < 0) { if (this.targetY < object.getY()) { that._yVelocity = Math.max( that._yVelocity - that._acceleration, -that._maxSpeed, this.getSpeedToReach(this.targetY - object.getY()) ); } const nextFrameY = object.getY() + that._yVelocity * object.getElapsedTime() / 1000; if (this.targetY >= nextFrameY) { that._yVelocity = 0; object.setY(this.roundToCellY(object.getY())); this.targetY = null; } } // The velocity is exact. There no need for Verlet integration. this.previousVelocityY = that._yVelocity; } } /** * Called before the velocity is applied to the object position and * angle. */ beforePositionUpdate() { if (!this.pixelPerfectBehavior.activated()) { return; } /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); if (cellSize <= 0) { return; } const that = this.topDownBehavior; if (this.isVelocityAdjustedOnX) { // The velocity is exact. There no need for Verlet integration. that._xVelocity = this.previousVelocityX; } if (this.isVelocityAdjustedOnY) { // The velocity is exact. There no need for Verlet integration. that._yVelocity = this.previousVelocityY; } } doStepPostEvents(instanceContainer) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); if (cellSize <= 0) { return; } const that = this.topDownBehavior; const object = that.owner; const isMovingOnX = this.lastDirection !== -1 && this.lastDirection !== 2 && this.lastDirection !== 6; const isMovingOnY = this.lastDirection !== -1 && this.lastDirection !== 0 && this.lastDirection !== 4; // Avoid rounding errors after a call to "separate" to make characters // move indefinitely in front of a wall because they can't reach the cell. if (!isMovingOnX && that._xVelocity !== 0) { const x = object.getX(); const roundedX = this.roundToCellX(x); if (Math.abs(roundedX - x) < epsilon) { object.setX(roundedX); this.targetDirectionX = 0; that._xVelocity = 0; } } if (!isMovingOnY && that._yVelocity !== 0) { const y = object.getY(); const roundedY = this.roundToCellY(y); if (Math.abs(roundedY - y) < epsilon) { object.setY(roundedY); this.targetDirectionY = 0; that._yVelocity = 0; } } } /** * @returns {number} the braking distance according to an initial speed and a deceleration. */ getBreakingDistanceX() { const that = this.topDownBehavior; return (that._xVelocity * that._xVelocity) / (2 * that._deceleration); } /** * @returns {number} the braking distance according to an initial speed and a deceleration. */ getBreakingDistanceY() { const that = this.topDownBehavior; return (that._yVelocity * that._yVelocity) / (2 * that._deceleration); } /** * @param {number} displacement * @returns {number} the speed necessary to cover a distance according to the deceleration. */ getSpeedToReach(displacement) { const that = this.topDownBehavior; return ( Math.sign(displacement) * Math.sqrt(2 * Math.abs(displacement) * that._deceleration) ); } /** * @param {number} x * @return {number} */ ceilToCellX(x) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetX = this.pixelPerfectBehavior._getOffsetX(); return ( gridOffsetX + cellSize * Math.ceil((x - gridOffsetX) / cellSize) ); } /** * @param {number} x * @return {number} */ roundToCellX(x) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetX = this.pixelPerfectBehavior._getOffsetX(); return ( gridOffsetX + cellSize * Math.round((x - gridOffsetX) / cellSize) ); } /** * @param {number} x * @return {number} */ floorToCellX(x) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetX = this.pixelPerfectBehavior._getOffsetX(); return ( gridOffsetX + cellSize * Math.floor((x - gridOffsetX) / cellSize) ); } /** * @param {number} y * @return {number} */ ceilToCellY(y) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetY = this.pixelPerfectBehavior._getOffsetY(); return ( gridOffsetY + cellSize * Math.ceil((y - gridOffsetY) / cellSize) ); } /** * @param {number} y * @return {number} */ roundToCellY(y) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetY = this.pixelPerfectBehavior._getOffsetY(); return ( gridOffsetY + cellSize * Math.round((y - gridOffsetY) / cellSize) ); } /** * @param {number} y * @return {number} */ floorToCellY(y) { /** @type {number} */ const cellSize = this.pixelPerfectBehavior._getPixelSize(); /** @type {number} */ const gridOffsetY = this.pixelPerfectBehavior._getOffsetY(); return ( gridOffsetY + cellSize * Math.floor((y - gridOffsetY) / cellSize) ); } } gdjs.__pixelPerfectExtension = { PixelPerfectTopDownMovement } }; gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.eventsList0 = function(runtimeScene, eventsFunctionContext) { { gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.userFunc0xa43328(runtimeScene, eventsFunctionContext); } }; gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.func = function(runtimeScene, parentEventsFunctionContext) { let scopeInstanceContainer = null; var eventsFunctionContext = { _objectsMap: { }, _objectArraysMap: { }, _behaviorNamesMap: { }, globalVariablesForExtension: runtimeScene.getGame().getVariablesForExtension("PixelPerfectMovement"), sceneVariablesForExtension: runtimeScene.getScene().getVariablesForExtension("PixelPerfectMovement"), localVariables: [], getObjects: function(objectName) { return eventsFunctionContext._objectArraysMap[objectName] || []; }, getObjectsLists: function(objectName) { return eventsFunctionContext._objectsMap[objectName] || null; }, getBehaviorName: function(behaviorName) { return eventsFunctionContext._behaviorNamesMap[behaviorName] || behaviorName; }, createObject: function(objectName) { const objectsList = eventsFunctionContext._objectsMap[objectName]; if (objectsList) { const object = parentEventsFunctionContext && !(scopeInstanceContainer && scopeInstanceContainer.isObjectRegistered(objectName)) ? parentEventsFunctionContext.createObject(objectsList.firstKey()) : runtimeScene.createObject(objectsList.firstKey()); if (object) { objectsList.get(objectsList.firstKey()).push(object); if (!(scopeInstanceContainer && scopeInstanceContainer.isObjectRegistered(objectName))) { eventsFunctionContext._objectArraysMap[objectName].push(object); } } return object; } return null; }, getInstancesCountOnScene: function(objectName) { const objectsList = eventsFunctionContext._objectsMap[objectName]; let count = 0; if (objectsList) { for(const objectName in objectsList.items) count += parentEventsFunctionContext && !(scopeInstanceContainer && scopeInstanceContainer.isObjectRegistered(objectName)) ? parentEventsFunctionContext.getInstancesCountOnScene(objectName) : runtimeScene.getInstancesCountOnScene(objectName); } return count; }, getLayer: function(layerName) { return runtimeScene.getLayer(layerName); }, getArgument: function(argName) { return ""; }, getOnceTriggers: function() { return runtimeScene.getOnceTriggers(); } }; gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.eventsList0(runtimeScene, eventsFunctionContext); return; } gdjs.evtsExt__PixelPerfectMovement__DefineJavaScriptForTopDown.registeredGdjsCallbacks = [];