/*
* 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.graphics;
import illarion.client.world.World;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* The animation handler is the base class for all animations. It offers some
* abstract class functions for animated objects like entities and some static
* function for animations that handle the approaching of values.
* <p>
* This class handles all kind of animations. This could be frame based
* animations as well as movement animations.
* </p>
*
* @author Nop
* @author Martin Karing <nitram@illarion.org>
*/
abstract class AbstractAnimation<T extends Animated> {
private static final Logger log = LoggerFactory.getLogger(AbstractAnimation.class);
/**
* The current time of the animation. This value is always between the 0 and
* {@link #duration}.
*/
private int currentTime;
/**
* The time the full animation needs to run. After this time the animation
* either stops or starts looping again from the start. The duration is
* stored in milliseconds.
* <p>
* The maximal duration is 2^31 - 1 what is around 24 days.
* </p>
*/
private int duration;
/**
* A flag to determine if a animation is currently running or not. If the
* animation is currently running this variable is true.
*/
private boolean running;
/**
* Make the animation skip the next timing update.
*/
private boolean skipNextUpdate;
/**
* The animation targets. This contains the objects that are handled by this
* animation. All targets handled by one animation are updated at the same
* time in a synchronized way.
*/
@Nonnull
private final List<T> targets;
private float storyboardStart;
private float storyboardEnd;
/**
* The constructor for a new animation. It does not start the animation
* right away it rather prepares the animation and takes the first animation
* target. In case there are more animation targets needed use
* {@link #addTarget(Animated, boolean)}.
*
* @param firstTarget the first animation target, so the first object that
* is actually animated
*/
protected AbstractAnimation(@Nullable T firstTarget) {
targets = new CopyOnWriteArrayList<>();
if (firstTarget != null) {
targets.add(firstTarget);
}
storyboardStart = 0.f;
storyboardEnd = 1.f;
}
/**
* The constructor for a new animation. It does not start the animation
* right away it rather prepares the animation and takes the first animation
* target. In case there are more animation targets needed use
* {@link #addTarget(Animated, boolean)}.
*
* @param firstTarget the first animation target, so the first object that
* is actually animated
*/
protected AbstractAnimation(@Nullable T firstTarget, @Nonnull AbstractAnimation<T> sourceAnimation) {
this(firstTarget);
duration = sourceAnimation.duration;
}
/**
* Add an animation target to this animation. All animations are handled
* synchronized. So they start in the same moment and stop in the same
* moment.
*
* @param target the new animation target that shall be added this animation
* @param autoStart true in case the animation shall start right away after
* the new animation target is added. The auto start will invoke
* the {@link #restart()} method so this one needs all data to
* start this animation in case its needed to do so
*/
public final void addTarget(T target, boolean autoStart) {
if (!targets.contains(target)) {
targets.add(target);
}
// connect to animation
if (autoStart && !isRunning()) {
restart();
}
}
/**
* Called by game loop to execute animations. This function executes the
* animation itself.
*
* @param delta the time since the last update of this animation
* @return {@code true} in case the animation is in process,
* {@code false} if its done
*/
public abstract boolean animate(int delta);
/**
* Get the progress of the animation.
*
* @return a value between 0 and 1. 0 means the animation just started, 1
* means the animation ended
*/
public final float animationProgress() {
return (float) currentTime / duration;
}
protected final float getStoryboardProgress(boolean wrapped) {
float animationProgress = animationProgress();
float storyboardSpan = storyboardEnd - storyboardStart;
float unwrappedStoryboard = (storyboardSpan * animationProgress) + storyboardStart;
if (wrapped) {
return unwrappedStoryboard % 1.f;
}
return unwrappedStoryboard;
}
public final void setStoryboard(float start, float end) {
storyboardStart = start;
storyboardEnd = end;
}
public final void continueStoryboard(float length) {
setStoryboard(storyboardEnd, storyboardEnd + length);
log.debug("Expanding storyboard from {} to {}", storyboardStart, storyboardEnd);
}
public final void resetStoryboard() {
setStoryboard(0.f, 1.f);
}
/**
* Checks if the animation is currently running.
*
* @return true in case the animation is currently running
*/
public final boolean isRunning() {
return running;
}
/**
* Remove an animation target from the animation. In case the removing of
* the target empties the target list the animation stops automatically.
*
* @param target the animation target that shall be removed from the target
* list
*/
public final void removeTarget(T target) {
targets.remove(target);
// stop animation when last target disappears
if (targets.isEmpty()) {
stop();
}
}
/**
* Restarts an animation with the last set of parameters.
*/
public abstract void restart();
/**
* Set the running state to a new value to either report that the animation
* Stopped or that it started.
*
* @param newState the new value for the running state. True means the
* animation is still running, false means that it stopped
*/
public final void setRunning(boolean newState) {
running = newState;
}
/**
* Stop a animation. Do not forget to report the changed animation state by
* setting the running indicator to false using {@link #setRunning(boolean)}
* .
*/
public abstract void stop();
/**
* Get the time remaining until the end of the animation.
*
* @return the time in ms that is still left to the end of the animation
*/
public final int timeRemaining() {
return duration - currentTime;
}
/**
* Signals that an animation has ended. This reports that the animation is
* finished to all animation targets.
*
* @param finished true in case the animation finished, false if not
*/
protected final void animationFinished(boolean finished) {
for (@Nonnull Animated target : targets) {
target.animationFinished(finished);
}
}
protected final void animationStarted() {
targets.forEach(Animated::animationStarted);
}
/**
* Get on of the animation targets of this animation. This is needed for
* some update functions.
*
* @param index the list index of the animation target that is wanted. This
* value needs to be between 0 and {@link #getTargetCount()}
* @return the requested target
*/
@Nullable
protected final T getAnimationTarget(int index) {
if (targets.size() <= index) {
return null;
}
return targets.get(index);
}
/**
* Get the amount of targets that are set to this animation.
*
* @return the amount of targets set to this animation
*/
protected final int getTargetCount() {
return targets.size();
}
/**
* Set the value of the duration of this animation.
*
* @param newDuration the new value for the duration of this animation
*/
public final void setDuration(int newDuration) {
duration = newDuration;
}
/**
* Get the duration of the animation.
*
* @return the duration of the animation
*/
public final int getDuration() {
return duration;
}
/**
* Set the timing values of this animation. This sets the start time to the
* current local time and sets the current time to the start time. The end
* time is calculated by the start time plus the duration. So the duration
* need to be known before this function is called.
*/
protected final void setTiming() {
currentTime = 0;
}
protected final void setSkipNextUpdate(boolean skip) {
skipNextUpdate = skip;
}
/**
* Start a animation. That causes that the timing values are set up and the
* animation is registered to AnimationManager. Before calling this function
* the {@link #duration} of the animation need to be set.
*/
protected final void start() {
// set timing
setTiming();
if (!running) {
running = true;
World.getAnimationManager().register(this);
animationStarted();
}
}
/**
* Increase the current time by a set delta time. The time values are handled in milliseconds.
*
* @param delta the time in milliseconds that shall be added to the current time
* @return true in case the animation ended by this update, means the current time reached the total duration of
* the animation
*/
protected final boolean updateCurrentTime(int delta) {
if (skipNextUpdate) {
skipNextUpdate = false;
} else {
currentTime += delta;
}
return currentTime > duration;
}
}