From 1d4374410b2c4b21135f181f872031b8a443a04e Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Wed, 5 Oct 2022 02:23:12 +0100
Subject: [PATCH] Import from Advent codebase

---
 .gitignore            |  12 ++
 Cargo.toml            |  24 +++
 rust-toolchain.toml   |   2 +
 rustfmt.toml          |   4 +
 src/definitions.rs    | 218 +++++++++++++++++++++++++
 src/directionality.rs |  89 +++++++++++
 src/lib.rs            |  22 +++
 src/loader.rs         |  67 ++++++++
 src/query.rs          | 359 ++++++++++++++++++++++++++++++++++++++++++
 src/systems.rs        | 104 ++++++++++++
 10 files changed, 901 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Cargo.toml
 create mode 100644 rust-toolchain.toml
 create mode 100644 rustfmt.toml
 create mode 100644 src/definitions.rs
 create mode 100644 src/directionality.rs
 create mode 100644 src/lib.rs
 create mode 100644 src/loader.rs
 create mode 100644 src/query.rs
 create mode 100644 src/systems.rs

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d0da6e5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+.idea/
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..53087d3
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "micro_banimate"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+description = "Easily manage complex Bevy 2D sprite animations"
+authors = [
+	"Louis Capitanchik <louis@microhacks.co.uk>"
+]
+
+[features]
+default = ["json_loader", "ecs_tilemap"]
+json_loader = ["serde", "dep:serde_json"]
+toml_loader = ["serde", "dep:toml"]
+ecs_tilemap = ["dep:bevy_ecs_tilemap"]
+serde = ["dep:serde"]
+
+[dependencies]
+anyhow = "1.0.65"
+serde = { version = "1.0.145", optional = true }
+serde_json = { version = "1.0.85", optional = true }
+toml = { version = "0.5.9", optional = true }
+bevy = { version = "0.8.1", default-features = false, features = ["bevy_asset", "render"] }
+bevy_ecs_tilemap = { version = "0.7.0", optional = true }
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..31578d3
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "stable"
\ No newline at end of file
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..d62aed7
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,4 @@
+hard_tabs = true
+#group_imports = "StdExternalCrate"
+use_field_init_shorthand = true
+use_try_shorthand = true
\ No newline at end of file
diff --git a/src/definitions.rs b/src/definitions.rs
new file mode 100644
index 0000000..3558442
--- /dev/null
+++ b/src/definitions.rs
@@ -0,0 +1,218 @@
+use std::collections::HashMap;
+use std::ops::{Deref, DerefMut};
+
+use bevy::asset::Handle;
+use bevy::prelude::{Bundle, Component};
+use bevy::reflect::TypeUuid;
+
+use crate::directionality::Directionality;
+
+#[derive(Clone, PartialOrd, PartialEq, Debug, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationFrames {
+	pub frames: Vec<usize>,
+	pub frame_secs: f32,
+}
+
+impl AnimationFrames {
+	pub fn len_secs(&self) -> f32 {
+		self.frames.len() as f32 * self.frame_secs
+	}
+}
+
+#[derive(Clone, Debug, TypeUuid, PartialEq, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[uuid = "a2823f96-0f63-434e-9030-d8f762898a18"]
+pub struct AnimationSet(pub HashMap<String, AnimationFrames>);
+impl Deref for AnimationSet {
+	type Target = HashMap<String, AnimationFrames>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for AnimationSet {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct SyncAnimationsToParent;
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct HasAnimations;
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct HasSimpleAnimations;
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct HasDirectionalityAnimation;
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationPaused;
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationUserData(pub u128);
+
+#[derive(Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum AnimationMode {
+	#[default]
+	Loop,
+	Once,
+	OnceThenPlay(String),
+}
+
+#[derive(Clone, Debug, Component, PartialEq, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationStatus {
+	pub active_name: String,
+	pub active_step: usize,
+	pub frame_time: f32,
+}
+
+impl AnimationStatus {
+	pub fn assert_animation<T: ToString>(&mut self, name: T) {
+		self.active_name = name.to_string();
+	}
+	pub fn start_animation<T: ToString>(&mut self, name: T) {
+		self.active_name = name.to_string();
+		self.active_step = 0;
+	}
+	pub fn start_or_continue<T: ToString>(&mut self, name: T) {
+		let name = name.to_string();
+		if self.active_name != name {
+			self.active_name = name;
+			self.active_step = 0;
+		}
+	}
+}
+
+#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)]
+pub struct DirectionalSpriteAnimationBundle {
+	pub animation_handle: Handle<AnimationSet>,
+	pub mode: AnimationMode,
+	pub status: AnimationStatus,
+	pub direction: Directionality,
+	pub marker: HasDirectionalityAnimation,
+}
+
+impl DirectionalSpriteAnimationBundle {
+	pub fn new(initial_anim: String, handle: Handle<AnimationSet>) -> Self {
+		Self {
+			animation_handle: handle,
+			status: AnimationStatus {
+				active_name: initial_anim,
+				active_step: 0,
+				frame_time: 0.0,
+			},
+			mode: AnimationMode::Loop,
+			direction: Directionality::default(),
+			marker: HasDirectionalityAnimation,
+		}
+	}
+	pub fn with_initial_facing(
+		initial_anim: String,
+		handle: Handle<AnimationSet>,
+		direction: Directionality,
+	) -> Self {
+		Self {
+			animation_handle: handle,
+			status: AnimationStatus {
+				active_name: initial_anim,
+				active_step: 0,
+				frame_time: 0.0,
+			},
+			mode: AnimationMode::Loop,
+			marker: HasDirectionalityAnimation,
+			direction,
+		}
+	}
+}
+
+#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)]
+pub struct SpriteAnimationBundle {
+	pub animation_handle: Handle<AnimationSet>,
+	pub mode: AnimationMode,
+	pub status: AnimationStatus,
+	pub marker: HasAnimations,
+}
+
+impl SpriteAnimationBundle {
+	pub fn new(initial_anim: String, handle: Handle<AnimationSet>) -> Self {
+		Self {
+			animation_handle: handle,
+			status: AnimationStatus {
+				active_name: initial_anim,
+				active_step: 0,
+				frame_time: 0.0,
+			},
+			mode: AnimationMode::Loop,
+			marker: HasAnimations,
+		}
+	}
+}
+
+#[derive(Clone, Debug, Component, PartialEq, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct SimpleLoopedAnimation {
+	pub frames: Vec<usize>,
+	pub frame_secs: f32,
+}
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct SimpleLoopedAnimationStatus {
+	pub active_step: usize,
+	pub frame_time: f32,
+}
+
+#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)]
+pub struct SimpleAnimationBundle {
+	pub anim: SimpleLoopedAnimation,
+	pub status: SimpleLoopedAnimationStatus,
+	pub marker: HasSimpleAnimations,
+}
+
+impl SimpleAnimationBundle {
+	pub fn new(frames: Vec<usize>, frame_secs: f32) -> Self {
+		SimpleAnimationBundle {
+			anim: SimpleLoopedAnimation { frames, frame_secs },
+			status: SimpleLoopedAnimationStatus {
+				active_step: 0,
+				frame_time: 0.0,
+			},
+			marker: HasSimpleAnimations,
+		}
+	}
+}
+
+#[derive(Clone, Debug, Component, PartialEq, Eq, PartialOrd, Ord, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationOverride {
+	pub name: String,
+	pub user_data: u128,
+}
+
+#[derive(Clone, Debug, Bundle, PartialEq, PartialOrd, Default)]
+pub struct ChildAnimationBundle {
+	pub animation_handle: Handle<AnimationSet>,
+	pub status: AnimationStatus,
+	pub marker: SyncAnimationsToParent,
+}
+
+impl ChildAnimationBundle {
+	pub fn new(handle: Handle<AnimationSet>) -> Self {
+		Self {
+			animation_handle: handle,
+			..Default::default()
+		}
+	}
+}
diff --git a/src/directionality.rs b/src/directionality.rs
new file mode 100644
index 0000000..5592f50
--- /dev/null
+++ b/src/directionality.rs
@@ -0,0 +1,89 @@
+use std::fmt::{Display, Formatter};
+
+use bevy::math::{Vec2, Vec3};
+use bevy::prelude::Component;
+
+#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum Horizontal {
+	Left,
+	#[default]
+	Right,
+}
+
+impl From<f32> for Horizontal {
+	fn from(other: f32) -> Self {
+		if other < 0.0 {
+			Self::Left
+		} else {
+			Self::Right
+		}
+	}
+}
+
+impl Display for Horizontal {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Horizontal::Left => f.write_str("left"),
+			Horizontal::Right => f.write_str("right"),
+		}
+	}
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum Vertical {
+	Up,
+	#[default]
+	Down,
+}
+
+impl From<f32> for Vertical {
+	fn from(other: f32) -> Self {
+		if other < 0.0 {
+			Self::Up
+		} else {
+			Self::Down
+		}
+	}
+}
+
+impl Display for Vertical {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Vertical::Up => f.write_str("up"),
+			Vertical::Down => f.write_str("down"),
+		}
+	}
+}
+
+#[derive(Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct Directionality {
+	pub vertical: Vertical,
+	pub horizontal: Horizontal,
+}
+
+impl From<Vec2> for Directionality {
+	fn from(other: Vec2) -> Self {
+		Self {
+			horizontal: other.x.into(),
+			vertical: other.y.into(),
+		}
+	}
+}
+
+impl From<Vec3> for Directionality {
+	fn from(other: Vec3) -> Self {
+		Self {
+			horizontal: other.x.into(),
+			vertical: other.y.into(),
+		}
+	}
+}
+
+impl Display for Directionality {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		write!(f, "{}_{}", self.horizontal, self.vertical)
+	}
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..16ecdf7
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,22 @@
+pub mod definitions;
+pub mod directionality;
+pub mod loader;
+pub mod query;
+pub mod systems;
+
+mod plugin {
+	use bevy::app::{PluginGroup, PluginGroupBuilder};
+
+	use crate::loader;
+
+	pub struct AdventAnimationsPlugin;
+	impl PluginGroup for AdventAnimationsPlugin {
+		fn build(&mut self, group: &mut PluginGroupBuilder) {
+			group.add(super::systems::AnimationSystemsPlugin);
+			#[cfg(any(feature = "json_loader", feature = "toml_loader"))]
+			group.add(loader::AnimationLoadersPlugin);
+		}
+	}
+}
+
+pub use plugin::AdventAnimationsPlugin;
diff --git a/src/loader.rs b/src/loader.rs
new file mode 100644
index 0000000..b2cd52b
--- /dev/null
+++ b/src/loader.rs
@@ -0,0 +1,67 @@
+#[cfg(feature = "json_loader")]
+mod json {
+	use bevy::asset::{AssetLoader, BoxedFuture, Error, LoadContext, LoadedAsset};
+
+	use crate::definitions::AnimationSet;
+
+	pub struct AnimationSetJsonLoader;
+	impl AssetLoader for AnimationSetJsonLoader {
+		fn load<'a>(
+			&'a self,
+			bytes: &'a [u8],
+			load_context: &'a mut LoadContext,
+		) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
+			Box::pin(async move {
+				let value: AnimationSet = serde_json::from_slice(bytes)?;
+				load_context.set_default_asset(LoadedAsset::new(value));
+				Ok(())
+			})
+		}
+
+		fn extensions(&self) -> &[&str] {
+			static EXTENSIONS: &[&str] = &["anim.json"];
+			EXTENSIONS
+		}
+	}
+}
+
+#[cfg(feature = "toml_loader")]
+mod toml {
+	use bevy::asset::{AssetLoader, BoxedFuture, Error, LoadContext, LoadedAsset};
+
+	use crate::definitions::AnimationSet;
+
+	pub struct AnimationSetTomlLoader;
+	impl AssetLoader for AnimationSetTomlLoader {
+		fn load<'a>(
+			&'a self,
+			bytes: &'a [u8],
+			load_context: &'a mut LoadContext,
+		) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
+			Box::pin(async move {
+				let value: AnimationSet = toml::from_slice(bytes)?;
+				load_context.set_default_asset(LoadedAsset::new(value));
+				Ok(())
+			})
+		}
+
+		fn extensions(&self) -> &[&str] {
+			static EXTENSIONS: &[&str] = &["anim.toml"];
+			EXTENSIONS
+		}
+	}
+}
+
+use bevy::app::App;
+use bevy::prelude::{AddAsset, Plugin};
+
+pub struct AnimationLoadersPlugin;
+impl Plugin for AnimationLoadersPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_asset::<crate::definitions::AnimationSet>();
+		#[cfg(feature = "json_loader")]
+		app.add_asset_loader(json::AnimationSetJsonLoader);
+		#[cfg(feature = "toml_loader")]
+		app.add_asset_loader(self::toml::AnimationSetTomlLoader);
+	}
+}
diff --git a/src/query.rs b/src/query.rs
new file mode 100644
index 0000000..a1d949b
--- /dev/null
+++ b/src/query.rs
@@ -0,0 +1,359 @@
+use std::ops::{Deref, DerefMut};
+use std::time::Duration;
+
+use anyhow::{Error, Result};
+use bevy::ecs::system::SystemParam;
+use bevy::prelude::*;
+
+use crate::definitions::{
+	AnimationFrames, AnimationMode, AnimationOverride, AnimationPaused, AnimationSet,
+	AnimationStatus, HasAnimations, HasDirectionalityAnimation, HasSimpleAnimations,
+	SyncAnimationsToParent,
+};
+use crate::directionality::{Directionality, Horizontal, Vertical};
+use crate::systems::AnimationCompleted;
+
+/// Manage animated entities
+#[derive(SystemParam)]
+pub struct AnimationQuery<'w, 's> {
+	commands: Commands<'w, 's>,
+	animations: Res<'w, Assets<AnimationSet>>,
+	inner: Query<
+		'w,
+		's,
+		(
+			Entity,
+			&'static Handle<AnimationSet>,
+			&'static mut AnimationMode,
+			&'static mut AnimationStatus,
+		),
+		(
+			Or<(With<HasAnimations>, With<HasDirectionalityAnimation>)>,
+			Without<HasSimpleAnimations>,
+			Without<SyncAnimationsToParent>,
+		),
+	>,
+	inner_child: Query<
+		'w,
+		's,
+		(
+			Entity,
+			&'static Parent,
+			&'static Handle<AnimationSet>,
+			&'static mut AnimationStatus,
+		),
+		(
+			With<SyncAnimationsToParent>,
+			Without<HasAnimations>,
+			Without<HasDirectionalityAnimation>,
+			Without<HasSimpleAnimations>,
+		),
+	>,
+	direction: Query<'w, 's, &'static mut Directionality>,
+	action_animation: Query<'w, 's, &'static AnimationOverride>,
+	tile_sprite: Query<'w, 's, &'static mut TextureAtlasSprite>,
+	paused: Query<'w, 's, Entity, With<AnimationPaused>>,
+	events: EventWriter<'w, 's, AnimationCompleted>,
+}
+
+impl<'w, 's> Deref for AnimationQuery<'w, 's> {
+	type Target = Query<
+		'w,
+		's,
+		(
+			Entity,
+			&'static Handle<AnimationSet>,
+			&'static mut AnimationMode,
+			&'static mut AnimationStatus,
+		),
+		(
+			Or<(With<HasAnimations>, With<HasDirectionalityAnimation>)>,
+			Without<HasSimpleAnimations>,
+			Without<SyncAnimationsToParent>,
+		),
+	>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.inner
+	}
+}
+
+impl<'w, 's> DerefMut for AnimationQuery<'w, 's> {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.inner
+	}
+}
+
+impl<'w, 's> AnimationQuery<'w, 's> {
+	/// Given an entity, ensure that the currently playing animation matches
+	/// the provided name. This method does not change the current animation
+	/// step
+	pub fn assert_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> {
+		let (_, _, _, mut status) = self.inner.get_mut(e)?;
+		status.assert_animation(name);
+		Ok(())
+	}
+
+	/// Given an entity, start an animation from that entity's animation set
+	/// from frame 0. If the currently playing animation is requested, it
+	/// will be restarted
+	pub fn start_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> {
+		let (_, _, _, mut status) = self.inner.get_mut(e)?;
+		status.start_animation(name);
+		Ok(())
+	}
+
+	/// Given an entity, start an animation from that entity's animation set
+	/// from frame 0. If the currently playing animation is requested, no change
+	/// will happen and it will continue to play uninterrupted
+	pub fn start_or_continue_animation<T: ToString>(&mut self, e: Entity, name: T) -> Result<()> {
+		let (_, _, _, mut status) = self.inner.get_mut(e)?;
+		status.start_or_continue(name);
+		Ok(())
+	}
+
+	/// Given an entity, start playing an animation once and then switch
+	/// to a looped version of another animation. If the currently playing
+	/// animation is requested, it will be restarted
+	pub fn start_animation_and_then<T: ToString, Y: ToString>(
+		&mut self,
+		e: Entity,
+		name: T,
+		next: Y,
+	) -> Result<()> {
+		let (_, _, mut mode, mut status) = self.inner.get_mut(e)?;
+
+		*mode = AnimationMode::OnceThenPlay(next.to_string());
+		status.active_name = name.to_string();
+		status.active_step = 0;
+
+		Ok(())
+	}
+
+	/// Given an entity, start playing an animation once and then switch
+	/// to a looped version of another animation. If the currently playing
+	/// animation is requested, it will be restarted
+	pub fn continue_animation_and_then<T: ToString, Y: ToString>(
+		&mut self,
+		e: Entity,
+		name: T,
+		next: Y,
+	) -> Result<()> {
+		let (_, _, mut mode, mut status) = self.inner.get_mut(e)?;
+		let name = name.to_string();
+
+		*mode = AnimationMode::OnceThenPlay(next.to_string());
+		if status.active_name != name {
+			status.active_name = name;
+			status.active_step = 0;
+		}
+		Ok(())
+	}
+
+	/// Set an entity's animations to play one time. The animation
+	/// will pause on the last frame
+	pub fn play_once(&mut self, e: Entity) -> Result<()> {
+		let (_, _, mut mode, _) = self.inner.get_mut(e)?;
+		*mode = AnimationMode::Once;
+		Ok(())
+	}
+
+	/// Set an entity's animations to loop continuously. The
+	/// animation will reset to the start once it has reached the
+	/// last frame
+	pub fn play_loop(&mut self, e: Entity) -> Result<()> {
+		let (_, _, mut mode, _) = self.inner.get_mut(e)?;
+		*mode = AnimationMode::Loop;
+		Ok(())
+	}
+
+	/// Set an entity's animations to play one time. The animation
+	/// will be changed to the specified set once the current animation
+	/// has reached the last frame
+	pub fn play_next<T: ToString>(&mut self, e: Entity, next: T) -> Result<()> {
+		let (_, _, mut mode, _) = self.inner.get_mut(e)?;
+		*mode = AnimationMode::OnceThenPlay(next.to_string());
+		Ok(())
+	}
+
+	/// Measure the amount of time it will take an animation to run.
+	/// The exact time that an animation runs for will differ slightly
+	/// from this, due to the minimum time increment being 1 frame
+	/// (16ms when running at 60fps)
+	pub fn get_animation_duration<T: ToString>(
+		&self,
+		handle: Handle<AnimationSet>,
+		name: T,
+	) -> Result<Duration> {
+		let sheet = self
+			.animations
+			.get(&handle)
+			.ok_or_else(|| anyhow::Error::msg("Failed to fetch animation set"))?;
+
+		let details = sheet.get(name.to_string().as_str()).ok_or_else(|| {
+			anyhow::Error::msg(format!("Missing animation from set: {}", name.to_string()))
+		})?;
+
+		Ok(Duration::from_secs_f32(
+			details.frame_secs * details.frames.len() as f32,
+		))
+	}
+
+	pub fn resolve_animation(
+		&self,
+		handle: &Handle<AnimationSet>,
+		name: &String,
+	) -> Option<&AnimationFrames> {
+		self.animations
+			.get(handle)
+			.and_then(|sheet| sheet.get(name))
+	}
+
+	/// Check whether or not an entity is currently paused. If so,
+	/// the entity will not tick its animation state
+	pub fn is_paused(&self, e: Entity) -> bool {
+		self.paused.get(e).is_ok()
+	}
+
+	/// Attach a "paused" marker to this entity that will cause
+	/// standard animations to not tick. This is safe to call
+	/// with an entity that might already be paused; no effect will
+	/// take place
+	pub fn pause(&mut self, e: Entity) {
+		self.commands.entity(e).insert(AnimationPaused);
+	}
+
+	/// Remove any "paused" markers from this entity, which will cause
+	/// standard animations to resume ticking. This is safe to call
+	/// with an entity that might already be paused; no effect will
+	/// take place
+	pub fn unpause(&mut self, e: Entity) {
+		self.commands.entity(e).remove::<AnimationPaused>();
+	}
+
+	pub fn set_direction(&mut self, e: Entity, horizontal: Horizontal, vertical: Vertical) {
+		if let Ok(mut direction) = self.direction.get_mut(e) {
+			direction.horizontal = horizontal;
+			direction.vertical = vertical;
+		}
+	}
+	pub fn set_partial_direction(
+		&mut self,
+		e: Entity,
+		horizontal: Option<Horizontal>,
+		vertical: Option<Vertical>,
+	) {
+		if let Ok(mut direction) = self.direction.get_mut(e) {
+			if let Some(horizontal) = horizontal {
+				direction.horizontal = horizontal;
+			}
+			if let Some(vertical) = vertical {
+				direction.vertical = vertical;
+			}
+		}
+	}
+
+	/// Apply a number of delta seconds to all unpaused animations
+	pub fn tick_all(&mut self, dt: f32) -> Result<()> {
+		for (entity, handle, mut mode, mut status) in &mut self.inner {
+			if self.paused.get(entity).is_ok() {
+				continue;
+			}
+
+			let sheet = match self.animations.get(handle) {
+				Some(sheet) => sheet,
+				None => continue,
+			};
+
+			let current = match sheet.get(&status.active_name) {
+				Some(set) => set,
+				None => continue,
+			};
+
+			status.frame_time += dt;
+			while status.frame_time >= current.frame_secs {
+				status.frame_time -= current.frame_secs;
+				status.active_step += 1;
+			}
+
+			if status.active_step >= current.frames.len() {
+				match mode.clone() {
+					AnimationMode::Loop => {
+						status.active_step = 0;
+					}
+					AnimationMode::Once => {
+						status.active_step = current.frames.len() - 1;
+					}
+					AnimationMode::OnceThenPlay(next) => {
+						*mode = AnimationMode::Loop;
+						status.active_name = next.clone();
+						status.frame_time = 0.0;
+					}
+				}
+
+				if let Ok(action_anim) = self.action_animation.get(entity) {
+					self.commands.entity(entity).remove::<AnimationOverride>();
+					self.events.send(AnimationCompleted {
+						entity,
+						user_data: action_anim.user_data,
+					});
+				}
+			}
+
+			if let Ok(mut sprite) = self.tile_sprite.get_mut(entity) {
+				sprite.index = current.frames[status.active_step];
+			}
+		}
+
+		Ok(())
+	}
+
+	/// Syncs all animations that are set to track their parents
+	pub fn sync_parent_animations(&mut self) {
+		for (entity, parent, handle, mut status) in &mut self.inner_child {
+			if let Ok((_, _, _, parent_status)) = self.inner.get(**parent) {
+				*status = parent_status.clone();
+			}
+
+			if let Ok(mut sprite) = self.tile_sprite.get_mut(entity) {
+				if let Some(current) = self
+					.animations
+					.get(handle)
+					.and_then(|sheet| sheet.get(&status.active_name))
+				{
+					sprite.index = current.frames[status.active_step];
+				}
+			}
+		}
+	}
+
+	pub fn apply_directionality_to<T: ToString>(&mut self, action: T, e: Entity) -> Result<()> {
+		if self.paused.get(e).is_ok() {
+			return Ok(());
+		}
+
+		let (_, animations, _, mut status) = self.inner.get_mut(e)?;
+		let direction = self.direction.get(e)?;
+		let over = self.action_animation.get(e).ok();
+		let set = self
+			.animations
+			.get(animations)
+			.ok_or_else(|| Error::msg("Missing Animation"))?;
+
+		let fallback = over
+			.map(|o| o.name.clone())
+			.unwrap_or_else(|| action.to_string());
+
+		let directed = format!("{}_{}", &fallback, direction);
+
+		// log::info!("Directed - {}", directed);
+
+		if set.contains_key(&directed) {
+			status.start_or_continue(directed);
+		} else {
+			status.start_or_continue(fallback);
+		}
+
+		Ok(())
+	}
+}
diff --git a/src/systems.rs b/src/systems.rs
new file mode 100644
index 0000000..6a04c2f
--- /dev/null
+++ b/src/systems.rs
@@ -0,0 +1,104 @@
+use bevy::prelude::*;
+
+use crate::definitions::*;
+use crate::query::AnimationQuery;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)]
+pub enum AnimationSystems {
+	TickAnimations,
+	SyncAnimations,
+}
+
+#[derive(Copy, Clone, Debug, Component, PartialEq, Eq, Ord, PartialOrd)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub struct AnimationCompleted {
+	pub entity: Entity,
+	pub user_data: u128,
+}
+
+pub fn tick_animations(time: Res<Time>, mut query: AnimationQuery) {
+	let seconds = time.delta_seconds();
+	let _ = query.tick_all(seconds);
+}
+
+pub fn tick_simple_sprite_animations(
+	time: Res<Time>,
+	mut query: Query<
+		(
+			&SimpleLoopedAnimation,
+			&mut SimpleLoopedAnimationStatus,
+			&mut TextureAtlasSprite,
+		),
+		With<HasSimpleAnimations>,
+	>,
+) {
+	let seconds = time.delta_seconds();
+	for (animation, mut state, mut sprite) in &mut query {
+		state.frame_time += seconds;
+		while state.frame_time >= animation.frame_secs {
+			state.frame_time -= animation.frame_secs;
+			state.active_step += 1;
+			if state.active_step >= animation.frames.len() {
+				state.active_step = 0;
+			}
+		}
+
+		sprite.index = animation.frames[state.active_step];
+	}
+}
+
+pub fn sync_parent_animations(mut query: AnimationQuery) {
+	query.sync_parent_animations();
+}
+
+#[cfg(feature = "ecs_tilemap")]
+pub fn tick_simple_tilemap_animation(
+	time: Res<Time>,
+	mut query: Query<
+		(
+			&SimpleLoopedAnimation,
+			&mut SimpleLoopedAnimationStatus,
+			&mut bevy_ecs_tilemap::tiles::TileTexture,
+		),
+		With<HasSimpleAnimations>,
+	>,
+) {
+	let seconds = time.delta_seconds();
+	for (animation, mut state, mut tile) in &mut query {
+		state.frame_time += seconds;
+		while state.frame_time >= animation.frame_secs {
+			state.frame_time -= animation.frame_secs;
+			state.active_step += 1;
+			if state.active_step >= animation.frames.len() {
+				state.active_step = 0;
+			}
+		}
+
+		*tile = bevy_ecs_tilemap::tiles::TileTexture(animation.frames[state.active_step] as u32);
+	}
+}
+
+pub struct AnimationSystemsPlugin;
+
+impl Plugin for AnimationSystemsPlugin {
+	fn build(&self, app: &mut App) {
+		let mut tick_systems = SystemSet::new()
+			.label(AnimationSystems::TickAnimations)
+			.with_system(tick_animations)
+			.with_system(tick_simple_sprite_animations);
+
+		#[cfg(feature = "ecs_tilemap")]
+		{
+			tick_systems = tick_systems.with_system(tick_simple_tilemap_animation);
+		}
+
+		app.add_event::<AnimationCompleted>()
+			.add_system_set_to_stage(CoreStage::PostUpdate, tick_systems)
+			.add_system_to_stage(
+				CoreStage::PostUpdate,
+				sync_parent_animations
+					.label(AnimationSystems::SyncAnimations)
+					.after(AnimationSystems::TickAnimations),
+			);
+	}
+}
-- 
GitLab