diff --git a/extensions/community/TopDownCornerSliding.json b/extensions/community/TopDownCornerSliding.json index 2208ed1a..45e27fe8 100644 --- a/extensions/community/TopDownCornerSliding.json +++ b/extensions/community/TopDownCornerSliding.json @@ -8,7 +8,7 @@ "name": "TopDownCornerSliding", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/f1e89ff4f907ed2245e2d4ad2d5f52f9e1bb6d0127aa370d44cc77462e5be8e3_subdirectory-arrow-right.svg", "shortDescription": "Slide on corners of rectangular obstacles.", - "version": "0.1.0", + "version": "0.1.1", "description": [ "In top-down games, players may have to go though gaps the same size as their avatar. This is almost impossible to do without any assistance. This extension makes objects slide on obstacles to lead them in the right direction.", "", @@ -31,6 +31,8 @@ "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" ], "dependencies": [], + "globalVariables": [], + "sceneVariables": [], "eventsFunctions": [ { "description": "Define JavaScript classes for top-down.", @@ -41,1270 +43,1239 @@ "sentence": "Define JavaScript classes for top-down", "events": [ { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "GlobalVariableAsBoolean" - }, - "parameters": [ - "__pixelPerfect.TopDownClassesDefined", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "SetGlobalVariableAsBoolean" - }, - "parameters": [ - "__pixelPerfect.TopDownClassesDefined", - "True" - ] - } + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "if (gdjs.__topDownCornerSlidingExtension) {", + " return;", + "}", + "", + "// TODO Remove this when afterPositionUpdate is added to the hook interface.", + "gdjs.TopDownMovementRuntimeBehavior.prototype.actuallyDoStepPreEvents = gdjs.TopDownMovementRuntimeBehavior.prototype.doStepPreEvents;", + "gdjs.TopDownMovementRuntimeBehavior.prototype.doStepPreEvents = function (instanceContainer) {", + " this.actuallyDoStepPreEvents(instanceContainer);", + "", + " for (const topDownMovementHook of this._topDownMovementHooks) {", + " if (topDownMovementHook.afterPositionUpdateTODO) {", + " topDownMovementHook.afterPositionUpdateTODO(null);", + " }", + " }", + "}", + "", + "const deltasX = [1, 1, 0, -1, -1, -1, 0, 1];", + "const deltasY = [0, 1, 1, 1, 0, -1, -1, -1];", + "const temporaryPointForTransformations = [0, 0];", + "const epsilon = 0.015625;", + "", + "/**", + " * {number} a", + " * {number} b", + " * {return {boolean}}", + " */", + "const almostEquals = (a, b) => {", + " return b - epsilon < a && a < b + epsilon;", + "}", + "", + "class CornerSlider {", + "", + " /**", + " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", + " * @param {gdjs.RuntimeBehavior} behavior", + " * @param {gdjs.TopDownMovementRuntimeBehavior} topDownBehavior", + " */", + " constructor(instanceContainer, behavior, topDownBehavior) {", + " this.instanceContainer = instanceContainer;", + " this.behavior = behavior;", + " this.topDownBehavior = topDownBehavior;", + "", + " topDownBehavior.registerHook(this);", + "", + " /**", + " * Obstacles near the object, updated with _updatePotentialCollidingObjects.", + " * @type {Obstacle[]}", + " */", + " this.potentialCollidingObjects = [];", + " /** @type {gdjs.RuntimeObject[]} */", + " this.collidingObjects = [];", + " this.obstacleManager = getManager(instanceContainer);", + "", + " // Remember the decision to bypass an obstacle...", + " this.lastAnyObstacle = false;", + " this.needToCheckBypassWay = true;", + "", + " // ...and the context of that decision", + " this.lastAssistanceDirection = -1;", + " this.lastDirection = -1;", + "", + " /** @type {FloatPoint} */", + " this.transformedPosition = [0, 0];", + " /** @type {gdjs.AABB} */", + " this.relativeHitBoxesAABB = { min: [0, 0], max: [0, 0] };", + " /** @type {gdjs.AABB} */", + " this.absoluteHitBoxesAABB = { min: [0, 0], max: [0, 0] };", + " this.hitBoxesAABBUpToDate = false;", + " this.oldWidth = 0;", + " this.oldHeight = 0;", + " this.previousX = 0;", + " this.previousY = 0;", + "", + " /** @type {AssistanceResult} */", + " this.result = new AssistanceResult();", + " }", + "", + " /**", + " * 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.behavior.activated()) {", + " return direction;", + " }", + "", + " const object = this.topDownBehavior.owner;", + " // Check if the object has moved", + " // To avoid to loop on the transform and its inverse", + " // because of float approximation.", + " const position = temporaryPointForTransformations;", + " // TODO Handle isometry", + " // if (this.topDownBehavior._basisTransformation) {", + " // this.topDownBehavior._basisTransformation.toScreen(", + " // this.transformedPosition,", + " // position", + " // );", + " // } else {", + " position[0] = this.transformedPosition[0];", + " position[1] = this.transformedPosition[1];", + " // }", + " if (object.getX() !== position[0] || object.getY() !== position[1]) {", + " position[0] = object.getX();", + " position[1] = object.getY();", + " // TODO Handle isometry", + " // if (this.topDownBehavior._basisTransformation) {", + " // this.topDownBehavior._basisTransformation.toWorld(", + " // position,", + " // this.transformedPosition", + " // );", + " // } else {", + " this.transformedPosition[0] = position[0];", + " this.transformedPosition[1] = position[1];", + " // }", + " }", + "", + " const stickIsUsed =", + " this.topDownBehavior._stickForce !== 0 && direction === -1;", + " let inputDirection = 0;", + " if (stickIsUsed) {", + " inputDirection = this.getStickDirection();", + " } else {", + " inputDirection = direction;", + " }", + " const assistanceDirection = this.suggestDirection(", + " inputDirection", + " );", + " if (assistanceDirection !== -1) {", + " if (stickIsUsed) {", + " this.topDownBehavior._stickAngle = assistanceDirection * 45;", + " }", + " return assistanceDirection;", + " }", + " return direction;", + " }", + "", + " /**", + " * Called before the acceleration and new direction is applied to the", + " * velocity.", + " * @param {gdjs.TopDownMovementRuntimeBehavior.TopDownMovementHookContext} context", + " */", + " beforeSpeedUpdate(context) { }", + "", + " /**", + " * Called before the velocity is applied to the object position and", + " * angle.", + " */", + " beforePositionUpdate() {", + " if (!this.behavior.activated()) {", + " return;", + " }", + "", + " const object = this.topDownBehavior.owner;", + " this.previousX = object.getX();", + " this.previousY = object.getY();", + " }", + "", + " // TODO Rename this methode to the new hook interface method.", + " afterPositionUpdateTODO() {", + " if (!this.behavior.activated()) {", + " return;", + " }", + "", + " const object = this.topDownBehavior.owner;", + " const point = temporaryPointForTransformations;", + " point[0] = object.getX() - this.previousX;", + " point[1] = object.getY() - this.previousY;", + " // TODO Handle isometry", + " // if (this.topDownBehavior._basisTransformation) {", + " // this.topDownBehavior._basisTransformation.toWorld(point, point);", + " // }", + " this.shift(point[0], point[1]);", + "", + " this.applyCollision();", + "", + " const position = temporaryPointForTransformations;", + "", + " // TODO Handle isometry", + " // if (this.topDownBehavior._basisTransformation) {", + " // this.topDownBehavior._basisTransformation.toScreen(", + " // this.transformedPosition,", + " // position", + " // );", + " // } else {", + " position[0] = this.transformedPosition[0];", + " position[1] = this.transformedPosition[1];", + " // }", + " object.setX(position[0]);", + " object.setY(position[1]);", + " }", + "", + " getStickDirection() {", + " let direction =", + " (this.topDownBehavior._stickAngle +", + " this.topDownBehavior._movementAngleOffset) /", + " 45;", + " direction = direction - Math.floor(direction / 8) * 8;", + " for (let strait = 0; strait < 8; strait += 2) {", + " if (strait - 0.125 < direction && direction < strait + 0.125) {", + " direction = strait;", + " }", + " if (strait + 0.125 <= direction && direction <= strait + 2 - 0.125) {", + " direction = strait + 1;", + " }", + " }", + " if (8 - 0.125 < direction) {", + " direction = 0;", + " }", + " return direction;", + " }", + "", + " /** Analyze the real intent of the player instead of applying the input blindly.", + " * @param {integer} direction", + " * @returns {integer} a direction that matches the player intents.", + " */", + " suggestDirection(direction) {", + " this.needToCheckBypassWay =", + " this.needToCheckBypassWay || direction !== this.lastDirection;", + "", + " if (direction === -1) {", + " return this.noAssistance();", + " }", + "", + " const object = this.topDownBehavior.owner;", + " if (", + " object.getWidth() !== this.oldWidth ||", + " object.getHeight() !== this.oldHeight", + " ) {", + " this.hitBoxesAABBUpToDate = false;", + " this.oldWidth = object.getWidth();", + " this.oldHeight = object.getHeight();", + " }", + "", + " // Compute the list of the objects that will be used", + " const timeDelta = object.getElapsedTime(this.instanceContainer) / 1000;", + " this.updatePotentialCollidingObjects(", + " 1 + this.topDownBehavior.getMaxSpeed() * timeDelta", + " );", + "", + " const downKey = 1 <= direction && direction <= 3;", + " const leftKey = 3 <= direction && direction <= 5;", + " const upKey = 5 <= direction && direction <= 7;", + " const rightKey = direction <= 1 || 7 <= direction;", + "", + " // Used to align the player when the assistance make him bypass an obstacle", + " let stopMinX = Number.MAX_VALUE;", + " let stopMinY = Number.MAX_VALUE;", + " let stopMaxX = -Number.MAX_VALUE;", + " let stopMaxY = -Number.MAX_VALUE;", + " let isBypassX = false;", + " let isBypassY = false;", + "", + " // Incites of how the player should be assisted", + " let assistanceLeft = 0;", + " let assistanceRight = 0;", + " let assistanceUp = 0;", + " let assistanceDown = 0;", + "", + " // the actual decision", + " let assistanceDirection = -1;", + "", + " const objectAABB = this.getHitBoxesAABB();", + " const minX = objectAABB.min[0];", + " const minY = objectAABB.min[1];", + " const maxX = objectAABB.max[0];", + " const maxY = objectAABB.max[1];", + " const width = maxX - minX;", + " const height = maxY - minY;", + "", + " // This affectation has no meaning, it will be override.", + " /** @type {gdjs.AABB | null} */", + " let bypassedObstacleAABB = null;", + "", + " this.collidingObjects.length = 0;", + " this.collidingObjects.push(object);", + "", + " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", + " const obstacleBehavior = this.potentialCollidingObjects[i];", + " const corner = obstacleBehavior.behavior._getSlidingCornerSize();", + " const obstacle = obstacleBehavior.owner;", + " if (obstacle === object) {", + " continue;", + " }", + "", + " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", + " const obstacleMinX = obstacleAABB.min[0];", + " const obstacleMinY = obstacleAABB.min[1];", + " const obstacleMaxX = obstacleAABB.max[0];", + " const obstacleMaxY = obstacleAABB.max[1];", + "", + " const deltaX = deltasX[direction];", + " const deltaY = deltasY[direction];", + " // Extends the box in the player direction", + " if (", + " Math.max(maxX, Math.floor(maxX + deltaX)) > obstacleMinX &&", + " Math.min(minX, Math.ceil(minX + deltaX)) < obstacleMaxX &&", + " Math.max(maxY, Math.floor(maxY + deltaY)) > obstacleMinY &&", + " Math.min(minY, Math.ceil(minY + deltaY)) < obstacleMaxY", + " ) {", + " this.collidingObjects.push(obstacle);", + "", + " // The player is corner to corner to the obstacle.", + " // The assistance will depend on other obstacles.", + " // Both direction are set and the actual to take", + " // is decided at the end.", + " if (", + " almostEquals(maxX, obstacleMinX) &&", + " almostEquals(maxY, obstacleMinY)", + " ) {", + " assistanceRight++;", + " assistanceDown++;", + " } else if (", + " almostEquals(maxX, obstacleMinX) &&", + " almostEquals(minY, obstacleMaxY)", + " ) {", + " assistanceRight++;", + " assistanceUp++;", + " } else if (", + " almostEquals(minX, obstacleMaxX) &&", + " almostEquals(minY, obstacleMaxY)", + " ) {", + " assistanceLeft++;", + " assistanceUp++;", + " } else if (", + " almostEquals(minX, obstacleMaxX) &&", + " almostEquals(maxY, obstacleMinY)", + " ) {", + " assistanceLeft++;", + " assistanceDown++;", + " } else if (", + " (upKey && almostEquals(minY, obstacleMaxY)) ||", + " (downKey && almostEquals(maxY, obstacleMinY))", + " ) {", + " // The player is not on the corner of the obstacle.", + " // Set the assistance both ways to fall back in", + " // the same case as 2 obstacles side by side", + " // being collide with the player.", + " if (", + " (rightKey || maxX > obstacleMinX + corner) &&", + " minX < obstacleMaxX &&", + " (leftKey || minX < obstacleMaxX - corner) &&", + " maxX > obstacleMinX", + " ) {", + " assistanceLeft++;", + " assistanceRight++;", + " }", + " // The player is on the corner of the obstacle.", + " // (not the exact corner, see corner affectation)", + " else if (", + " !rightKey &&", + " obstacleMinX < maxX &&", + " maxX <= obstacleMinX + corner &&", + " // In case the cornerSize is bigger than the obstacle size,", + " // go the on the shortest side.", + " (leftKey || minX + maxX <= obstacleMinX + obstacleMaxX)", + " ) {", + " assistanceLeft++;", + " isBypassX = true;", + " if (obstacleMinX - width < stopMinX) {", + " stopMinX = obstacleMinX - width;", + " bypassedObstacleAABB = obstacleAABB;", + " }", + " } else if (", + " !leftKey &&", + " obstacleMaxX - corner <= minX &&", + " minX < obstacleMaxX &&", + " (rightKey || minX + maxX > obstacleMinX + obstacleMaxX)", + " ) {", + " assistanceRight++;", + " isBypassX = true;", + " if (obstacleMaxX > stopMaxX) {", + " stopMaxX = obstacleMaxX;", + " bypassedObstacleAABB = obstacleAABB;", + " }", + " }", + " } else if (", + " (leftKey && almostEquals(minX, obstacleMaxX)) ||", + " (rightKey && almostEquals(maxX, obstacleMinX))", + " ) {", + " // The player is not on the corner of the obstacle.", + " // Set the assistance both ways to fall back in", + " // the same case as 2 obstacles side by side", + " // being collide with the player.", + " if (", + " (downKey || maxY > obstacleMinY + corner) &&", + " minY < obstacleMaxY &&", + " (upKey || minY < obstacleMaxY - corner) &&", + " maxY > obstacleMinY", + " ) {", + " assistanceUp++;", + " assistanceDown++;", + " }", + " // The player is on the corner of the obstacle.", + " // (not the exact corner, see corner affectation)", + " else if (", + " !downKey &&", + " obstacleMinY < maxY &&", + " maxY <= obstacleMinY + corner &&", + " (upKey || minY + maxY <= obstacleMinY + obstacleMaxY)", + " ) {", + " assistanceUp++;", + " isBypassY = true;", + " if (obstacleMinY - height < stopMinY) {", + " stopMinY = obstacleMinY - height;", + " bypassedObstacleAABB = obstacleAABB;", + " }", + " } else if (", + " !upKey &&", + " obstacleMaxY - corner <= minY &&", + " minY < obstacleMaxY &&", + " (downKey || minY + maxY > obstacleMinY + obstacleMaxY)", + " ) {", + " assistanceDown++;", + " isBypassY = true;", + " if (obstacleMaxY > stopMaxY) {", + " stopMaxY = obstacleMaxY;", + " bypassedObstacleAABB = obstacleAABB;", + " }", + " }", + " }", + " }", + " }", + "", + " // This may happen when the player is in the corner of 2 perpendicular walls.", + " // No assistance is needed.", + " if (", + " assistanceLeft &&", + " assistanceRight &&", + " assistanceUp &&", + " assistanceDown", + " ) {", + " return this.noAssistance();", + " }", + " // This may happen when the player goes in diagonal against a wall.", + " // Make him follow the wall. This allows player to keep full speed.", + " //", + " // When he collided a square from the wall corner to corner,", + " // a 3rd assistance may be true but it fall back in the same case.", + " else if (assistanceLeft && assistanceRight) {", + " isBypassX = false;", + " if (leftKey && !rightKey) {", + " assistanceDirection = 4;", + " } else if (rightKey && !leftKey) {", + " assistanceDirection = 0;", + " } else {", + " // Contradictory decisions are dismissed.", + " //", + " // This can happen, for instance, with a wall composed of squares.", + " // Taken separately from one to another, a square could be bypass one the right", + " // and the next one on the left even though they are side by side", + " // and the player can't actually go between them.", + " return this.noAssistance();", + " }", + " } else if (assistanceUp && assistanceDown) {", + " isBypassY = false;", + " if (upKey && !downKey) {", + " assistanceDirection = 6;", + " } else if (downKey && !upKey) {", + " assistanceDirection = 2;", + " } else {", + " // see previous comment", + " return this.noAssistance();", + " }", + " }", + " // The player goes in diagonal and is corner to corner with the obstacle.", + " // (but not against a wall, this time)", + " // The velocity is used to decide.", + " // This may only happen after an alignment.", + " // (see \"Alignment:\" comment)", + " else if (assistanceRight && assistanceDown) {", + " if (", + " (downKey && !rightKey) ||", + " (downKey === rightKey && assistanceDown > assistanceRight) ||", + " (assistanceDown === assistanceRight &&", + " this.topDownBehavior._yVelocity > 0 &&", + " Math.abs(this.topDownBehavior._xVelocity) <", + " Math.abs(this.topDownBehavior._yVelocity))", + " ) {", + " assistanceDirection = 2;", + " } else {", + " assistanceDirection = 0;", + " }", + " } else if (assistanceLeft && assistanceDown) {", + " if (", + " (downKey && !leftKey) ||", + " (downKey === leftKey && assistanceDown > assistanceLeft) ||", + " (assistanceDown === assistanceLeft &&", + " this.topDownBehavior._yVelocity > 0 &&", + " Math.abs(this.topDownBehavior._xVelocity) <", + " Math.abs(this.topDownBehavior._yVelocity))", + " ) {", + " assistanceDirection = 2;", + " } else {", + " assistanceDirection = 4;", + " }", + " } else if (assistanceLeft && assistanceUp) {", + " if (", + " (upKey && !leftKey) ||", + " (upKey === leftKey && assistanceUp > assistanceLeft) ||", + " (assistanceUp === assistanceLeft &&", + " this.topDownBehavior._yVelocity < 0 &&", + " Math.abs(this.topDownBehavior._xVelocity) <", + " Math.abs(this.topDownBehavior._yVelocity))", + " ) {", + " assistanceDirection = 6;", + " } else {", + " assistanceDirection = 4;", + " }", + " } else if (assistanceRight && assistanceUp) {", + " if (", + " (upKey && !rightKey) ||", + " (upKey === rightKey && assistanceUp > assistanceRight) ||", + " (assistanceUp === assistanceRight &&", + " this.topDownBehavior._yVelocity < 0 &&", + " Math.abs(this.topDownBehavior._xVelocity) <", + " Math.abs(this.topDownBehavior._yVelocity))", + " ) {", + " assistanceDirection = 6;", + " } else {", + " assistanceDirection = 0;", + " }", + " } else {", + " // Slide on the corner of an obstacle to bypass it.", + " // Every tricky cases are already handled .", + " if (assistanceLeft) {", + " assistanceDirection = 4;", + " } else if (assistanceRight) {", + " assistanceDirection = 0;", + " } else if (assistanceUp) {", + " assistanceDirection = 6;", + " } else if (assistanceDown) {", + " assistanceDirection = 2;", + " } else {", + " return this.noAssistance();", + " }", + " }", + "", + " // Check if there is any obstacle in the way.", + " //", + " // There must be no obstacle to go at least", + " // as far in the direction the player chose", + " // as the assistance must take to align the player.", + " //", + " // Because, if the assistance moves the player by 32 pixels", + " // before been able to go in the right direction", + " // and can only move by 4 pixels afterward", + " // that it'll sound silly.", + " this.needToCheckBypassWay =", + " this.needToCheckBypassWay ||", + " assistanceDirection !== this.lastAssistanceDirection;", + " if ((isBypassX || isBypassY) && !this.needToCheckBypassWay) {", + " // Don't check again if the player intent stays the same.", + " //", + " // Do it, for instance, if an obstacle has moved out of the way", + " // and the player releases and presses agin the key.", + " // Because, doing it automatically would seems weird.", + " if (this.lastAnyObstacle) {", + " return this.noAssistance();", + " }", + " } else if (isBypassX || isBypassY) {", + " this.lastAssistanceDirection = assistanceDirection;", + " this.lastDirection = direction;", + "", + " let anyObstacle = false;", + " // reflection symmetry: y = x", + " // 0 to 6, 2 to 4, 4 to 2, 6 to 0", + " if (direction + assistanceDirection === 6) {", + " // Because the obstacle may not be a square.", + " let cornerX = 0;", + " let cornerY = 0;", + " if (assistanceDirection === 4 || assistanceDirection === 6) {", + " cornerX = bypassedObstacleAABB.min[0];", + " cornerY = bypassedObstacleAABB.min[1];", + " } else {", + " cornerX = bypassedObstacleAABB.max[0];", + " cornerY = bypassedObstacleAABB.max[1];", + " }", + " // / cornerX \\ / 0 1 \\ / x - cornerX \\", + " // \\ cornerY / + \\ 1 0 / * \\ y - cornerY /", + " //", + " // min and max are preserved by the symmetry.", + " // The symmetry image is extended to check there is no obstacle before going into the passage.", + " const searchMinX =", + " cornerX +", + " minY -", + " cornerY +", + " epsilon +", + " (assistanceDirection === 6 ? cornerY - maxY : 0);", + " const searchMaxX =", + " cornerX +", + " maxY -", + " cornerY -", + " epsilon +", + " (assistanceDirection === 2 ? cornerY - minY : 0);", + " const searchMinY =", + " cornerY +", + " minX -", + " cornerX +", + " epsilon +", + " (assistanceDirection === 4 ? cornerX - maxX : 0);", + " const searchMaxY =", + " cornerY +", + " maxX -", + " cornerX -", + " epsilon +", + " (assistanceDirection === 0 ? cornerX - minX : 0);", + "", + " anyObstacle = this.obstacleManager.anyObstacle(", + " searchMinX,", + " searchMaxX,", + " searchMinY,", + " searchMaxY,", + " this.collidingObjects", + " );", + " }", + " // reflection symmetry: y = -x", + " // 0 to 2, 2 to 0, 4 to 6, 6 to 4", + " else if ((direction + assistanceDirection) % 8 === 2) {", + " // Because the obstacle may not be a square.", + " let cornerX = 0;", + " let cornerY = 0;", + " if (assistanceDirection === 2 || assistanceDirection === 4) {", + " cornerX = bypassedObstacleAABB.min[0];", + " cornerY = bypassedObstacleAABB.max[1];", + " } else {", + " cornerX = bypassedObstacleAABB.max[0];", + " cornerY = bypassedObstacleAABB.min[1];", + " }", + " // / cornerX \\ / 0 -1 \\ / x - cornerX \\", + " // \\ cornerY / + \\ -1 0 / * \\ y - cornerY /", + " //", + " // min and max are switched by the symmetry.", + " // The symmetry image is extended to check there is no obstacle before going into the passage.", + " const searchMinX =", + " cornerX -", + " (maxY - cornerY) +", + " epsilon +", + " (assistanceDirection === 2 ? minY - cornerY : 0);", + " const searchMaxX =", + " cornerX -", + " (minY - cornerY) -", + " epsilon +", + " (assistanceDirection === 6 ? maxY - cornerY : 0);", + " const searchMinY =", + " cornerY -", + " (maxX - cornerX) +", + " epsilon +", + " (assistanceDirection === 0 ? minX - cornerX : 0);", + " const searchMaxY =", + " cornerY -", + " (minX - cornerX) -", + " epsilon +", + " (assistanceDirection === 4 ? maxX - cornerX : 0);", + "", + " anyObstacle = this.obstacleManager.anyObstacle(", + " searchMinX,", + " searchMaxX,", + " searchMinY,", + " searchMaxY,", + " this.collidingObjects", + " );", + " }", + " this.lastAnyObstacle = anyObstacle;", + " this.needToCheckBypassWay = false;", + "", + " if (anyObstacle) {", + " return this.noAssistance();", + " }", + " }", + "", + " this.result.inputDirection = direction;", + " this.result.assistanceLeft = assistanceLeft > 0;", + " this.result.assistanceRight = assistanceRight > 0;", + " this.result.assistanceUp = assistanceUp > 0;", + " this.result.assistanceDown = assistanceDown > 0;", + " this.result.isBypassX = isBypassX;", + " this.result.isBypassY = isBypassY;", + " this.result.stopMinX = stopMinX;", + " this.result.stopMinY = stopMinY;", + " this.result.stopMaxX = stopMaxX;", + " this.result.stopMaxY = stopMaxY;", + "", + " return assistanceDirection;", + " }", + "", + " /**", + " * @return {integer}", + " */", + " noAssistance = function () {", + " this.result.isBypassX = false;", + " this.result.isBypassY = false;", + "", + " return -1;", + " }", + "", + " applyCollision = function () {", + " this.checkCornerStop();", + " this.separateFromObstacles();", + " // check again because the object can be pushed on the stop limit,", + " // it won't be detected on the next frame and the alignment won't be applied.", + " this.checkCornerStop();", + " }", + "", + " /**", + " * Check if the object must take a corner.", + " *", + " * When the object reach the limit of an obstacle", + " * and it should take the corner according to the player intent,", + " * it is aligned right on this limit and the velocity is set in the right direction.", + " *", + " * This avoid issues with the inertia. For instance,", + " * when the object could go between 2 obstacles,", + " * with it will just fly over the hole because of its inertia.", + " */", + " checkCornerStop = function () {", + " const objectAABB = this.getHitBoxesAABB();", + " const minX = objectAABB.min[0];", + " const minY = objectAABB.min[1];", + " const object = this.topDownBehavior.owner;", + "", + " const direction = this.result.inputDirection;", + " const leftKey = 3 <= direction && direction <= 5;", + " const upKey = 5 <= direction && direction <= 7;", + "", + " // Alignment: avoid to go too far and kind of drift or oscillate in front of a hole.", + " if (", + " this.result.isBypassX &&", + " ((this.result.assistanceLeft && minX <= this.result.stopMinX) ||", + " (this.result.assistanceRight && minX >= this.result.stopMaxX))", + " ) {", + " this.shift(", + " -minX +", + " (this.result.assistanceLeft", + " ? this.result.stopMinX", + " : this.result.stopMaxX),", + " 0", + " );", + " this.topDownBehavior._yVelocity =", + " (upKey ? -1 : 1) *", + " Math.sqrt(", + " this.topDownBehavior._xVelocity *", + " this.topDownBehavior._xVelocity +", + " this.topDownBehavior._yVelocity *", + " this.topDownBehavior._yVelocity", + " );", + " this.topDownBehavior._xVelocity = 0;", + " }", + " if (", + " this.result.isBypassY &&", + " ((this.result.assistanceUp && minY <= this.result.stopMinY) ||", + " (this.result.assistanceDown && minY >= this.result.stopMaxY))", + " ) {", + " this.shift(", + " 0,", + " -minY +", + " (this.result.assistanceUp", + " ? this.result.stopMinY", + " : this.result.stopMaxY)", + " );", + " this.topDownBehavior._xVelocity =", + " (leftKey ? -1 : 1) *", + " Math.sqrt(", + " this.topDownBehavior._xVelocity *", + " this.topDownBehavior._xVelocity +", + " this.topDownBehavior._yVelocity *", + " this.topDownBehavior._yVelocity", + " );", + " this.topDownBehavior._yVelocity = 0;", + " }", + " }", + "", + " /**", + " * Separate from TopDownObstacleRuntimeBehavior instances.", + " */", + " separateFromObstacles = function () {", + " const object = this.topDownBehavior.owner;", + " const objectAABB = this.getHitBoxesAABB();", + " const minX = objectAABB.min[0];", + " const minY = objectAABB.min[1];", + " const maxX = objectAABB.max[0];", + " const maxY = objectAABB.max[1];", + "", + " // Search the obstacle with the biggest intersection", + " // to separate from this one first.", + " // Because smaller collisions may shift the player", + " // in the wrong direction.", + " let maxSurface = 0;", + " /** @type {gdjs.RuntimeBehavior | null} */", + " let bestObstacleBehavior = null;", + " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", + " const obstacleBehavior = this.potentialCollidingObjects[i];", + " if (obstacleBehavior.behavior.owner === object) {", + " continue;", + " }", + "", + " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", + " const obstacleMinX = obstacleAABB.min[0];", + " const obstacleMinY = obstacleAABB.min[1];", + " const obstacleMaxX = obstacleAABB.max[0];", + " const obstacleMaxY = obstacleAABB.max[1];", + "", + " const interMinX = Math.max(minX, obstacleMinX);", + " const interMinY = Math.max(minY, obstacleMinY);", + " const interMaxX = Math.min(maxX, obstacleMaxX);", + " const interMaxY = Math.min(maxY, obstacleMaxY);", + "", + " if (interMinX < interMaxX && interMinY < interMaxY) {", + " const surface = (interMaxX - interMinX) * (interMaxY - interMinY);", + " if (surface > maxSurface) {", + " maxSurface = surface;", + " bestObstacleBehavior = obstacleBehavior;", + " }", + " }", + " }", + " if (bestObstacleBehavior !== null) {", + " this.separateFrom(bestObstacleBehavior);", + " }", + " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", + " const obstacleBehavior = this.potentialCollidingObjects[i];", + " const obstacle = obstacleBehavior.behavior.owner;", + " if (obstacle === object) {", + " continue;", + " }", + " this.separateFrom(obstacleBehavior);", + " }", + " }", + "", + " /**", + " * Separate object and obstacle, only object move.", + " * @param {Obstacle} obstacleBehavior", + " */", + " separateFrom = function (obstacleBehavior) {", + " const objectAABB = this.getHitBoxesAABB();", + " const minX = objectAABB.min[0];", + " const minY = objectAABB.min[1];", + " const maxX = objectAABB.max[0];", + " const maxY = objectAABB.max[1];", + "", + " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", + " const obstacleMinX = obstacleAABB.min[0];", + " const obstacleMinY = obstacleAABB.min[1];", + " const obstacleMaxX = obstacleAABB.max[0];", + " const obstacleMaxY = obstacleAABB.max[1];", + "", + " const leftDistance = maxX - obstacleMinX;", + " const upDistance = maxY - obstacleMinY;", + " const rightDistance = obstacleMaxX - minX;", + " const downDistance = obstacleMaxY - minY;", + " const minDistance = Math.min(", + " leftDistance,", + " upDistance,", + " rightDistance,", + " downDistance", + " );", + "", + " if (minDistance > 0) {", + " if (leftDistance === minDistance) {", + " this.shift(-maxX + obstacleMinX, 0);", + " } else if (rightDistance === minDistance) {", + " this.shift(-minX + obstacleMaxX, 0);", + " } else if (upDistance === minDistance) {", + " this.shift(0, -maxY + obstacleMinY);", + " } else if (downDistance === minDistance) {", + " this.shift(0, -minY + obstacleMaxY);", + " }", + " }", + " }", + "", + " /**", + " * @param {float} deltaX", + " * @param {float} deltaY", + " */", + " shift(deltaX, deltaY) {", + " this.transformedPosition[0] += deltaX;", + " this.transformedPosition[1] += deltaY;", + " }", + "", + " /**", + " * @return {gdjs.AABB}", + " */", + " getHitBoxesAABB() {", + " if (!this.hitBoxesAABBUpToDate) {", + " const hitBoxes = this.topDownBehavior.owner.getHitBoxes();", + "", + " let minX = Number.MAX_VALUE;", + " let minY = Number.MAX_VALUE;", + " let maxX = -Number.MAX_VALUE;", + " let maxY = -Number.MAX_VALUE;", + " for (let h = 0, lenh = hitBoxes.length; h < lenh; ++h) {", + " let hitBox = hitBoxes[h];", + " for (let p = 0, lenp = hitBox.vertices.length; p < lenp; ++p) {", + " const point = this.topDownBehavior", + " ._temporaryPointForTransformations;", + " // TODO Handle isometry", + " // if (this.topDownBehavior._basisTransformation) {", + " // this.topDownBehavior._basisTransformation.toWorld(", + " // hitBox.vertices[p],", + " // point", + " // );", + " // } else {", + " point[0] = hitBox.vertices[p][0];", + " point[1] = hitBox.vertices[p][1];", + " // }", + " minX = Math.min(minX, point[0]);", + " maxX = Math.max(maxX, point[0]);", + " minY = Math.min(minY, point[1]);", + " maxY = Math.max(maxY, point[1]);", + " }", + " }", + " this.relativeHitBoxesAABB.min[0] = minX - this.transformedPosition[0];", + " this.relativeHitBoxesAABB.min[1] = minY - this.transformedPosition[1];", + " this.relativeHitBoxesAABB.max[0] = maxX - this.transformedPosition[0];", + " this.relativeHitBoxesAABB.max[1] = maxY - this.transformedPosition[1];", + "", + " this.hitBoxesAABBUpToDate = true;", + " }", + " this.absoluteHitBoxesAABB.min[0] =", + " this.relativeHitBoxesAABB.min[0] + this.transformedPosition[0];", + " this.absoluteHitBoxesAABB.min[1] =", + " this.relativeHitBoxesAABB.min[1] + this.transformedPosition[1];", + " this.absoluteHitBoxesAABB.max[0] =", + " this.relativeHitBoxesAABB.max[0] + this.transformedPosition[0];", + " this.absoluteHitBoxesAABB.max[1] =", + " this.relativeHitBoxesAABB.max[1] + this.transformedPosition[1];", + " return this.absoluteHitBoxesAABB;", + " }", + "", + " /**", + " * Update _potentialCollidingObjects member with platforms near the object.", + " * @param {float} maxMovementLength", + " */", + " updatePotentialCollidingObjects(maxMovementLength) {", + " this.obstacleManager.getAllObstaclesAround(", + " this.getHitBoxesAABB(),", + " maxMovementLength,", + " this.potentialCollidingObjects", + " );", + " }", + "}", + "", + "", + "/**", + " * TopDownMovementRuntimeBehavior represents a behavior allowing objects to", + " * follow a path computed to avoid obstacles.", + " */", + "class AssistanceResult {", + " constructor() {", + " this.inputDirection = -1;", + " this.assistanceLeft = false;", + " this.assistanceRight = false;", + " this.assistanceUp = false;", + " this.assistanceDown = false;", + " this.isBypassX = false;", + " this.isBypassY = false;", + " this.stopMinX = 0;", + " this.stopMinY = 0;", + " this.stopMaxX = 0;", + " this.stopMaxY = 0;", + " }", + "}", + "", + "/**", + " * Allow to store a behavior in a RBush (spatial data structure).", + " * Because this duplicates the AABB, this ensures the RBush AABB", + " * stays the same even if the underlying object is moved", + " * (in which case the behavior is responsible for removing/adding", + " * back/updating this BehaviorRBushAABB).", + " */", + "class ObstacleRBushAABB {", + " /**", + " * @param {Obstacle} obstacle", + " */", + " constructor(obstacle) {", + " this.obstacle = obstacle;", + " this.minX = 0;", + " this.minY = 0;", + " this.maxX = 0;", + " this.maxY = 0;", + " this.updateAABBFromOwner();", + " }", + "", + " updateAABBFromOwner() {", + " const aabb = this.obstacle.getHitBoxesAABB();", + " this.minX = aabb.min[0];", + " this.minY = aabb.min[1];", + " this.maxX = aabb.max[0];", + " this.maxY = aabb.max[1];", + " }", + "}", + "", + "/**", + " * Get the obstacle manager of an instance container.", + " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", + " * @return {ObstacleManager}", + " */", + "function getManager(instanceContainer) {", + " if (!instanceContainer.__topDownCornerSlidingExtension) {", + " instanceContainer.__topDownCornerSlidingExtension = {};", + " }", + " if (!instanceContainer.__topDownCornerSlidingExtension.manager) {", + " // Create the shared manager if necessary.", + " instanceContainer.__topDownCornerSlidingExtension.manager = new ObstacleManager();", + " }", + " return instanceContainer.__topDownCornerSlidingExtension.manager;", + "}", + "", + "/**", + " * An obstacle manager for fast spacial search.", + " */", + "class ObstacleManager {", + " /**", + " * Create a manager.", + " */", + " constructor() {", + " this.obstacleRBush = new rbush();", + " }", + "", + " /**", + " * Register an obstacle.", + " * @param {Obstacle} obstacle", + " */", + " addObstacle(obstacle) {", + " obstacle.currentRBushAABB.updateAABBFromOwner();", + " this.obstacleRBush.insert(obstacle.currentRBushAABB);", + " }", + "", + " /**", + " * Unregister an obstacle.", + " * @param {Obstacle} obstacle", + " */", + " removeObstacle(obstacle) {", + " this.obstacleRBush.remove(obstacle.currentRBushAABB);", + " }", + "", + " /**", + " * Returns all the boids around the specified boid.", + " * @param {gdjs.AABB} aabb", + " * @param {number} maxMovementLength", + " * @param {Obstacle[]} result the returned array.", + " * @return {Obstacle[]} An array with all obstacles near the object.", + " */", + " getAllObstaclesAround(aabb, maxMovementLength, result) {", + " const searchArea = gdjs.staticObject(", + " ObstacleManager.prototype.getAllObstaclesAround", + " );", + " searchArea.minX = aabb.min[0] - maxMovementLength;", + " searchArea.minY = aabb.min[1] - maxMovementLength;", + " searchArea.maxX = aabb.max[0] + maxMovementLength;", + " searchArea.maxY = aabb.max[1] + maxMovementLength;", + " /** @type {ObstacleRBushAABB[]} */", + " const nearbyObstacles = this.obstacleRBush.search(searchArea);", + "", + " result.length = 0;", + " for (let i = 0; i < nearbyObstacles.length; i++) {", + " const obstacle = nearbyObstacles[i].obstacle;", + " result.push(obstacle);", + " }", + "", + " return result;", + " }", + "", + " /**", + " * Returns true if there is any obstacle intersecting the area.", + " * @param {number} minX", + " * @param {number} maxX", + " * @param {number} minY", + " * @param {number} maxY", + " * @param {gdjs.RuntimeObject[]} excluded", + " * @return {boolean}", + " */", + " anyObstacle(", + " minX,", + " maxX,", + " minY,", + " maxY,", + " excluded", + " ) {", + " const searchArea = gdjs.staticObject(", + " ObstacleManager.prototype.anyObstacle", + " );", + " // @ts-ignore", + " searchArea.minX = minX;", + " // @ts-ignore", + " searchArea.minY = minY;", + " // @ts-ignore", + " searchArea.maxX = maxX;", + " // @ts-ignore", + " searchArea.maxY = maxY;", + " /** @type {ObstacleRBushAABB[]} */", + " const nearbyObstacles = this.obstacleRBush.search(searchArea);", + "", + " for (let i = 0; i < nearbyObstacles.length; i++) {", + " if (!excluded.includes(nearbyObstacles[i].obstacle.behavior.owner)) {", + " return true;", + " }", + " }", + " return false;", + " }", + "}", + "", + "/**", + " * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be", + " * considered as a obstacle by objects having Pathfinding Behavior.", + " */", + "class Obstacle {", + " /**", + " * ", + " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", + " * @param {gdjs.RuntimeBehavior} behavior", + " */", + " constructor(instanceContainer, behavior) {", + " this.oldX = 0;", + " this.oldY = 0;", + " this.oldWidth = 0;", + " this.oldHeight = 0;", + " this.registeredInManager = false;", + " this.behavior = behavior;", + " this.manager = getManager(instanceContainer);", + " // Note that we can't use getX(), getWidth()... of owner here:", + " // The owner is not yet fully constructed.", + " /** @type {gdjs.AABB} */", + " this.hitBoxesAABB = { min: [0, 0], max: [0, 0] };", + " this.hitBoxesAABBUpToDate = false;", + "", + " /** @type {FloatPoint} */", + " this.point = [0, 0];", + " this.currentRBushAABB = new ObstacleRBushAABB(this);", + " }", + " onCreated() {", + " this.manager.addObstacle(this);", + " this.registeredInManager = true;", + " }", + " onDestroy() {", + " if (this.manager && this.registeredInManager) {", + " this.manager.removeObstacle(this);", + " }", + " }", + " doStepPreEvents(instanceContainer) {", + " var owner = this.behavior.owner;", + " // Make sure the obstacle is or is not in the obstacles manager.", + " if (!this.behavior.activated() && this.registeredInManager) {", + " this.manager.removeObstacle(this);", + " this.registeredInManager = false;", + " }", + " else {", + " if (this.behavior.activated() && !this.registeredInManager) {", + " this.manager.addObstacle(this);", + " this.registeredInManager = true;", + " }", + " }", + " // Track changes in size or position", + " if (this.oldX !== owner.getX() ||", + " this.oldY !== owner.getY() ||", + " this.oldWidth !== owner.getWidth() ||", + " this.oldHeight !== owner.getHeight()) {", + " this.hitBoxesAABBUpToDate = false;", + " if (this.registeredInManager) {", + " this.manager.removeObstacle(this);", + " this.manager.addObstacle(this);", + " }", + " this.oldX = owner.getX();", + " this.oldY = owner.getY();", + " this.oldWidth = owner.getWidth();", + " this.oldHeight = owner.getHeight();", + " }", + " }", + " doStepPostEvents(instanceContainer) { }", + " onActivate() {", + " if (this.registeredInManager) {", + " return;", + " }", + " this.manager.addObstacle(this);", + " this.registeredInManager = true;", + " }", + " onDeActivate() {", + " if (!this.registeredInManager) {", + " return;", + " }", + " this.manager.removeObstacle(this);", + " this.registeredInManager = false;", + " }", + "", + " /**", + " * @return {gdjs.AABB}", + " */", + " getHitBoxesAABB() {", + " if (!this.hitBoxesAABBUpToDate) {", + " const hitBoxes = this.behavior.owner.getHitBoxes();", + "", + " let minX = Number.MAX_VALUE;", + " let minY = Number.MAX_VALUE;", + " let maxX = -Number.MAX_VALUE;", + " let maxY = -Number.MAX_VALUE;", + " for (let h = 0, lenh = hitBoxes.length; h < lenh; ++h) {", + " let hitBox = hitBoxes[h];", + " for (let p = 0, lenp = hitBox.vertices.length; p < lenp; ++p) {", + " const point = this.point;", + " // TODO Handle Isometry", + " //this._basisTransformation.toWorld(hitBox.vertices[p], point);", + " point[0] = hitBox.vertices[p][0];", + " point[1] = hitBox.vertices[p][1];", + "", + " minX = Math.min(minX, point[0]);", + " maxX = Math.max(maxX, point[0]);", + " minY = Math.min(minY, point[1]);", + " maxY = Math.max(maxY, point[1]);", + " }", + " }", + " this.hitBoxesAABB.min[0] = minX;", + " this.hitBoxesAABB.min[1] = minY;", + " this.hitBoxesAABB.max[0] = maxX;", + " this.hitBoxesAABB.max[1] = maxY;", + "", + " this.hitBoxesAABBUpToDate = true;", + " }", + " return this.hitBoxesAABB;", + " }", + "}", + "", + "gdjs.__topDownCornerSlidingExtension = {", + " CornerSlider,", + " Obstacle,", + "};", + "" ], - "events": [ - { - "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": [ - "// TODO Remove this when afterPositionUpdate is added to the hook interface.", - "gdjs.TopDownMovementRuntimeBehavior.prototype.actuallyDoStepPreEvents = gdjs.TopDownMovementRuntimeBehavior.prototype.doStepPreEvents;", - "gdjs.TopDownMovementRuntimeBehavior.prototype.doStepPreEvents = function (instanceContainer) {", - " this.actuallyDoStepPreEvents(instanceContainer);", - "", - " for (const topDownMovementHook of this._topDownMovementHooks) {", - " topDownMovementHook.afterPositionUpdateTODO(null);", - " }", - "}", - "", - "const deltasX = [1, 1, 0, -1, -1, -1, 0, 1];", - "const deltasY = [0, 1, 1, 1, 0, -1, -1, -1];", - "const temporaryPointForTransformations = [0, 0];", - "const epsilon = 0.015625;", - "", - "/**", - " * {number} a", - " * {number} b", - " * {return {boolean}}", - " */", - "const almostEquals = function (a, b) {", - " return b - epsilon < a && a < b + epsilon;", - "}", - "", - "const CornerSlider = /** @class */ (function () {", - "", - " /**", - " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", - " * @param {gdjs.RuntimeBehavior} behavior", - " * @param {gdjs.TopDownMovementRuntimeBehavior} topDownBehavior", - " */", - " function CornerSlider(instanceContainer, behavior, topDownBehavior) {", - " this.instanceContainer = instanceContainer;", - " this.behavior = behavior;", - " this.topDownBehavior = topDownBehavior;", - "", - " topDownBehavior.registerHook(this);", - "", - " /**", - " * Obstacles near the object, updated with _updatePotentialCollidingObjects.", - " * @type {Obstacle[]}", - " */", - " this.potentialCollidingObjects = [];", - " /** @type {gdjs.RuntimeObject[]} */", - " this.collidingObjects = [];", - " this.obstacleManager = ObstacleManager.getManager(instanceContainer);", - "", - " // Remember the decision to bypass an obstacle...", - " this.lastAnyObstacle = false;", - " this.needToCheckBypassWay = true;", - "", - " // ...and the context of that decision", - " this.lastAssistanceDirection = -1;", - " this.lastDirection = -1;", - "", - " /** @type {FloatPoint} */", - " this.transformedPosition = [0, 0];", - " /** @type {gdjs.AABB} */", - " this.relativeHitBoxesAABB = { min: [0, 0], max: [0, 0] };", - " /** @type {gdjs.AABB} */", - " this.absoluteHitBoxesAABB = { min: [0, 0], max: [0, 0] };", - " this.hitBoxesAABBUpToDate = false;", - " this.oldWidth = 0;", - " this.oldHeight = 0;", - " this.previousX = 0;", - " this.previousY = 0;", - "", - " /** @type {AssistanceResult} */", - " this.result = new AssistanceResult();", - " }", - "", - " /**", - " * Return the direction to use instead of the direction given in", - " * parameter.", - " * @param {gdjs.TopDownMovementRuntimeBehavior.TopDownMovementHookContext} context", - " * @return {number}", - " */", - " CornerSlider.prototype.overrideDirection = function (context) {", - " let direction = context.getDirection();", - " if (!this.behavior.activated()) {", - " return direction;", - " }", - "", - " const object = this.topDownBehavior.owner;", - " // Check if the object has moved", - " // To avoid to loop on the transform and its inverse", - " // because of float approximation.", - " const position = temporaryPointForTransformations;", - " // TODO Handle isometry", - " // if (this.topDownBehavior._basisTransformation) {", - " // this.topDownBehavior._basisTransformation.toScreen(", - " // this.transformedPosition,", - " // position", - " // );", - " // } else {", - " position[0] = this.transformedPosition[0];", - " position[1] = this.transformedPosition[1];", - " // }", - " if (object.getX() !== position[0] || object.getY() !== position[1]) {", - " position[0] = object.getX();", - " position[1] = object.getY();", - " // TODO Handle isometry", - " // if (this.topDownBehavior._basisTransformation) {", - " // this.topDownBehavior._basisTransformation.toWorld(", - " // position,", - " // this.transformedPosition", - " // );", - " // } else {", - " this.transformedPosition[0] = position[0];", - " this.transformedPosition[1] = position[1];", - " // }", - " }", - "", - " const stickIsUsed =", - " this.topDownBehavior._stickForce !== 0 && direction === -1;", - " let inputDirection = 0;", - " if (stickIsUsed) {", - " inputDirection = this.getStickDirection();", - " } else {", - " inputDirection = direction;", - " }", - " const assistanceDirection = this.suggestDirection(", - " inputDirection", - " );", - " if (assistanceDirection !== -1) {", - " if (stickIsUsed) {", - " this.topDownBehavior._stickAngle = assistanceDirection * 45;", - " }", - " return assistanceDirection;", - " }", - " return direction;", - " }", - "", - " /**", - " * Called before the acceleration and new direction is applied to the", - " * velocity.", - " * @param {gdjs.TopDownMovementRuntimeBehavior.TopDownMovementHookContext} context", - " */", - " CornerSlider.prototype.beforeSpeedUpdate = function (context) { }", - "", - " /**", - " * Called before the velocity is applied to the object position and", - " * angle.", - " */", - " CornerSlider.prototype.beforePositionUpdate = function () {", - " if (!this.behavior.activated()) {", - " return;", - " }", - "", - " const object = this.topDownBehavior.owner;", - " this.previousX = object.getX();", - " this.previousY = object.getY();", - " }", - "", - " // TODO Rename this methode to the new hook interface method.", - " CornerSlider.prototype.afterPositionUpdateTODO = function () {", - " if (!this.behavior.activated()) {", - " return;", - " }", - "", - " const object = this.topDownBehavior.owner;", - " const point = temporaryPointForTransformations;", - " point[0] = object.getX() - this.previousX;", - " point[1] = object.getY() - this.previousY;", - " // TODO Handle isometry", - " // if (this.topDownBehavior._basisTransformation) {", - " // this.topDownBehavior._basisTransformation.toWorld(point, point);", - " // }", - " this.shift(point[0], point[1]);", - "", - " this.applyCollision();", - "", - " const position = temporaryPointForTransformations;", - "", - " // TODO Handle isometry", - " // if (this.topDownBehavior._basisTransformation) {", - " // this.topDownBehavior._basisTransformation.toScreen(", - " // this.transformedPosition,", - " // position", - " // );", - " // } else {", - " position[0] = this.transformedPosition[0];", - " position[1] = this.transformedPosition[1];", - " // }", - " object.setX(position[0]);", - " object.setY(position[1]);", - " }", - "", - " CornerSlider.prototype.getStickDirection = function () {", - " let direction =", - " (this.topDownBehavior._stickAngle +", - " this.topDownBehavior._movementAngleOffset) /", - " 45;", - " direction = direction - Math.floor(direction / 8) * 8;", - " for (let strait = 0; strait < 8; strait += 2) {", - " if (strait - 0.125 < direction && direction < strait + 0.125) {", - " direction = strait;", - " }", - " if (strait + 0.125 <= direction && direction <= strait + 2 - 0.125) {", - " direction = strait + 1;", - " }", - " }", - " if (8 - 0.125 < direction) {", - " direction = 0;", - " }", - " return direction;", - " }", - "", - " /** Analyze the real intent of the player instead of applying the input blindly.", - " * @param {integer} direction", - " * @returns {integer} a direction that matches the player intents.", - " */", - " CornerSlider.prototype.suggestDirection = function (direction) {", - " this.needToCheckBypassWay =", - " this.needToCheckBypassWay || direction !== this.lastDirection;", - "", - " if (direction === -1) {", - " return this.noAssistance();", - " }", - "", - " const object = this.topDownBehavior.owner;", - " if (", - " object.getWidth() !== this.oldWidth ||", - " object.getHeight() !== this.oldHeight", - " ) {", - " this.hitBoxesAABBUpToDate = false;", - " this.oldWidth = object.getWidth();", - " this.oldHeight = object.getHeight();", - " }", - "", - " // Compute the list of the objects that will be used", - " const timeDelta = object.getElapsedTime(this.instanceContainer) / 1000;", - " this.updatePotentialCollidingObjects(", - " 1 + this.topDownBehavior.getMaxSpeed() * timeDelta", - " );", - "", - " const downKey = 1 <= direction && direction <= 3;", - " const leftKey = 3 <= direction && direction <= 5;", - " const upKey = 5 <= direction && direction <= 7;", - " const rightKey = direction <= 1 || 7 <= direction;", - "", - " // Used to align the player when the assistance make him bypass an obstacle", - " let stopMinX = Number.MAX_VALUE;", - " let stopMinY = Number.MAX_VALUE;", - " let stopMaxX = -Number.MAX_VALUE;", - " let stopMaxY = -Number.MAX_VALUE;", - " let isBypassX = false;", - " let isBypassY = false;", - "", - " // Incites of how the player should be assisted", - " let assistanceLeft = 0;", - " let assistanceRight = 0;", - " let assistanceUp = 0;", - " let assistanceDown = 0;", - "", - " // the actual decision", - " let assistanceDirection = -1;", - "", - " const objectAABB = this.getHitBoxesAABB();", - " const minX = objectAABB.min[0];", - " const minY = objectAABB.min[1];", - " const maxX = objectAABB.max[0];", - " const maxY = objectAABB.max[1];", - " const width = maxX - minX;", - " const height = maxY - minY;", - "", - " // This affectation has no meaning, it will be override.", - " /** @type {gdjs.AABB | null} */", - " let bypassedObstacleAABB = null;", - "", - " this.collidingObjects.length = 0;", - " this.collidingObjects.push(object);", - "", - " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", - " const obstacleBehavior = this.potentialCollidingObjects[i];", - " const corner = obstacleBehavior.behavior._getSlidingCornerSize();", - " const obstacle = obstacleBehavior.owner;", - " if (obstacle === object) {", - " continue;", - " }", - "", - " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", - " const obstacleMinX = obstacleAABB.min[0];", - " const obstacleMinY = obstacleAABB.min[1];", - " const obstacleMaxX = obstacleAABB.max[0];", - " const obstacleMaxY = obstacleAABB.max[1];", - "", - " const deltaX = deltasX[direction];", - " const deltaY = deltasY[direction];", - " // Extends the box in the player direction", - " if (", - " Math.max(maxX, Math.floor(maxX + deltaX)) > obstacleMinX &&", - " Math.min(minX, Math.ceil(minX + deltaX)) < obstacleMaxX &&", - " Math.max(maxY, Math.floor(maxY + deltaY)) > obstacleMinY &&", - " Math.min(minY, Math.ceil(minY + deltaY)) < obstacleMaxY", - " ) {", - " this.collidingObjects.push(obstacle);", - "", - " // The player is corner to corner to the obstacle.", - " // The assistance will depend on other obstacles.", - " // Both direction are set and the actual to take", - " // is decided at the end.", - " if (", - " almostEquals(maxX, obstacleMinX) &&", - " almostEquals(maxY, obstacleMinY)", - " ) {", - " assistanceRight++;", - " assistanceDown++;", - " } else if (", - " almostEquals(maxX, obstacleMinX) &&", - " almostEquals(minY, obstacleMaxY)", - " ) {", - " assistanceRight++;", - " assistanceUp++;", - " } else if (", - " almostEquals(minX, obstacleMaxX) &&", - " almostEquals(minY, obstacleMaxY)", - " ) {", - " assistanceLeft++;", - " assistanceUp++;", - " } else if (", - " almostEquals(minX, obstacleMaxX) &&", - " almostEquals(maxY, obstacleMinY)", - " ) {", - " assistanceLeft++;", - " assistanceDown++;", - " } else if (", - " (upKey && almostEquals(minY, obstacleMaxY)) ||", - " (downKey && almostEquals(maxY, obstacleMinY))", - " ) {", - " // The player is not on the corner of the obstacle.", - " // Set the assistance both ways to fall back in", - " // the same case as 2 obstacles side by side", - " // being collide with the player.", - " if (", - " (rightKey || maxX > obstacleMinX + corner) &&", - " minX < obstacleMaxX &&", - " (leftKey || minX < obstacleMaxX - corner) &&", - " maxX > obstacleMinX", - " ) {", - " assistanceLeft++;", - " assistanceRight++;", - " }", - " // The player is on the corner of the obstacle.", - " // (not the exact corner, see corner affectation)", - " else if (", - " !rightKey &&", - " obstacleMinX < maxX &&", - " maxX <= obstacleMinX + corner &&", - " // In case the cornerSize is bigger than the obstacle size,", - " // go the on the shortest side.", - " (leftKey || minX + maxX <= obstacleMinX + obstacleMaxX)", - " ) {", - " assistanceLeft++;", - " isBypassX = true;", - " if (obstacleMinX - width < stopMinX) {", - " stopMinX = obstacleMinX - width;", - " bypassedObstacleAABB = obstacleAABB;", - " }", - " } else if (", - " !leftKey &&", - " obstacleMaxX - corner <= minX &&", - " minX < obstacleMaxX &&", - " (rightKey || minX + maxX > obstacleMinX + obstacleMaxX)", - " ) {", - " assistanceRight++;", - " isBypassX = true;", - " if (obstacleMaxX > stopMaxX) {", - " stopMaxX = obstacleMaxX;", - " bypassedObstacleAABB = obstacleAABB;", - " }", - " }", - " } else if (", - " (leftKey && almostEquals(minX, obstacleMaxX)) ||", - " (rightKey && almostEquals(maxX, obstacleMinX))", - " ) {", - " // The player is not on the corner of the obstacle.", - " // Set the assistance both ways to fall back in", - " // the same case as 2 obstacles side by side", - " // being collide with the player.", - " if (", - " (downKey || maxY > obstacleMinY + corner) &&", - " minY < obstacleMaxY &&", - " (upKey || minY < obstacleMaxY - corner) &&", - " maxY > obstacleMinY", - " ) {", - " assistanceUp++;", - " assistanceDown++;", - " }", - " // The player is on the corner of the obstacle.", - " // (not the exact corner, see corner affectation)", - " else if (", - " !downKey &&", - " obstacleMinY < maxY &&", - " maxY <= obstacleMinY + corner &&", - " (upKey || minY + maxY <= obstacleMinY + obstacleMaxY)", - " ) {", - " assistanceUp++;", - " isBypassY = true;", - " if (obstacleMinY - height < stopMinY) {", - " stopMinY = obstacleMinY - height;", - " bypassedObstacleAABB = obstacleAABB;", - " }", - " } else if (", - " !upKey &&", - " obstacleMaxY - corner <= minY &&", - " minY < obstacleMaxY &&", - " (downKey || minY + maxY > obstacleMinY + obstacleMaxY)", - " ) {", - " assistanceDown++;", - " isBypassY = true;", - " if (obstacleMaxY > stopMaxY) {", - " stopMaxY = obstacleMaxY;", - " bypassedObstacleAABB = obstacleAABB;", - " }", - " }", - " }", - " }", - " }", - "", - " // This may happen when the player is in the corner of 2 perpendicular walls.", - " // No assistance is needed.", - " if (", - " assistanceLeft &&", - " assistanceRight &&", - " assistanceUp &&", - " assistanceDown", - " ) {", - " return this.noAssistance();", - " }", - " // This may happen when the player goes in diagonal against a wall.", - " // Make him follow the wall. This allows player to keep full speed.", - " //", - " // When he collided a square from the wall corner to corner,", - " // a 3rd assistance may be true but it fall back in the same case.", - " else if (assistanceLeft && assistanceRight) {", - " isBypassX = false;", - " if (leftKey && !rightKey) {", - " assistanceDirection = 4;", - " } else if (rightKey && !leftKey) {", - " assistanceDirection = 0;", - " } else {", - " // Contradictory decisions are dismissed.", - " //", - " // This can happen, for instance, with a wall composed of squares.", - " // Taken separately from one to another, a square could be bypass one the right", - " // and the next one on the left even though they are side by side", - " // and the player can't actually go between them.", - " return this.noAssistance();", - " }", - " } else if (assistanceUp && assistanceDown) {", - " isBypassY = false;", - " if (upKey && !downKey) {", - " assistanceDirection = 6;", - " } else if (downKey && !upKey) {", - " assistanceDirection = 2;", - " } else {", - " // see previous comment", - " return this.noAssistance();", - " }", - " }", - " // The player goes in diagonal and is corner to corner with the obstacle.", - " // (but not against a wall, this time)", - " // The velocity is used to decide.", - " // This may only happen after an alignment.", - " // (see \"Alignment:\" comment)", - " else if (assistanceRight && assistanceDown) {", - " if (", - " (downKey && !rightKey) ||", - " (downKey === rightKey && assistanceDown > assistanceRight) ||", - " (assistanceDown === assistanceRight &&", - " this.topDownBehavior._yVelocity > 0 &&", - " Math.abs(this.topDownBehavior._xVelocity) <", - " Math.abs(this.topDownBehavior._yVelocity))", - " ) {", - " assistanceDirection = 2;", - " } else {", - " assistanceDirection = 0;", - " }", - " } else if (assistanceLeft && assistanceDown) {", - " if (", - " (downKey && !leftKey) ||", - " (downKey === leftKey && assistanceDown > assistanceLeft) ||", - " (assistanceDown === assistanceLeft &&", - " this.topDownBehavior._yVelocity > 0 &&", - " Math.abs(this.topDownBehavior._xVelocity) <", - " Math.abs(this.topDownBehavior._yVelocity))", - " ) {", - " assistanceDirection = 2;", - " } else {", - " assistanceDirection = 4;", - " }", - " } else if (assistanceLeft && assistanceUp) {", - " if (", - " (upKey && !leftKey) ||", - " (upKey === leftKey && assistanceUp > assistanceLeft) ||", - " (assistanceUp === assistanceLeft &&", - " this.topDownBehavior._yVelocity < 0 &&", - " Math.abs(this.topDownBehavior._xVelocity) <", - " Math.abs(this.topDownBehavior._yVelocity))", - " ) {", - " assistanceDirection = 6;", - " } else {", - " assistanceDirection = 4;", - " }", - " } else if (assistanceRight && assistanceUp) {", - " if (", - " (upKey && !rightKey) ||", - " (upKey === rightKey && assistanceUp > assistanceRight) ||", - " (assistanceUp === assistanceRight &&", - " this.topDownBehavior._yVelocity < 0 &&", - " Math.abs(this.topDownBehavior._xVelocity) <", - " Math.abs(this.topDownBehavior._yVelocity))", - " ) {", - " assistanceDirection = 6;", - " } else {", - " assistanceDirection = 0;", - " }", - " } else {", - " // Slide on the corner of an obstacle to bypass it.", - " // Every tricky cases are already handled .", - " if (assistanceLeft) {", - " assistanceDirection = 4;", - " } else if (assistanceRight) {", - " assistanceDirection = 0;", - " } else if (assistanceUp) {", - " assistanceDirection = 6;", - " } else if (assistanceDown) {", - " assistanceDirection = 2;", - " } else {", - " return this.noAssistance();", - " }", - " }", - "", - " // Check if there is any obstacle in the way.", - " //", - " // There must be no obstacle to go at least", - " // as far in the direction the player chose", - " // as the assistance must take to align the player.", - " //", - " // Because, if the assistance moves the player by 32 pixels", - " // before been able to go in the right direction", - " // and can only move by 4 pixels afterward", - " // that it'll sound silly.", - " this.needToCheckBypassWay =", - " this.needToCheckBypassWay ||", - " assistanceDirection !== this.lastAssistanceDirection;", - " if ((isBypassX || isBypassY) && !this.needToCheckBypassWay) {", - " // Don't check again if the player intent stays the same.", - " //", - " // Do it, for instance, if an obstacle has moved out of the way", - " // and the player releases and presses agin the key.", - " // Because, doing it automatically would seems weird.", - " if (this.lastAnyObstacle) {", - " return this.noAssistance();", - " }", - " } else if (isBypassX || isBypassY) {", - " this.lastAssistanceDirection = assistanceDirection;", - " this.lastDirection = direction;", - "", - " let anyObstacle = false;", - " // reflection symmetry: y = x", - " // 0 to 6, 2 to 4, 4 to 2, 6 to 0", - " if (direction + assistanceDirection === 6) {", - " // Because the obstacle may not be a square.", - " let cornerX = 0;", - " let cornerY = 0;", - " if (assistanceDirection === 4 || assistanceDirection === 6) {", - " cornerX = bypassedObstacleAABB.min[0];", - " cornerY = bypassedObstacleAABB.min[1];", - " } else {", - " cornerX = bypassedObstacleAABB.max[0];", - " cornerY = bypassedObstacleAABB.max[1];", - " }", - " // / cornerX \\ / 0 1 \\ / x - cornerX \\", - " // \\ cornerY / + \\ 1 0 / * \\ y - cornerY /", - " //", - " // min and max are preserved by the symmetry.", - " // The symmetry image is extended to check there is no obstacle before going into the passage.", - " const searchMinX =", - " cornerX +", - " minY -", - " cornerY +", - " epsilon +", - " (assistanceDirection === 6 ? cornerY - maxY : 0);", - " const searchMaxX =", - " cornerX +", - " maxY -", - " cornerY -", - " epsilon +", - " (assistanceDirection === 2 ? cornerY - minY : 0);", - " const searchMinY =", - " cornerY +", - " minX -", - " cornerX +", - " epsilon +", - " (assistanceDirection === 4 ? cornerX - maxX : 0);", - " const searchMaxY =", - " cornerY +", - " maxX -", - " cornerX -", - " epsilon +", - " (assistanceDirection === 0 ? cornerX - minX : 0);", - "", - " anyObstacle = this.obstacleManager.anyObstacle(", - " searchMinX,", - " searchMaxX,", - " searchMinY,", - " searchMaxY,", - " this.collidingObjects", - " );", - " }", - " // reflection symmetry: y = -x", - " // 0 to 2, 2 to 0, 4 to 6, 6 to 4", - " else if ((direction + assistanceDirection) % 8 === 2) {", - " // Because the obstacle may not be a square.", - " let cornerX = 0;", - " let cornerY = 0;", - " if (assistanceDirection === 2 || assistanceDirection === 4) {", - " cornerX = bypassedObstacleAABB.min[0];", - " cornerY = bypassedObstacleAABB.max[1];", - " } else {", - " cornerX = bypassedObstacleAABB.max[0];", - " cornerY = bypassedObstacleAABB.min[1];", - " }", - " // / cornerX \\ / 0 -1 \\ / x - cornerX \\", - " // \\ cornerY / + \\ -1 0 / * \\ y - cornerY /", - " //", - " // min and max are switched by the symmetry.", - " // The symmetry image is extended to check there is no obstacle before going into the passage.", - " const searchMinX =", - " cornerX -", - " (maxY - cornerY) +", - " epsilon +", - " (assistanceDirection === 2 ? minY - cornerY : 0);", - " const searchMaxX =", - " cornerX -", - " (minY - cornerY) -", - " epsilon +", - " (assistanceDirection === 6 ? maxY - cornerY : 0);", - " const searchMinY =", - " cornerY -", - " (maxX - cornerX) +", - " epsilon +", - " (assistanceDirection === 0 ? minX - cornerX : 0);", - " const searchMaxY =", - " cornerY -", - " (minX - cornerX) -", - " epsilon +", - " (assistanceDirection === 4 ? maxX - cornerX : 0);", - "", - " anyObstacle = this.obstacleManager.anyObstacle(", - " searchMinX,", - " searchMaxX,", - " searchMinY,", - " searchMaxY,", - " this.collidingObjects", - " );", - " }", - " this.lastAnyObstacle = anyObstacle;", - " this.needToCheckBypassWay = false;", - "", - " if (anyObstacle) {", - " return this.noAssistance();", - " }", - " }", - "", - " this.result.inputDirection = direction;", - " this.result.assistanceLeft = assistanceLeft > 0;", - " this.result.assistanceRight = assistanceRight > 0;", - " this.result.assistanceUp = assistanceUp > 0;", - " this.result.assistanceDown = assistanceDown > 0;", - " this.result.isBypassX = isBypassX;", - " this.result.isBypassY = isBypassY;", - " this.result.stopMinX = stopMinX;", - " this.result.stopMinY = stopMinY;", - " this.result.stopMaxX = stopMaxX;", - " this.result.stopMaxY = stopMaxY;", - "", - " return assistanceDirection;", - " }", - "", - " /**", - " * @return {integer}", - " */", - " CornerSlider.prototype.noAssistance = function() {", - " this.result.isBypassX = false;", - " this.result.isBypassY = false;", - "", - " return -1;", - " }", - "", - " CornerSlider.prototype.applyCollision = function() {", - " this.checkCornerStop();", - " this.separateFromObstacles();", - " // check again because the object can be pushed on the stop limit,", - " // it won't be detected on the next frame and the alignment won't be applied.", - " this.checkCornerStop();", - " }", - "", - " /**", - " * Check if the object must take a corner.", - " *", - " * When the object reach the limit of an obstacle", - " * and it should take the corner according to the player intent,", - " * it is aligned right on this limit and the velocity is set in the right direction.", - " *", - " * This avoid issues with the inertia. For instance,", - " * when the object could go between 2 obstacles,", - " * with it will just fly over the hole because of its inertia.", - " */", - " CornerSlider.prototype.checkCornerStop = function() {", - " const objectAABB = this.getHitBoxesAABB();", - " const minX = objectAABB.min[0];", - " const minY = objectAABB.min[1];", - " const object = this.topDownBehavior.owner;", - "", - " const direction = this.result.inputDirection;", - " const leftKey = 3 <= direction && direction <= 5;", - " const upKey = 5 <= direction && direction <= 7;", - "", - " // Alignment: avoid to go too far and kind of drift or oscillate in front of a hole.", - " if (", - " this.result.isBypassX &&", - " ((this.result.assistanceLeft && minX <= this.result.stopMinX) ||", - " (this.result.assistanceRight && minX >= this.result.stopMaxX))", - " ) {", - " this.shift(", - " -minX +", - " (this.result.assistanceLeft", - " ? this.result.stopMinX", - " : this.result.stopMaxX),", - " 0", - " );", - " this.topDownBehavior._yVelocity =", - " (upKey ? -1 : 1) *", - " Math.sqrt(", - " this.topDownBehavior._xVelocity *", - " this.topDownBehavior._xVelocity +", - " this.topDownBehavior._yVelocity *", - " this.topDownBehavior._yVelocity", - " );", - " this.topDownBehavior._xVelocity = 0;", - " }", - " if (", - " this.result.isBypassY &&", - " ((this.result.assistanceUp && minY <= this.result.stopMinY) ||", - " (this.result.assistanceDown && minY >= this.result.stopMaxY))", - " ) {", - " this.shift(", - " 0,", - " -minY +", - " (this.result.assistanceUp", - " ? this.result.stopMinY", - " : this.result.stopMaxY)", - " );", - " this.topDownBehavior._xVelocity =", - " (leftKey ? -1 : 1) *", - " Math.sqrt(", - " this.topDownBehavior._xVelocity *", - " this.topDownBehavior._xVelocity +", - " this.topDownBehavior._yVelocity *", - " this.topDownBehavior._yVelocity", - " );", - " this.topDownBehavior._yVelocity = 0;", - " }", - " }", - "", - " /**", - " * Separate from TopDownObstacleRuntimeBehavior instances.", - " */", - " CornerSlider.prototype.separateFromObstacles = function() {", - " const object = this.topDownBehavior.owner;", - " const objectAABB = this.getHitBoxesAABB();", - " const minX = objectAABB.min[0];", - " const minY = objectAABB.min[1];", - " const maxX = objectAABB.max[0];", - " const maxY = objectAABB.max[1];", - "", - " // Search the obstacle with the biggest intersection", - " // to separate from this one first.", - " // Because smaller collisions may shift the player", - " // in the wrong direction.", - " let maxSurface = 0;", - " /** @type {gdjs.RuntimeBehavior | null} */", - " let bestObstacleBehavior = null;", - " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", - " const obstacleBehavior = this.potentialCollidingObjects[i];", - " if (obstacleBehavior.behavior.owner === object) {", - " continue;", - " }", - "", - " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", - " const obstacleMinX = obstacleAABB.min[0];", - " const obstacleMinY = obstacleAABB.min[1];", - " const obstacleMaxX = obstacleAABB.max[0];", - " const obstacleMaxY = obstacleAABB.max[1];", - "", - " const interMinX = Math.max(minX, obstacleMinX);", - " const interMinY = Math.max(minY, obstacleMinY);", - " const interMaxX = Math.min(maxX, obstacleMaxX);", - " const interMaxY = Math.min(maxY, obstacleMaxY);", - "", - " if (interMinX < interMaxX && interMinY < interMaxY) {", - " const surface = (interMaxX - interMinX) * (interMaxY - interMinY);", - " if (surface > maxSurface) {", - " maxSurface = surface;", - " bestObstacleBehavior = obstacleBehavior;", - " }", - " }", - " }", - " if (bestObstacleBehavior !== null) {", - " this.separateFrom(bestObstacleBehavior);", - " }", - " for (var i = 0; i < this.potentialCollidingObjects.length; ++i) {", - " const obstacleBehavior = this.potentialCollidingObjects[i];", - " const obstacle = obstacleBehavior.behavior.owner;", - " if (obstacle === object) {", - " continue;", - " }", - " this.separateFrom(obstacleBehavior);", - " }", - " }", - "", - " /**", - " * Separate object and obstacle, only object move.", - " * @param {Obstacle} obstacleBehavior", - " */", - " CornerSlider.prototype.separateFrom = function(obstacleBehavior) {", - " const objectAABB = this.getHitBoxesAABB();", - " const minX = objectAABB.min[0];", - " const minY = objectAABB.min[1];", - " const maxX = objectAABB.max[0];", - " const maxY = objectAABB.max[1];", - "", - " const obstacleAABB = obstacleBehavior.getHitBoxesAABB();", - " const obstacleMinX = obstacleAABB.min[0];", - " const obstacleMinY = obstacleAABB.min[1];", - " const obstacleMaxX = obstacleAABB.max[0];", - " const obstacleMaxY = obstacleAABB.max[1];", - "", - " const leftDistance = maxX - obstacleMinX;", - " const upDistance = maxY - obstacleMinY;", - " const rightDistance = obstacleMaxX - minX;", - " const downDistance = obstacleMaxY - minY;", - " const minDistance = Math.min(", - " leftDistance,", - " upDistance,", - " rightDistance,", - " downDistance", - " );", - "", - " if (minDistance > 0) {", - " if (leftDistance === minDistance) {", - " this.shift(-maxX + obstacleMinX, 0);", - " } else if (rightDistance === minDistance) {", - " this.shift(-minX + obstacleMaxX, 0);", - " } else if (upDistance === minDistance) {", - " this.shift(0, -maxY + obstacleMinY);", - " } else if (downDistance === minDistance) {", - " this.shift(0, -minY + obstacleMaxY);", - " }", - " }", - " }", - "", - "/**", - " * @param {float} deltaX", - " * @param {float} deltaY", - " */", - " CornerSlider.prototype.shift = function (deltaX, deltaY) {", - " this.transformedPosition[0] += deltaX;", - " this.transformedPosition[1] += deltaY;", - " }", - "", - " /**", - " * @return {gdjs.AABB}", - " */", - " CornerSlider.prototype.getHitBoxesAABB = function() {", - " if (!this.hitBoxesAABBUpToDate) {", - " const hitBoxes = this.topDownBehavior.owner.getHitBoxes();", - "", - " let minX = Number.MAX_VALUE;", - " let minY = Number.MAX_VALUE;", - " let maxX = -Number.MAX_VALUE;", - " let maxY = -Number.MAX_VALUE;", - " for (let h = 0, lenh = hitBoxes.length; h < lenh; ++h) {", - " let hitBox = hitBoxes[h];", - " for (let p = 0, lenp = hitBox.vertices.length; p < lenp; ++p) {", - " const point = this.topDownBehavior", - " ._temporaryPointForTransformations;", - " // TODO Handle isometry", - " // if (this.topDownBehavior._basisTransformation) {", - " // this.topDownBehavior._basisTransformation.toWorld(", - " // hitBox.vertices[p],", - " // point", - " // );", - " // } else {", - " point[0] = hitBox.vertices[p][0];", - " point[1] = hitBox.vertices[p][1];", - " // }", - " minX = Math.min(minX, point[0]);", - " maxX = Math.max(maxX, point[0]);", - " minY = Math.min(minY, point[1]);", - " maxY = Math.max(maxY, point[1]);", - " }", - " }", - " this.relativeHitBoxesAABB.min[0] = minX - this.transformedPosition[0];", - " this.relativeHitBoxesAABB.min[1] = minY - this.transformedPosition[1];", - " this.relativeHitBoxesAABB.max[0] = maxX - this.transformedPosition[0];", - " this.relativeHitBoxesAABB.max[1] = maxY - this.transformedPosition[1];", - "", - " this.hitBoxesAABBUpToDate = true;", - " }", - " this.absoluteHitBoxesAABB.min[0] =", - " this.relativeHitBoxesAABB.min[0] + this.transformedPosition[0];", - " this.absoluteHitBoxesAABB.min[1] =", - " this.relativeHitBoxesAABB.min[1] + this.transformedPosition[1];", - " this.absoluteHitBoxesAABB.max[0] =", - " this.relativeHitBoxesAABB.max[0] + this.transformedPosition[0];", - " this.absoluteHitBoxesAABB.max[1] =", - " this.relativeHitBoxesAABB.max[1] + this.transformedPosition[1];", - " return this.absoluteHitBoxesAABB;", - " }", - "", - " /**", - " * Update _potentialCollidingObjects member with platforms near the object.", - " * @param {float} maxMovementLength", - " */", - " CornerSlider.prototype.updatePotentialCollidingObjects = function(maxMovementLength) {", - " this.obstacleManager.getAllObstaclesAround(", - " this.getHitBoxesAABB(),", - " maxMovementLength,", - " this.potentialCollidingObjects", - " );", - " }", - "", - " return CornerSlider;", - "}());", - "", - "", - "/**", - " * TopDownMovementRuntimeBehavior represents a behavior allowing objects to", - " * follow a path computed to avoid obstacles.", - " */", - "const AssistanceResult = /** @class */ (function () {", - " function AssistanceResult() {", - " this.inputDirection = -1;", - " this.assistanceLeft = false;", - " this.assistanceRight = false;", - " this.assistanceUp = false;", - " this.assistanceDown = false;", - " this.isBypassX = false;", - " this.isBypassY = false;", - " this.stopMinX = 0;", - " this.stopMinY = 0;", - " this.stopMaxX = 0;", - " this.stopMaxY = 0;", - " }", - "", - " return AssistanceResult;", - "}());", - "", - " /**", - " * Allow to store a behavior in a RBush (spatial data structure).", - " * Because this duplicates the AABB, this ensures the RBush AABB", - " * stays the same even if the underlying object is moved", - " * (in which case the behavior is responsible for removing/adding", - " * back/updating this BehaviorRBushAABB).", - " */", - " const ObstacleRBushAABB = /** @class */ (function () {", - " /**", - " * @param {Obstacle} obstacle", - " */", - " function ObstacleRBushAABB(obstacle) {", - " this.obstacle = obstacle;", - " this.minX = 0;", - " this.minY = 0;", - " this.maxX = 0;", - " this.maxY = 0;", - " this.updateAABBFromOwner();", - " }", - "", - " ObstacleRBushAABB.prototype.updateAABBFromOwner = function () {", - " const aabb = this.obstacle.getHitBoxesAABB();", - " this.minX = aabb.min[0];", - " this.minY = aabb.min[1];", - " this.maxX = aabb.max[0];", - " this.maxY = aabb.max[1];", - " }", - "", - " return ObstacleRBushAABB;", - "}());", - "", - "/**", - " * An obstacle manager for fast spacial search.", - " */", - "const ObstacleManager = /** @class */ (function () {", - " /**", - " * Create a manager.", - " */", - " function ObstacleManager() {", - " this.obstacleRBush = new rbush();", - " }", - "", - " /**", - " * Get the obstacle manager of an instance container.", - " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", - " * @return {ObstacleManager}", - " */", - " ObstacleManager.getManager = function (instanceContainer) {", - " if (!instanceContainer.__topDownCornerSlidingExtension) {", - " instanceContainer.__topDownCornerSlidingExtension = {};", - " }", - " if (!instanceContainer.__topDownCornerSlidingExtension.manager) {", - " // Create the shared manager if necessary.", - " instanceContainer.__topDownCornerSlidingExtension.manager = new ObstacleManager();", - " }", - " return instanceContainer.__topDownCornerSlidingExtension.manager;", - " };", - "", - " /**", - " * Register an obstacle.", - " * @param {Obstacle} obstacle", - " */", - " ObstacleManager.prototype.addObstacle = function (obstacle) {", - " obstacle.currentRBushAABB.updateAABBFromOwner();", - " this.obstacleRBush.insert(obstacle.currentRBushAABB);", - " }", - "", - " /**", - " * Unregister an obstacle.", - " * @param {Obstacle} obstacle", - " */", - " ObstacleManager.prototype.removeObstacle = function (obstacle) {", - " this.obstacleRBush.remove(obstacle.currentRBushAABB);", - " }", - "", - " /**", - " * Returns all the boids around the specified boid.", - " * @param {gdjs.AABB} aabb", - " * @param {number} maxMovementLength", - " * @param {Obstacle[]} result the returned array.", - " * @return {Obstacle[]} An array with all obstacles near the object.", - " */", - " ObstacleManager.prototype.getAllObstaclesAround = function (aabb, maxMovementLength, result) {", - " const searchArea = gdjs.staticObject(", - " ObstacleManager.prototype.getAllObstaclesAround", - " );", - " searchArea.minX = aabb.min[0] - maxMovementLength;", - " searchArea.minY = aabb.min[1] - maxMovementLength;", - " searchArea.maxX = aabb.max[0] + maxMovementLength;", - " searchArea.maxY = aabb.max[1] + maxMovementLength;", - " /** @type {ObstacleRBushAABB[]} */", - " const nearbyObstacles = this.obstacleRBush.search(searchArea);", - "", - " result.length = 0;", - " for (let i = 0; i < nearbyObstacles.length; i++) {", - " const obstacle = nearbyObstacles[i].obstacle;", - " result.push(obstacle);", - " }", - "", - " return result;", - " }", - "", - " /**", - " * Returns true if there is any obstacle intersecting the area.", - " * @param {number} minX", - " * @param {number} maxX", - " * @param {number} minY", - " * @param {number} maxY", - " * @param {gdjs.RuntimeObject[]} excluded", - " * @return {boolean}", - " */", - " ObstacleManager.prototype.anyObstacle = function (", - " minX,", - " maxX,", - " minY,", - " maxY,", - " excluded", - " ) {", - " const searchArea = gdjs.staticObject(", - " ObstacleManager.prototype.anyObstacle", - " );", - " // @ts-ignore", - " searchArea.minX = minX;", - " // @ts-ignore", - " searchArea.minY = minY;", - " // @ts-ignore", - " searchArea.maxX = maxX;", - " // @ts-ignore", - " searchArea.maxY = maxY;", - " /** @type {ObstacleRBushAABB[]} */", - " const nearbyObstacles = this.obstacleRBush.search(searchArea);", - "", - " for (let i = 0; i < nearbyObstacles.length; i++) {", - " if (!excluded.includes(nearbyObstacles[i].obstacle.behavior.owner)) {", - " return true;", - " }", - " }", - " return false;", - " }", - "", - " return ObstacleManager;", - "}());", - "", - "/**", - " * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be", - " * considered as a obstacle by objects having Pathfinding Behavior.", - " */", - "const Obstacle = /** @class */ (function () {", - " /**", - " * ", - " * @param {gdjs.RuntimeInstanceContainer} instanceContainer", - " * @param {gdjs.RuntimeBehavior} behavior", - " */", - " function Obstacle(instanceContainer, behavior) {", - " this.oldX = 0;", - " this.oldY = 0;", - " this.oldWidth = 0;", - " this.oldHeight = 0;", - " this.registeredInManager = false;", - " this.behavior = behavior;", - " this.manager = ObstacleManager.getManager(instanceContainer);", - " // Note that we can't use getX(), getWidth()... of owner here:", - " // The owner is not yet fully constructed.", - " /** @type {gdjs.AABB} */", - " this.hitBoxesAABB = { min: [0, 0], max: [0, 0] };", - " this.hitBoxesAABBUpToDate = false;", - " ", - " /** @type {FloatPoint} */", - " this.point = [0, 0];", - " this.currentRBushAABB = new ObstacleRBushAABB(this);", - " }", - " Obstacle.prototype.onCreated = function () {", - " this.manager.addObstacle(this);", - " this.registeredInManager = true;", - " };", - " Obstacle.prototype.onDestroy = function () {", - " if (this.manager && this.registeredInManager) {", - " this.manager.removeObstacle(this);", - " }", - " };", - " Obstacle.prototype.doStepPreEvents = function (instanceContainer) {", - " var owner = this.behavior.owner;", - " // Make sure the obstacle is or is not in the obstacles manager.", - " if (!this.behavior.activated() && this.registeredInManager) {", - " this.manager.removeObstacle(this);", - " this.registeredInManager = false;", - " }", - " else {", - " if (this.behavior.activated() && !this.registeredInManager) {", - " this.manager.addObstacle(this);", - " this.registeredInManager = true;", - " }", - " }", - " // Track changes in size or position", - " if (this.oldX !== owner.getX() ||", - " this.oldY !== owner.getY() ||", - " this.oldWidth !== owner.getWidth() ||", - " this.oldHeight !== owner.getHeight()) {", - " this.hitBoxesAABBUpToDate = false;", - " if (this.registeredInManager) {", - " this.manager.removeObstacle(this);", - " this.manager.addObstacle(this);", - " }", - " this.oldX = owner.getX();", - " this.oldY = owner.getY();", - " this.oldWidth = owner.getWidth();", - " this.oldHeight = owner.getHeight();", - " }", - " };", - " Obstacle.prototype.doStepPostEvents = function (instanceContainer) { };", - " Obstacle.prototype.onActivate = function () {", - " if (this.registeredInManager) {", - " return;", - " }", - " this.manager.addObstacle(this);", - " this.registeredInManager = true;", - " };", - " Obstacle.prototype.onDeActivate = function () {", - " if (!this.registeredInManager) {", - " return;", - " }", - " this.manager.removeObstacle(this);", - " this.registeredInManager = false;", - " };", - "", - " /**", - " * @return {gdjs.AABB}", - " */", - " Obstacle.prototype.getHitBoxesAABB = function() {", - " if (!this.hitBoxesAABBUpToDate) {", - " const hitBoxes = this.behavior.owner.getHitBoxes();", - "", - " let minX = Number.MAX_VALUE;", - " let minY = Number.MAX_VALUE;", - " let maxX = -Number.MAX_VALUE;", - " let maxY = -Number.MAX_VALUE;", - " for (let h = 0, lenh = hitBoxes.length; h < lenh; ++h) {", - " let hitBox = hitBoxes[h];", - " for (let p = 0, lenp = hitBox.vertices.length; p < lenp; ++p) {", - " const point = this.point;", - " // TODO Handle Isometry", - " //this._basisTransformation.toWorld(hitBox.vertices[p], point);", - " point[0] = hitBox.vertices[p][0];", - " point[1] = hitBox.vertices[p][1];", - "", - " minX = Math.min(minX, point[0]);", - " maxX = Math.max(maxX, point[0]);", - " minY = Math.min(minY, point[1]);", - " maxY = Math.max(maxY, point[1]);", - " }", - " }", - " this.hitBoxesAABB.min[0] = minX;", - " this.hitBoxesAABB.min[1] = minY;", - " this.hitBoxesAABB.max[0] = maxX;", - " this.hitBoxesAABB.max[1] = maxY;", - "", - " this.hitBoxesAABBUpToDate = true;", - " }", - " return this.hitBoxesAABB;", - " }", - "", - " return Obstacle;", - "}());", - "", - "gdjs.__topDownCornerSlidingExtension = gdjs.__topDownCornerSlidingExtension || {};", - "gdjs.__topDownCornerSlidingExtension.CornerSlider = CornerSlider;", - "gdjs.__topDownCornerSlidingExtension.Obstacle = Obstacle;", - "", - "" - ], - "parameterObjects": "", - "useStrict": true, - "eventsSheetExpanded": true - } - ] + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true } ], "parameters": [], @@ -1350,8 +1321,9 @@ "/** @type {gdjs.TopDownMovementRuntimeBehavior} */\r", "const topDownBehavior = object.getBehavior(behavior._getTopDownMovement());\r", "\r", - "behavior.__topDownCornerSlidingExtension = behavior.__topDownCornerSlidingExtension || {};\r", - "behavior.__topDownCornerSlidingExtension.cornerSlider = new gdjs.__topDownCornerSlidingExtension.CornerSlider(runtimeScene, behavior, topDownBehavior);\r", + "behavior.__topDownCornerSlidingExtension = {\r", + " cornerSlider: new gdjs.__topDownCornerSlidingExtension.CornerSlider(runtimeScene, behavior, topDownBehavior)\r", + "};\r", "" ], "parameterObjects": "Object", @@ -1436,10 +1408,9 @@ "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r", "const behavior = object.getBehavior(behaviorName);\r", "\r", - "behavior.__topDownCornerSlidingExtension = behavior.__topDownCornerSlidingExtension || {};\r", - "behavior.__topDownCornerSlidingExtension.obstacle =\r", - " new gdjs.__topDownCornerSlidingExtension.Obstacle(runtimeScene, behavior);\r", - "behavior.__topDownCornerSlidingExtension.obstacle.onCreated(runtimeScene, behavior);\r", + "const obstacle = new gdjs.__topDownCornerSlidingExtension.Obstacle(runtimeScene, behavior)\r", + "behavior.__topDownCornerSlidingExtension = { obstacle };\r", + "obstacle.onCreated(runtimeScene, behavior);\r", "" ], "parameterObjects": "Object",