/* * 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.graphics.AnimatedMove; import illarion.client.graphics.MoveAnimation; import illarion.client.util.UpdateTaskManager; import illarion.client.world.Char; import illarion.client.world.CharMovementMode; import illarion.client.world.Player; import illarion.client.world.World; import illarion.common.graphics.Layer; import illarion.common.types.Direction; import illarion.common.types.DisplayCoordinate; 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.LinkedList; import java.util.Objects; import java.util.Queue; /** * This class takes care for everything regarding the animation of the moves. It triggers and monitors the required * animations and reports back the different states of the animation. * * @author Martin Karing <nitram@illarion.org> */ class MoveAnimator implements AnimatedMove { @Nonnull private static final Logger log = LoggerFactory.getLogger(MoveAnimator.class); @Nonnull private static final Marker marker = MarkerFactory.getMarker("Movement"); @Nonnull private final Movement movement; @Nonnull private final MoveAnimation moveAnimation; @Nonnull private final Queue<MoveAnimatorTask> taskQueue = new LinkedList<>(); private boolean animationInProgress; private boolean reportingDone; @Nullable private Direction lastRequestedTurn; /** * Schedule a move task that is not yet confirmed. */ @Nullable private MovingTask uncomfirmedMoveTask; @Nullable private MovingTask confirmedMoveTask; public MoveAnimator(@Nonnull Movement movement, @Nonnull MoveAnimation moveAnimation) { this.movement = movement; this.moveAnimation = moveAnimation; } private void scheduleMove(@Nonnull CharMovementMode mode, @Nonnull ServerCoordinate target, int duration) { scheduleTask(new MovingTask(this, mode, target, duration)); } private void scheduleTask(@Nonnull MoveAnimatorTask task) { taskQueue.offer(task); if (!animationInProgress) { World.getUpdateTaskManager().addTaskForLater((container, delta) -> { if (!animationInProgress) { executeNext(); } }); } } void scheduleEarlyMove(@Nonnull CharMovementMode mode, @Nonnull ServerCoordinate target, int duration) { if (uncomfirmedMoveTask != null) { log.warn(marker, "Scheduling another early move is not possible as there is already one set."); } else { log.debug(marker, "Scheduling a early move. Mode: {}, Target: {}, Duration: {}ms", mode, target, duration); MovingTask task = new MovingTask(this, mode, target, duration); uncomfirmedMoveTask = task; scheduleTask(task); } } /** * Cancel a currently executed move in case there is any. * <br /> * In case the currently executed move targets location that is reported, the move is allowed to continue. * * @param allowedTarget allowed target location */ void cancelMove(@Nonnull ServerCoordinate allowedTarget) { Player parentPlayer = movement.getPlayer(); MovingTask task = uncomfirmedMoveTask; UpdateTaskManager utm = World.getUpdateTaskManager(); if (task == null) { log.debug(marker, "Received cancel move, but there is no unconfirmed move. Settings location to {}", allowedTarget); utm.addTaskForLater((container, delta) -> { log.debug(marker, "Setting player location to {} now.", allowedTarget); parentPlayer.setLocation(allowedTarget); }); } else { taskQueue.clear(); if (task.isExecuted()) { uncomfirmedMoveTask = null; confirmedMoveTask = null; if (moveAnimation.isRunning()) { log.debug(marker, "Received cancel move, move was already in progress. Resetting"); utm.addTaskForLater((container, delta) -> { log.debug(marker, "Resetting location to {} for cancel.", allowedTarget); parentPlayer.setLocation(allowedTarget); parentPlayer.getCharacter().resetAnimation(true); moveAnimation.stop(); }); } else { log.debug(marker, "Move seems to be done already."); utm.addTaskForLater((container, delta) -> parentPlayer.setLocation(allowedTarget)); } } else { log.debug(marker, "Move did not start yet. We are good."); } } movement.reportReadyForNextStep(); } /** * Confirm a move with the specified parameters. * * @param mode the move * @param target the target of the move * @param duration the duration of the move */ void confirmMove(@Nonnull CharMovementMode mode, @Nonnull ServerCoordinate target, int duration) { MovingTask task = uncomfirmedMoveTask; if (task == null) { log.debug(marker, "No unconfirmed move found. Schedule the move."); confirmedMoveTask = null; scheduleMove(mode, target, duration); } else { if (task.isExecuted()) { uncomfirmedMoveTask = null; confirmedMoveTask = null; if (moveAnimation.isRunning()) { /* We have a active move. Lets check it out. */ Player parentPlayer = movement.getPlayer(); if (parentPlayer.getLocation().equals(target)) { /* Okay we are moving to the right place. Lets check if the timing fits. */ if (moveAnimation.getDuration() == duration) { log.debug(marker, "Already running animation with {}ms to {} is correct", duration, target); } else { if (log.isWarnEnabled()) { log.warn(marker, "Move to the correct place is in progress. Fixing time from {}ms to {}ms", moveAnimation.getDuration(), duration); } /* The timing is off. Lets fix that. */ moveAnimation.setDuration(duration); parentPlayer.getCharacter().updateMoveDuration(duration); } } else { log.warn(marker, "Move to the wrong location. Resetting. Expected location: {} Player " + "location: {}", target, parentPlayer.getLocation()); /* Crap! We are moving to the wrong place... */ movement.executeServerLocation(target); movement.reportReadyForNextStep(); } } else { log.debug(marker, "The unconfirmed move seems to be done already."); movement.reportReadyForNextStep(); } } else { if (task.isSetupCorrectly(mode, target, duration)) { log.debug(marker, "Move is correctly scheduled."); confirmedMoveTask = task; } else { confirmedMoveTask = new MovingTask(this, mode, target, duration); log.warn(marker, "Move is not correctly scheduled. Scheduled: {}, New: {}", task, confirmedMoveTask); } } } } void scheduleTurn(@Nonnull Direction direction) { if (lastRequestedTurn != direction) { lastRequestedTurn = direction; scheduleTask(new TurningTask(this, direction)); } } void cancelAll() { taskQueue.clear(); moveAnimation.stop(); lastRequestedTurn = null; } void executeTurn(@Nonnull Direction direction) { log.debug("Executing turn to {} now.", direction); movement.getPlayer().getCharacter().setDirection(direction); executeNext(); } void executeMove(@Nonnull CharMovementMode mode, @Nonnull ServerCoordinate target, int duration) { log.debug("Executing move (Mode: {}) to {} (Duration: {}ms) now.", mode, target, duration); Player parentPlayer = movement.getPlayer(); Char playerCharacter = parentPlayer.getCharacter(); if ((mode == CharMovementMode.None) || playerCharacter.getLocation().equals(target)) { parentPlayer.updateLocation(target); playerCharacter.setLocation(target); World.getMapDisplay().animationFinished(true); movement.reportReadyForNextStep(); executeNext(); return; } reportingDone = false; DisplayCoordinate startDC = getDisplayCoordinateAt(parentPlayer.getLocation()); DisplayCoordinate targetDC = getDisplayCoordinateAt(target); playerCharacter.moveTo(target, mode, duration); moveAnimation.start(startDC, targetDC, duration); parentPlayer.updateLocation(target); } @Nonnull private DisplayCoordinate getDisplayCoordinateAt(@Nonnull ServerCoordinate coordinate) { int elevation = World.getMap().getElevationAt(coordinate); int x = coordinate.toDisplayX(); int y = coordinate.toDisplayY() - elevation; int layer = coordinate.toDisplayLayer(Layer.Chars); return new DisplayCoordinate(x, y, layer); } private boolean executeNext() { @Nullable MovingTask unconfirmedTask = uncomfirmedMoveTask; if ((unconfirmedTask != null) && unconfirmedTask.isExecuted()) { log.debug("Stopping move execution because a unconfirmed move finished executing."); /* Found a executed but not yet confirmed move. Hold everything right here and wait for the confirmation. */ animationInProgress = false; return false; } MoveAnimatorTask task = taskQueue.poll(); if (task != null) { if (Objects.equals(task, unconfirmedTask)) { MoveAnimatorTask confirmedTask = confirmedMoveTask; if (confirmedTask != null) { log.debug("Current move is a unconfirmed move. Using the confirmed version."); confirmedMoveTask = null; uncomfirmedMoveTask = null; // overwrite used task //noinspection ReuseOfLocalVariable task = confirmedTask; } else { log.debug("Current move is a unconfirmed move. No confirmed version present."); } } animationInProgress = true; log.debug(marker, "Movement animator is executing next task: {}", task); task.execute(); return true; } else { animationInProgress = false; return false; } } @Override public void setPosition(@Nonnull DisplayCoordinate position) { if (isReportingRequired()) { int remaining = moveAnimation.timeRemaining(); if (remaining < 20) { log.debug(marker, "Requesting next move {}ms before the animation finishes.", remaining); reportingDone = true; movement.reportReadyForNextStep(); } } } private boolean isReportingRequired() { return !reportingDone && (uncomfirmedMoveTask == null); } @Override public void animationStarted() { } @Override public void animationFinished(boolean finished) { if (isReportingRequired()) { log.debug(marker, "Requesting next move at the end of the animation."); reportingDone = true; if (finished) { movement.reportReadyForNextStep(); } } World.getMapDisplay().setLocation(getDisplayCoordinateAt(movement.getPlayer().getLocation())); executeNext(); } }