use std::{collections::HashMap, ffi::OsStr, path::Path, time::Duration};
use ggez::{
graphics::{Drawable, Image, Rect},
*,
};
use crate::{
ui::UiContent,
ui::{Size, UiElementBuilder},
};
use std::hash::Hash;
use regex;
#[derive(Debug, Clone)]
pub struct Sprite {
w: u32,
h: u32,
spritesheet: Option<Image>,
frame_time: Duration,
current_frame_time: Duration,
current_frame: u32,
current_variant: u32,
}
impl Sprite {
pub fn new(spritesheet: Image, w: u32, h: u32, frame_time: Duration) -> Self {
Self {
frame_time,
w,
h,
spritesheet: Some(spritesheet),
current_frame_time: Duration::ZERO,
current_frame: 0,
current_variant: 0,
}
}
pub fn from_path(
path: impl AsRef<Path>,
ctx: &Context,
w: u32,
h: u32,
frame_time: Duration,
) -> Result<Self, GameError> {
Ok(Self {
frame_time,
w,
h,
spritesheet: Some(Image::from_path(ctx, path)?),
current_frame_time: Duration::ZERO,
current_frame: 0,
current_variant: 0,
})
}
pub fn from_path_fmt(
path: impl AsRef<Path>,
ctx: &Context,
frame_time: Duration,
) -> Result<Self, GameError> {
let pathstring = path
.as_ref()
.file_name()
.unwrap_or(OsStr::new(""))
.to_str()
.ok_or(GameError::CustomError(
"Path formatted incorrectly.".to_owned(),
))?;
let width_height = pathstring
.split('.')
.next()
.unwrap_or("")
.split('_')
.collect::<Vec<&str>>()
.iter()
.rev()
.take(2)
.copied()
.rev()
.collect::<Vec<&str>>();
let w = *width_height.first().ok_or(GameError::CustomError(format!(
"Filename formatted incorretly - not ending in _width_height.extension. Filename: {}",
pathstring
)))?;
let h = *width_height.get(1).ok_or(GameError::CustomError(format!(
"Filename formatted incorretly - not ending in _width_height.extension. Filename: {}",
pathstring
)))?;
let w = w.parse::<u32>().map_err(|_| {
GameError::CustomError(
format!("Filename formatted correctly, but width numbers could not be parsed. Width number: {}", w),
)
})?;
let h = h.parse::<u32>().map_err(|_| {
GameError::CustomError(
format!("Filename formatted correctly, but height numbers could not be parsed. Height number: {}", h),
)
})?;
Ok(Self {
frame_time,
w,
h,
spritesheet: Some(Image::from_path(ctx, path)?),
current_frame_time: Duration::ZERO,
current_frame: 0,
current_variant: 0,
})
}
pub fn set_variant(&mut self, variant: u32) {
if self.current_variant != variant {
self.current_variant = variant;
if self.h != 0 {
self.current_variant %= self
.spritesheet
.as_ref()
.map(|img| img.height())
.unwrap_or_default()
/ self.h;
}
self.current_frame_time = Duration::ZERO;
self.current_frame = 0;
}
}
pub fn get_variant(&self) -> u32 {
self.current_variant
}
pub fn get_dimensions(&self) -> (f32, f32) {
(self.w as f32, self.h as f32)
}
pub fn get_frame_time(&self) -> Duration {
self.frame_time
}
pub fn set_frame_time(&mut self, frame_time: Duration) {
self.frame_time = frame_time;
}
pub fn get_cycle_time(&self) -> Duration {
self.frame_time
* self
.spritesheet
.as_ref()
.map(|img| img.width())
.unwrap_or_default()
/ self.w
}
pub fn draw_sprite(
&mut self,
ctx: &Context,
canvas: &mut graphics::Canvas,
param: impl Into<graphics::DrawParam>,
) {
self.current_frame_time += ctx.time.delta();
while self.current_frame_time >= self.frame_time && !self.frame_time.is_zero() {
self.current_frame_time -= self.frame_time;
self.current_frame = (self.current_frame + 1)
% (self
.spritesheet
.as_ref()
.map(|img| img.width())
.unwrap_or_default()
/ self.w);
}
self.draw(canvas, param);
}
}
impl Drawable for Sprite {
fn draw(&self, canvas: &mut graphics::Canvas, param: impl Into<graphics::DrawParam>) {
if let Some(spritesheet) = &self.spritesheet {
spritesheet.draw(
canvas,
(param.into() as graphics::DrawParam).src(Rect::new(
(self.w * self.current_frame) as f32 / spritesheet.width() as f32,
(self.h * self.current_variant) as f32 / spritesheet.height() as f32,
self.w as f32 / spritesheet.width() as f32,
self.h as f32 / spritesheet.height() as f32,
)),
);
}
}
fn dimensions(
&self,
_gfx: &impl ggez::context::Has<ggez::graphics::GraphicsContext>,
) -> Option<ggez::graphics::Rect> {
Some(Rect::new(0., 0., self.w as f32, self.h as f32))
}
}
impl<T: Copy + Eq + Hash> UiContent<T> for Sprite {
fn to_element_builder(self, id: u32, _ctx: &Context) -> UiElementBuilder<T>
where
Self: Sized + 'static,
{
let (w, h) = (self.w, self.h);
UiElementBuilder::new(id, self)
.with_size(
Size::Fill(w as f32, f32::INFINITY),
Size::Fill(h as f32, f32::INFINITY),
)
.with_preserve_ratio(true)
}
fn draw_content(
&mut self,
ctx: &mut Context,
canvas: &mut graphics::Canvas,
param: crate::ui::UiDrawParam,
) {
self.draw_sprite(
ctx,
canvas,
param.param.dest_rect(Rect::new(
param.target.x,
param.target.y,
param.target.w / self.w as f32,
param.target.h / self.h as f32,
)),
);
}
}
impl Default for Sprite {
fn default() -> Self {
Self {
w: 0,
h: 0,
spritesheet: None,
frame_time: Duration::ZERO,
current_frame_time: Duration::ZERO,
current_frame: 0,
current_variant: 0,
}
}
}
pub struct SpritePool {
sprites: HashMap<String, Sprite>,
default_duration: Duration,
}
impl SpritePool {
pub fn new() -> Self {
Self {
sprites: HashMap::new(),
default_duration: Duration::ZERO,
}
}
pub fn with_default_duration(mut self, default_duration: Duration) -> Self {
self.default_duration = default_duration;
self
}
pub fn with_folder(
mut self,
ctx: &Context,
path: impl AsRef<Path>,
search_subfolders: bool,
) -> Self {
let paths = ctx
.fs
.read_dir(path.as_ref())
.expect("Could not find specified path.");
let sprite_match = regex::Regex::new(r"(.*)_\d*_\d*.[png|jpg|jpeg]").unwrap();
for sub_path in paths {
let path_string = sub_path.to_string_lossy().to_string();
if sprite_match.is_match(&path_string) {
if let Ok(sprite) =
Sprite::from_path_fmt(sub_path.clone(), ctx, self.default_duration)
{
self.sprites.insert(
sprite_match
.captures(&path_string)
.map(|c| c.get(1).map(|m| m.as_str()))
.unwrap_or_default()
.unwrap_or_default()
.replace('\\', "/"),
sprite,
);
}
} else if search_subfolders {
self = self.with_folder(ctx, sub_path, search_subfolders);
}
}
self
}
pub fn init_sprite(
&self,
path: impl AsRef<Path>,
frame_time: Duration,
) -> Result<Sprite, GameError> {
let sprite = self
.sprites
.get(&path.as_ref().to_string_lossy().to_string())
.ok_or_else(|| GameError::CustomError("Could not find sprite.".to_owned()))?;
Ok(Sprite {
frame_time,
..sprite.clone()
})
}
pub fn init_sprite_unchecked(&self, path: impl AsRef<Path>, frame_time: Duration) -> Sprite {
let sprite = self
.sprites
.get(&path.as_ref().to_string_lossy().to_string())
.unwrap_or_else(|| {
panic!(
"[ERROR/Mooeye] Could not find sprite {}.",
path.as_ref().to_string_lossy()
)
});
Sprite {
frame_time,
..sprite.clone()
}
}
pub fn init_sprite_lazy(
&mut self,
ctx: &Context,
path: impl AsRef<Path>,
frame_time: Duration,
) -> Result<Sprite, GameError> {
let key = path.as_ref().to_string_lossy().to_string();
if !self.sprites.contains_key(&key) {
self.attempt_load(ctx, &key)?;
}
self.init_sprite(path, frame_time)
}
fn attempt_load(&mut self, ctx: &Context, key: &str) -> Result<(), GameError> {
let directory = key.rsplit_once('/').unwrap_or_default().0.to_owned() + "/";
let paths = ctx.fs.read_dir(directory)?;
let sprite_match = regex::Regex::new(r"(.*)_\d*_\d*.[png|jpg|jpeg]").unwrap();
for sub_path in paths {
let path_string = sub_path.to_string_lossy().to_string();
if sprite_match.is_match(&path_string) {
if let Some(Some(path_str)) = sprite_match
.captures(&path_string)
.map(|c| c.get(1).map(|m| m.as_str().replace('\\', "/").to_owned()))
{
if path_str == key {
if let Ok(sprite) =
Sprite::from_path_fmt(sub_path.clone(), ctx, self.default_duration)
{
self.sprites.insert(path_str, sprite);
return Ok(());
}
}
}
}
}
Err(GameError::CustomError(
"[ERROR/Mooeye] Could not find sprite.".to_owned(),
))
}
pub fn sprite_ref(&mut self, path: impl AsRef<Path>) -> Result<&mut Sprite, GameError> {
let sprite = self
.sprites
.get_mut(&path.as_ref().to_string_lossy().to_string())
.ok_or_else(|| {
GameError::CustomError(
"[ERROR/Mooeye] Could not find the specified sprite.".to_owned(),
)
})?;
Ok(sprite)
}
pub fn sprite_ref_lazy(
&mut self,
ctx: &Context,
path: impl AsRef<Path>,
) -> Result<&mut Sprite, GameError> {
let key = path.as_ref().to_string_lossy().to_string();
if !self.sprites.contains_key(&key) {
self.attempt_load(ctx, &key)?;
}
self.sprite_ref(path)
}
pub fn print_keys(&self) {
println!("Currently registered keys:");
for (key, _) in self.sprites.iter() {
println!(" | {}", &key);
}
println!("-+----------------")
}
}
impl Default for SpritePool {
fn default() -> Self {
Self::new()
}
}