import Vector from '../../math/Vector';
import Location from '../../utils/Location';
import Steerable from '../Steerable';
import SteeringAcceleration from '../SteeringAcceleration';
import Limiter from '../Limiter';
import Arrive from './Arrive';
import Path, { PathParam } from '../utils/Path';
/**
* {@code FollowPath} behavior produces a linear acceleration that moves the agent along the given path. First it calculates the
* agent location based on the specified prediction time. Then it works out the position of the internal target based on the
* location just calculated and the shape of the path. It finally uses {@link Seek seek} behavior to move the owner towards the
* internal target position. However, if the path is open {@link Arrive arrive} behavior is used to approach path's extremities
* when they are far less than the {@link FollowPath#decelerationRadius deceleration radius} from the internal target position.
*
* For complex paths with sudden changes of direction the predictive behavior (i.e., with prediction time greater than 0) can
* appear smoother than the non-predictive one (i.e., with no prediction time). However, predictive path following has the
* downside of cutting corners when some sections of the path come close together. This cutting-corner attitude can make the
* character miss a whole section of the path. This might not be what you want if, for example, the path represents a patrol
* route.
*
* @param Type of vector, either 2D or 3D, implementing the {@link Vector} interface
* @param Type of path parameter implementing the {@link PathParam} interface
*
* @author davebaol
*/
class FollowPath, P extends PathParam> extends Arrive {
/** The path to follow */
protected path: Path;
/** The distance along the path to generate the target. Can be negative if the owner has to move along the reverse direction. */
protected pathOffset: number;
/** The current position on the path */
protected pathParam: P;
/** The flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. It defaults to {@code true}. */
protected arriveEnabled: boolean;
/** The time in the future to predict the owner's position. Set it to 0 for non-predictive path following. */
protected predictionTime: number;
private internalTargetPosition: T;
/**
* Creates a {@code FollowPath} behavior for the specified owner, path, path offset, maximum linear acceleration and prediction
* time.
* @param owner the owner of this behavior
* @param path the path to be followed by the owner
* @param pathOffset the distance along the path to generate the target. Can be negative if the owner is to move along the
* reverse direction.
* @param predictionTime the time in the future to predict the owner's position. Can be 0 for non-predictive path following.
*/
constructor(owner: Steerable , path: Path, pathOffset = 0, predictionTime = 0) {
super(owner);
this.path = path;
this.pathParam = path.createParam();
this.pathOffset = pathOffset;
this.predictionTime = predictionTime;
this.arriveEnabled = true;
this.internalTargetPosition = this.newVector(owner);
}
/** Returns the path to follow */
public getPath(): Path {
return this.path;
}
/**
* Sets the path followed by this behavior.
* @param path the path to set
* @return this behavior for chaining.
*/
public setPath(path: Path): FollowPath {
this.path = path;
return this;
}
/** Returns the path offset. */
public getPathOffset(): number {
return this.pathOffset;
}
/** Returns the flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. */
public isArriveEnabled(): boolean {
return this.arriveEnabled;
}
/** Returns the prediction time. */
public getPredictionTime(): number {
return this.predictionTime;
}
/**
* Sets the prediction time. Set it to 0 for non-predictive path following.
* @param predictionTime the predictionTime to set
* @return this behavior for chaining.
*/
public setPredictionTime(predictionTime: number): FollowPath {
this.predictionTime = predictionTime;
return this;
}
/**
* Sets the flag indicating whether to use {@link Arrive} behavior to approach the end of an open path. It defaults to
* {@code true}.
* @param arriveEnabled the flag value to set
* @return this behavior for chaining.
*/
public setArriveEnabled(arriveEnabled: boolean): FollowPath {
this.arriveEnabled = arriveEnabled;
return this;
}
/**
* Sets the path offset to generate the target. Can be negative if the owner has to move along the reverse direction.
* @param pathOffset the pathOffset to set
* @return this behavior for chaining.
*/
public setPathOffset(pathOffset: number): FollowPath {
this.pathOffset = pathOffset;
return this;
}
/** Returns the current path parameter. */
public getPathParam(): P {
return this.pathParam;
}
/** Returns the current position of the internal target. This method is useful for debug purpose. */
public getInternalTargetPosition(): T {
return this.internalTargetPosition;
}
//
// Setters overridden in order to fix the correct return type for chaining
//
public setOwner(owner: Steerable): FollowPath {
this.owner = owner;
return this;
}
public setEnabled(enabled: boolean): FollowPath {
this.enabled = enabled;
return this;
}
/**
* Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear speed and
* acceleration. However the maximum linear speed is not required for a closed path.
* @return this behavior for chaining.
*/
public setLimiter(limiter: Limiter): FollowPath {
this.limiter = limiter;
return this;
}
public setTarget (target: Location): FollowPath {
this.target = target;
return this;
}
public setArrivalTolerance(arrivalTolerance: number): FollowPath {
this.arrivalTolerance = arrivalTolerance;
return this;
}
public setDecelerationRadius(decelerationRadius: number): FollowPath {
this.decelerationRadius = decelerationRadius;
return this;
}
public setTimeToTarget(timeToTarget: number): FollowPath {
this.timeToTarget = timeToTarget;
return this;
}
protected calculateRealSteering(steering: SteeringAcceleration): SteeringAcceleration {
// Predictive or non-predictive behavior?
const location = (this.predictionTime === 0) ?
// Use the current position of the owner
this.owner.getPosition() :
// Calculate the predicted future position of the owner. We're reusing steering.linear here.
steering.linear
.copy(this.owner.getPosition())
.scaleAndAdd(this.owner.getLinearVelocity(), this.predictionTime);
// Find the distance from the start of the path
const distance = this.path.calculateDistance(location, this.pathParam);
// Offset it
const targetDistance = distance + this.pathOffset;
// Calculate the target position
this.path.calculateTargetPosition(this.internalTargetPosition, this.pathParam, targetDistance);
if (this.arriveEnabled && this.path.isOpen()) {
if (this.pathOffset >= 0) {
// Use Arrive to approach the last point of the path
if (targetDistance > this.path.getLength() - this.decelerationRadius) {
return this.arrive(steering, this.internalTargetPosition);
}
} else {
// Use Arrive to approach the first point of the path
if (targetDistance < this.decelerationRadius) {
return this.arrive(steering, this.internalTargetPosition);
}
}
}
// Seek the target position
steering.linear
.copy(this.internalTargetPosition)
.sub(this.owner.getPosition())
.nor()
.scale(this.getActualLimiter().getMaxLinearAcceleration());
// No angular acceleration
steering.angular = 0;
// Output steering acceleration
return steering;
}
}
export default FollowPath;