/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package illarion.client.world.movement;
import illarion.client.util.pathfinding.*;
import illarion.client.world.CharMovementMode;
import illarion.client.world.World;
import illarion.common.types.Direction;
import illarion.common.types.ServerCoordinate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import static illarion.client.world.CharMovementMode.Run;
import static illarion.client.world.CharMovementMode.Walk;
/**
* This movement handler is used to approach a specified location.
*
* @author Martin Karing <nitram@illarion.org>
*/
class WalkToMovementHandler extends AbstractMovementHandler implements TargetMovementHandler, MoveCostProvider {
@Nonnull
private static final Logger log = LoggerFactory.getLogger(WalkToMovementHandler.class);
@Nonnull
private static final Marker marker = MarkerFactory.getMarker("Movement");
/**
* The path finder used to calculate the paths towards the target location.
*/
@Nonnull
private final PathFindingAlgorithm pathFindingAlgorithm;
@Nullable
private ServerCoordinate targetLocation;
private int targetDistance;
@Nullable
private Runnable targetAction;
@Nullable
private Path currentPath;
@Nonnull
private final Collection<Direction> allowedDirections;
WalkToMovementHandler(@Nonnull Movement movement) {
super(movement);
allowedDirections = EnumSet.allOf(Direction.class);
pathFindingAlgorithm = new AStar();
}
@Nullable
@Override
public StepData getNextStep(@Nonnull ServerCoordinate currentLocation) {
ServerCoordinate target = targetLocation;
if (target == null) {
return null;
}
int remainingDistance = currentLocation.getStepDistance(target);
log.debug(marker, "Remaining distance to target: {} Expected distance: {}", remainingDistance, targetDistance);
if (remainingDistance <= targetDistance) {
return new DefaultStepData(CharMovementMode.None, finishMove(currentLocation), fetchTargetAction());
}
Path activePath;
if (isCurrentPathValid()) {
activePath = currentPath;
} else {
activePath = calculateNewPath(currentLocation);
currentPath = activePath;
}
if ((activePath == null) || activePath.isEmpty()) {
return new DefaultStepData(CharMovementMode.None, finishMove(currentLocation), fetchTargetAction());
}
PathNode node = activePath.nextStep();
if (node == null) {
return new DefaultStepData(CharMovementMode.None, finishMove(currentLocation), fetchTargetAction());
}
if (!isPathNodeValid(currentLocation, node)) {
activePath = calculateNewPath(currentLocation);
currentPath = activePath;
if (activePath == null) {
return new DefaultStepData(CharMovementMode.None, finishMove(currentLocation), fetchTargetAction());
}
node = activePath.nextStep();
if (node == null) {
return new DefaultStepData(CharMovementMode.None, finishMove(currentLocation), fetchTargetAction());
}
if (!isPathNodeValid(currentLocation, node)) {
Direction lastDirection = currentLocation.getDirection(target);
targetLocation = null;
targetAction = null;
return new DefaultStepData(CharMovementMode.None, lastDirection);
}
}
log.debug(marker, "Performing step to: {}", node.getLocation());
Direction moveDir = currentLocation.getDirection(node.getLocation());
if (activePath.isEmpty() && (targetDistance == 0) && !target.equals(node.getLocation())) {
targetDistance = 1;
}
return new DefaultStepData(node.getMovementMethod(), moveDir);
}
@Nullable
private Direction finishMove(@Nonnull ServerCoordinate currentLocation) {
if (targetLocation == null) {
throw new IllegalStateException("Finishing a move is not possible while there is no target location set.");
}
Direction direction = null;
int remainingDistance = currentLocation.getStepDistance(targetLocation);
if (remainingDistance > 0) {
direction = currentLocation.getDirection(targetLocation);
}
targetLocation = null;
return direction;
}
@Nullable
private Runnable fetchTargetAction() {
Runnable action = targetAction;
targetAction = null;
return action;
}
private static boolean isPathNodeValid(@Nonnull ServerCoordinate currentLocation, @Nonnull PathNode node) {
int distanceToPlayer = currentLocation.getStepDistance(node.getLocation());
switch (node.getMovementMethod()) {
case Walk:
if (distanceToPlayer == 1) {
return true;
}
break;
case Run:
if (!World.getPlayer().getCarryLoad().isRunningPossible()) {
return false;
}
if (distanceToPlayer == 2) {
return true;
}
break;
}
log.warn(marker, "Next path node {} is out of range: {}", node, distanceToPlayer);
return false;
}
protected int getTargetDistance() {
return targetDistance;
}
protected void increaseTargetDistance() {
targetDistance++;
}
private boolean isCurrentPathValid() {
Path path = currentPath;
if (path == null) {
log.debug(marker, "Path is not valid: Current path is NULL");
return false;
}
ServerCoordinate destination = path.getDestination();
if (destination == null) {
log.debug(marker, "Path is not valid: Path destination is NULL");
return false;
}
if (!destination.equals(targetLocation)) {
log.debug(marker, "Path is not valid: Destination ({}) does not equal the current target location ({}).",
destination, targetLocation);
return false;
}
return true;
}
@Nullable
protected Path calculateNewPath(@Nonnull ServerCoordinate currentLocation) {
ServerCoordinate target = getTargetLocation();
log.info(marker, "Calculating a new path from {} to {}", currentLocation, target);
PathFindingAlgorithm algorithm = pathFindingAlgorithm;
switch (getMovementMode()) {
case Walk:
return algorithm.findPath(this, currentLocation, target, targetDistance,
getAllowedDirections(currentLocation, target), Walk);
case Run:
return algorithm.findPath(this, currentLocation, target, targetDistance,
getAllowedDirections(currentLocation, target), Walk, Run);
default:
return null;
}
}
@Nonnull
protected CharMovementMode getMovementMode() {
if (!World.getPlayer().getCarryLoad().isRunningPossible()) {
return Walk;
}
CharMovementMode mode = getMovement().getDefaultMovementMode();
if (getMovement().isMovementModePossible(mode)) {
return mode;
}
return Walk;
}
@Override
public void disengage(boolean transferAllowed) {
super.disengage(transferAllowed);
targetLocation = null;
setTargetReachedAction(null);
}
@Nonnull
protected Collection<Direction> getAllowedDirections(@Nonnull ServerCoordinate current,
@Nonnull ServerCoordinate target) {
return Collections.unmodifiableCollection(allowedDirections);
}
@Override
public void walkTo(@Nonnull ServerCoordinate target, int distance) {
setTargetReachedAction(null);
targetLocation = target;
targetDistance = distance;
}
@Override
public void setTargetReachedAction(@Nullable Runnable action) {
if (!Objects.equals(targetAction, action)) {
targetAction = action;
if (action == null) {
log.debug(marker, "Removed target reached action");
} else {
log.debug(marker, "Set target reached action");
}
}
}
@Nonnull
@Override
public String toString() {
return "Walk to target movement handler";
}
protected boolean isTargetSet() {
return targetLocation != null;
}
@Nonnull
protected ServerCoordinate getTargetLocation() {
if (targetLocation == null) {
throw new IllegalStateException("The target location is not set.");
}
return targetLocation;
}
@Override
public int getMovementCost(@Nonnull ServerCoordinate origin, @Nonnull CharMovementMode mode, @Nonnull Direction direction) {
int cost = getMovement().getMovementDuration(origin, mode, direction);
if ((cost != MoveCostProvider.BLOCKED) &&
origin.equals(getMovement().getServerLocation()) &&
(mode == getMovementMode()) &&
(direction == getPreferredDirection())) {
cost /= 2;
}
return cost;
}
@Nullable
protected Direction getPreferredDirection() {
if (isTargetSet()) {
ServerCoordinate currentPos = getMovement().getServerLocation();
ServerCoordinate targetPos = getTargetLocation();
return currentPos.getDirection(targetPos);
}
return null;
}
}