package com.asteria.game.character;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Optional;
import com.asteria.game.NodeType;
import com.asteria.game.World;
import com.asteria.game.character.combat.Combat;
import com.asteria.game.character.player.Player;
import com.asteria.game.location.Position;
import com.asteria.task.Task;
import com.asteria.utility.RandomGen;
/**
* The movement queue sequencer that handles the entire movement process for
* characters.
*
* @author lare96 <http://github.com/lare96>
*/
public final class MovementQueue {
/**
* The direction delta {@code X} coordinates for movement.
*/
public static final byte[] DIRECTION_DELTA_X = new byte[] { -1, 0, 1, -1, 1, -1, 0, 1 };
/**
* The direction delta {@code Y} coordinates for movement.
*/
public static final byte[] DIRECTION_DELTA_Y = new byte[] { 1, 1, 1, 0, 0, -1, -1, -1 };
/**
* The character this movement queue is for.
*/
private final CharacterNode character;
/**
* A double ended queue of waypoints in this movement queue.
*/
private Deque<Point> waypoints = new LinkedList<>();
/**
* The task ran when following another character.
*/
private Optional<Task> followTask = Optional.empty();
/**
* The flag that determines if the run button is toggled.
*/
private boolean running = false;
/**
* The flag that determines if the current path is a run path.
*/
private boolean runPath = false;
/**
* The flag that determines if movement is locked.
*/
private boolean lockMovement;
/**
* Creates a new {@link MovementQueue}.
*
* @param character
* the character this movement queue is for.
*/
public MovementQueue(CharacterNode character) {
this.character = character;
}
/**
* Executes movement processing which primarily consists of polling
* waypoints, and updating the map region.
*
* @throws Exception
* if any errors occur while sequencing movement.
*/
public void sequence() throws Exception {
if (lockMovement || character.isFrozen()) {
return;
}
Point walkPoint = null;
Point runPoint = null;
walkPoint = waypoints.poll();
if (running) {
runPoint = waypoints.poll();
}
runPath = runPoint != null;
if (walkPoint != null && walkPoint.getDirection() != -1) {
int x = MovementQueue.DIRECTION_DELTA_X[walkPoint.getDirection()];
int y = MovementQueue.DIRECTION_DELTA_Y[walkPoint.getDirection()];
if (character.isFollowing() && character.getFollowCharacter() != null) {
if (character.getPosition().copy().move(x, y).equals(character.getFollowCharacter().getPosition())) {
return;
}
}
character.setLastPosition(character.getPosition().copy());
character.getPosition().move(x, y);
character.setPrimaryDirection(walkPoint.getDirection());
character.setLastDirection(walkPoint.getDirection());
if (character.getType() == NodeType.PLAYER) {
((Player) character).sendInterfaces();
}
}
if (runPoint != null && runPoint.getDirection() != -1) {
int x = MovementQueue.DIRECTION_DELTA_X[runPoint.getDirection()];
int y = MovementQueue.DIRECTION_DELTA_Y[runPoint.getDirection()];
if (character.isFollowing() && character.getFollowCharacter() != null) {
if (character.getPosition().copy().move(x, y).equals(character.getFollowCharacter().getPosition())) {
return;
}
}
if (character.getType() == NodeType.PLAYER) {
Player player = (Player) character;
if (player.getRunEnergy().getAndDecrement() > 0) {
player.sendInterfaces();
player.getMessages().sendString(player.getRunEnergy() + "%", 149);
} else {
running = false;
player.getMessages().sendByteState(173, 0);
}
}
character.setLastPosition(character.getPosition().copy());
character.getPosition().move(x, y);
character.setSecondaryDirection(runPoint.getDirection());
character.setLastDirection(runPoint.getDirection());
}
if (character.getType() == NodeType.PLAYER) {
int deltaX = character.getPosition().getX() - character.getCurrentRegion().getRegionX() * 8;
int deltaY = character.getPosition().getY() - character.getCurrentRegion().getRegionY() * 8;
if (deltaX < 16 || deltaX >= 88 || deltaY < 16 || deltaY > 88) {
((Player) character).getMessages().sendMapRegion();
}
}
}
/**
* Forces the character to walk to a certain position point relevant to its
* current position.
*
* @param addX
* the amount of spaces to walk to the {@code X}.
* @param addY
* the amount of spaces to walk to the {@code Y}.
*/
public void walk(int addX, int addY) {
walk(new Position(character.getPosition().getX() + addX, character.getPosition().getY() + addY));
}
/**
* Forces the character to walk to a certain position point not relevant to
* its current position.
*
* @param position
* the position the character is moving too.
*/
public void walk(Position position) {
reset();
addToPath(position);
finish();
}
/**
* Resets the walking queue for this character.
*/
public void reset() {
runPath = false;
waypoints.clear();
Position p = character.getPosition();
waypoints.add(new Point(p.getX(), p.getY(), -1));
}
/**
* Finishes the current path for this character.
*/
public void finish() {
waypoints.removeFirst();
}
/**
* Determines if this walking queue is finished or not.
*
* @return {@code true} if this walking queue is finished, {@code false}
* otherwise.
*/
public boolean isMovementDone() {
return waypoints.size() == 0;
}
/**
* Adds a new position to the walking queue.
*
* @param position
* the position to add.
*/
public void addToPath(Position position) {
if (waypoints.size() == 0) {
reset();
}
Point last = waypoints.peekLast();
int deltaX = position.getX() - last.getX();
int deltaY = position.getY() - last.getY();
int max = Math.max(Math.abs(deltaX), Math.abs(deltaY));
for (int i = 0; i < max; i++) {
if (deltaX < 0) {
deltaX++;
} else if (deltaX > 0) {
deltaX--;
}
if (deltaY < 0) {
deltaY++;
} else if (deltaY > 0) {
deltaY--;
}
addStep(position.getX() - deltaX, position.getY() - deltaY);
}
}
/**
* Adds a step to the walking queue.
*
* @param x
* the {@code X} coordinate of the step.
* @param y
* the {@code Y} coordinate of the step.
*/
private void addStep(int x, int y) {
if (waypoints.size() >= 100) {
return;
}
Point last = waypoints.peekLast();
int deltaX = x - last.getX();
int deltaY = y - last.getY();
int direction = direction(deltaX, deltaY);
if (direction > -1) {
waypoints.add(new Point(x, y, direction));
}
}
/**
* Calculates the direction between the two coordinates.
*
* @param dx
* the first coordinate.
* @param dy
* the second coordinate.
* @return the direction.
*/
private int direction(int dx, int dy) {
if (dx < 0) {
if (dy < 0) {
return 5;
} else if (dy > 0) {
return 0;
} else {
return 3;
}
} else if (dx > 0) {
if (dy < 0) {
return 7;
} else if (dy > 0) {
return 2;
} else {
return 4;
}
} else {
if (dy < 0) {
return 6;
} else if (dy > 0) {
return 1;
} else {
return -1;
}
}
}
/**
* Prompts the controller of this movement queue to follow {@code leader}.
*
* @param leader
* the character being followed.
*/
public void follow(CharacterNode leader) {
if (character.getFollowCharacter() != null && character.getFollowCharacter().equals(leader)) {
return;
}
if (character.isFollowing() && !leader.equals(character.getFollowCharacter())) {
character.faceCharacter(null);
character.setFollowing(false);
character.setFollowCharacter(null);
}
if (!character.isFollowing()) {
followTask.ifPresent(Task::cancel);
}
if (!followTask.isPresent()) {
character.setFollowing(true);
character.setFollowCharacter(leader);
followTask = Optional.of(new CharacterFollowTask(character, leader));
World.submit(followTask.get());
}
}
/**
* Determines if the run button is toggled.
*
* @return {@code true} if the run button is toggled, {@code false}
* otherwise.
*/
public boolean isRunning() {
return running;
}
/**
* Sets the value for {@link MovementQueue#running}.
*
* @param runToggled
* the new value to set.
*/
public void setRunning(boolean runToggled) {
this.running = runToggled;
}
/**
* Determines if the current path is a run path.
*
* @return {@code true} if the current path is a run path, {@code false}
* otherwise.
*/
public boolean isRunPath() {
return runPath;
}
/**
* Sets the value for {@link MovementQueue#runPath}.
*
* @param runPath
* the new value to set.
*/
public void setRunPath(boolean runPath) {
this.runPath = runPath;
}
/**
* Determines if the movement queue is locked.
*
* @return {@code true} if the movement queue is locked, {@code false}
* otherwise.
*/
public boolean isLockMovement() {
return lockMovement;
}
/**
* Sets the value for {@link MovementQueue#lockMovement}.
*
* @param lockMovement
* the new value to set.
*/
public void setLockMovement(boolean lockMovement) {
this.lockMovement = lockMovement;
}
/**
* The internal position type class with support for direction.
*
* @author blakeman8192
*/
private final class Point extends Position {
/**
* The direction to this point.
*/
private final int direction;
/**
* Creates a new {@link Point}.
*
* @param x
* the {@code X} coordinate.
* @param y
* the {@code Y} coordinate.
* @param direction
* the direction to this point.
*/
public Point(int x, int y, int direction) {
super(x, y);
this.direction = direction;
}
/**
* Gets the direction to this point.
*
* @return the direction.
*/
public int getDirection() {
return direction;
}
}
/**
* The {@link Task} implementation that handles the entire following
* process.
*
* @author lare96 <http://github.com/lare96>
*/
private static final class CharacterFollowTask extends Task {
/**
* The character this process is being executed for.
*/
private final CharacterNode character;
/**
* The character being followed in this process.
*/
private final CharacterNode leader;
/**
* The thread local random instance.
*/
private final RandomGen random = new RandomGen();
/**
* Creates a new {@link CharacterFollowTask}.
*
* @param character
* the character this process is being executed for.
* @param leader
* the character being followed in this process.
*/
public CharacterFollowTask(CharacterNode character, CharacterNode leader) {
super(1, true);
this.character = character;
this.leader = leader;
}
@Override
public void execute() {
if (!character.isFollowing() || !character.getPosition().withinDistance(leader.getPosition(), 20) || character.isDead() || leader
.isDead()) {
character.faceCharacter(null);
character.setFollowing(false);
character.setFollowCharacter(null);
this.cancel();
return;
}
character.faceCharacter(leader);
if (character.getMovementQueue().isLockMovement() || character.isFrozen()) {
return;
}
if (character.getPosition().equals(leader.getPosition().copy())) {
character.getMovementQueue().reset();
int[] dir = { 1, -1 };
if (random.get().nextBoolean()) {
character.getMovementQueue().walk(random.random(dir), 0);
} else {
character.getMovementQueue().walk(0, random.random(dir));
}
return;
}
if (character.getCombatBuilder().isAttacking() && character.getType() == NodeType.PLAYER) {
character.getCombatBuilder().determineStrategy();
if (Combat.checkAttackDistance(character.getCombatBuilder())) {
return;
}
}
if (character.getPosition().withinDistance(leader.getPosition(), 1)) {
return;
}
character.getMovementQueue().walk(leader.getPosition().copy());
}
@Override
public void onCancel() {
character.getMovementQueue().followTask = Optional.empty();
}
}
}