From 8c89ed889a9f90415102a029abd68dd62f951519 Mon Sep 17 00:00:00 2001 From: Nils Hasenbanck Date: Sat, 28 Dec 2024 15:35:56 +0100 Subject: [PATCH] Separate sprite and entity animations This PR separates the action types and the way we render the animations of entities and "actor" sprites like the mouse cursor and the skill icons completely. We also now properly calculate the base offset of entity actions based on the entity type. The workaround for the mouse cursor has also been removed by fixing the underlying problem. --- korangar/src/input/mode.rs | 5 +- korangar/src/interface/cursor/mod.rs | 59 +++-- .../interface/elements/miscellanious/skill.rs | 2 +- korangar/src/inventory/skills.rs | 16 +- korangar/src/loaders/action/mod.rs | 203 +----------------- korangar/src/loaders/animation/mod.rs | 4 +- korangar/src/main.rs | 9 +- korangar/src/world/action/mod.rs | 131 +++++++++++ korangar/src/world/animation/mod.rs | 132 ++++++++++-- korangar/src/world/entity/mod.rs | 55 ++--- korangar/src/world/mod.rs | 2 + 11 files changed, 333 insertions(+), 285 deletions(-) create mode 100644 korangar/src/world/action/mod.rs diff --git a/korangar/src/input/mode.rs b/korangar/src/input/mode.rs index 3a82f7bd..0d56c036 100644 --- a/korangar/src/input/mode.rs +++ b/korangar/src/input/mode.rs @@ -9,7 +9,8 @@ use crate::graphics::Texture; use crate::interface::application::InterfaceSettings; use crate::interface::resource::{ItemSource, SkillSource}; use crate::inventory::Skill; -use crate::loaders::{Actions, AnimationState, ResourceMetadata, Sprite}; +use crate::loaders::{ResourceMetadata, Sprite}; +use crate::world::{Actions, SpriteAnimationState}; #[derive(Default)] pub enum MouseInputMode { @@ -27,7 +28,7 @@ pub enum MouseInputMode { pub enum Grabbed { Texture(Arc), - Action(Arc, Arc, AnimationState), + Action(Arc, Arc, SpriteAnimationState), } impl MouseInputMode { diff --git a/korangar/src/interface/cursor/mod.rs b/korangar/src/interface/cursor/mod.rs index df4d418b..b0de3519 100644 --- a/korangar/src/interface/cursor/mod.rs +++ b/korangar/src/interface/cursor/mod.rs @@ -7,26 +7,27 @@ use super::application::InterfaceSettings; use super::layout::{ScreenClip, ScreenPosition, ScreenSize}; use crate::graphics::Color; use crate::input::Grabbed; -use crate::loaders::{ActionLoader, Actions, AnimationState, Sprite, SpriteLoader}; +use crate::loaders::{ActionLoader, Sprite, SpriteLoader}; use crate::renderer::{GameInterfaceRenderer, SpriteRenderer}; +use crate::world::{Actions, SpriteAnimationState}; #[allow(dead_code)] -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum MouseCursorState { - Default, - Dialog, - Click, - Unsure0, - RotateCamera, - Attack, - Attack1, - Warp, - NoAction, - Grab, - Unsure1, - Unsure2, - WarpFast, - Unsure3, + Default = 0, + Dialog = 1, + Click = 2, + Unsure0 = 3, + RotateCamera = 4, + Attack = 5, + Attack1 = 6, + Warp = 7, + NoAction = 8, + Grab = 9, + Unsure1 = 10, + Unsure2 = 11, + WarpFast = 12, + Unsure3 = 13, } impl From for usize { @@ -38,7 +39,8 @@ impl From for usize { pub struct MouseCursor { sprite: Arc, actions: Arc, - animation_state: AnimationState, + cursor_state: MouseCursorState, + animation_state: SpriteAnimationState, shown: bool, } @@ -46,12 +48,13 @@ impl MouseCursor { pub fn new(sprite_loader: &mut SpriteLoader, action_loader: &mut ActionLoader) -> Self { let sprite = sprite_loader.get("cursors.spr").unwrap(); let actions = action_loader.get("cursors.act").unwrap(); - let animation_state = AnimationState::new(MouseCursorState::Default, ClientTick(0)); + let animation_state = SpriteAnimationState::new(ClientTick(0)); let shown = true; Self { sprite, actions, + cursor_state: MouseCursorState::Default, animation_state, shown, } @@ -69,18 +72,12 @@ impl MouseCursor { self.animation_state.update(client_tick); } - // TODO: this is just a workaround until i find a better solution to make the - // cursor always look correct. - pub fn set_start_time(&mut self, client_tick: ClientTick) { - self.animation_state.start_time = client_tick; - } - pub fn set_state(&mut self, state: MouseCursorState, client_tick: ClientTick) { - if self.animation_state.action != state { + if self.cursor_state != state { + self.cursor_state = state; + self.animation_state.action_base_offset = usize::from(self.cursor_state); self.animation_state.start_time = client_tick; } - - self.animation_state.action = state; } #[cfg_attr(feature = "debug", korangar_debug::profile("render mouse cursor"))] @@ -106,7 +103,7 @@ impl MouseCursor { Color::WHITE, false, ), - Grabbed::Action(sprite, actions, animation_state) => actions.render( + Grabbed::Action(sprite, actions, animation_state) => actions.render_sprite( renderer, &sprite, &animation_state, @@ -118,13 +115,13 @@ impl MouseCursor { } } - // TODO: figure out how this is actually supposed to work - let direction = match self.animation_state.action { + // TODO: Figure out how this is actually supposed to work + let direction = match self.cursor_state { MouseCursorState::Default | MouseCursorState::Click | MouseCursorState::RotateCamera => 0, _ => 7, }; - self.actions.render( + self.actions.render_sprite( renderer, &self.sprite, &self.animation_state, diff --git a/korangar/src/interface/elements/miscellanious/skill.rs b/korangar/src/interface/elements/miscellanious/skill.rs index 358dcee7..ed143517 100644 --- a/korangar/src/interface/elements/miscellanious/skill.rs +++ b/korangar/src/interface/elements/miscellanious/skill.rs @@ -101,7 +101,7 @@ impl Element for SkillBox { renderer.render_background(CornerRadius::uniform(5.0), background_color); if let Some(skill) = &self.skill { - skill.actions.render( + skill.actions.render_sprite( renderer.renderer, &skill.sprite, &skill.animation_state, diff --git a/korangar/src/inventory/skills.rs b/korangar/src/inventory/skills.rs index ac40a552..a299e7dd 100644 --- a/korangar/src/inventory/skills.rs +++ b/korangar/src/inventory/skills.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use korangar_interface::state::{PlainRemote, PlainTrackedState, TrackedState}; use ragnarok_packets::{ClientTick, SkillId, SkillInformation, SkillLevel, SkillType}; -use crate::loaders::{ActionLoader, ActionType, Actions, AnimationState, Sprite, SpriteLoader}; +use crate::loaders::{ActionLoader, Sprite, SpriteLoader}; +use crate::world::{Actions, SpriteAnimationState}; #[derive(Clone, Debug)] pub struct Skill { @@ -13,7 +14,7 @@ pub struct Skill { pub skill_name: String, pub sprite: Arc, pub actions: Arc, - pub animation_state: AnimationState, + pub animation_state: SpriteAnimationState, } #[derive(Default)] @@ -22,7 +23,13 @@ pub struct SkillTree { } impl SkillTree { - pub fn fill(&mut self, sprite_loader: &mut SpriteLoader, action_loader: &mut ActionLoader, skill_data: Vec) { + pub fn fill( + &mut self, + sprite_loader: &mut SpriteLoader, + action_loader: &mut ActionLoader, + skill_data: Vec, + client_tick: ClientTick, + ) { let skills = skill_data .into_iter() .map(|skill_data| { @@ -37,8 +44,7 @@ impl SkillTree { skill_name: skill_data.skill_name, sprite, actions, - // FIX: give correct client tick - animation_state: AnimationState::new(ActionType::Idle, ClientTick(0)), + animation_state: SpriteAnimationState::new(client_tick), } }) .collect(); diff --git a/korangar/src/loaders/action/mod.rs b/korangar/src/loaders/action/mod.rs index 729d606a..788e06b3 100644 --- a/korangar/src/loaders/action/mod.rs +++ b/korangar/src/loaders/action/mod.rs @@ -1,217 +1,22 @@ use std::num::{NonZeroU32, NonZeroUsize}; -use std::ops::Mul; use std::sync::Arc; -use cgmath::{Array, Vector2}; -use derive_new::new; -use korangar_audio::{AudioEngine, SoundEffectKey}; +use korangar_audio::AudioEngine; #[cfg(feature = "debug")] use korangar_debug::logging::{print_debug, Colorize, Timer}; -use korangar_interface::elements::{ElementCell, PrototypeElement}; -use korangar_util::container::{Cacheable, SimpleCache}; +use korangar_util::container::SimpleCache; use korangar_util::FileLoader; use ragnarok_bytes::{ByteReader, FromBytes}; -use ragnarok_formats::action::{Action, ActionsData}; +use ragnarok_formats::action::ActionsData; use ragnarok_formats::version::InternalVersion; -use ragnarok_packets::ClientTick; use super::error::LoadError; -use super::Sprite; -use crate::graphics::Color; -use crate::interface::application::InterfaceSettings; -use crate::interface::layout::{ScreenClip, ScreenPosition, ScreenSize}; use crate::loaders::{GameFileLoader, FALLBACK_ACTIONS_FILE}; -use crate::renderer::SpriteRenderer; +use crate::world::{ActionEvent, Actions}; const MAX_CACHE_COUNT: u32 = 256; const MAX_CACHE_SIZE: usize = 64 * 1024 * 1024; -// TODO: NHA The numeric value of action types are based on the EntityType! -// For example "Dead" is 8 for the PC and 4 for a monster. -// This means we need to refactor the AnimationState, so that the mouse -// uses a different animation state struct (since we can't do simple usize -// conversions). -#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum ActionType { - #[default] - Idle = 0, - Walk = 1, - Dead = 8, -} - -impl From for usize { - fn from(value: ActionType) -> Self { - value as usize - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub enum ActionEvent { - /// Start playing a WAV sound file. - Sound { key: SoundEffectKey }, - /// An attack event when the "flinch" animation is played. - Attack, - /// Start playing a WAV sound file. - Unknown, -} - -impl PrototypeElement for ActionEvent { - fn to_element(&self, display: String) -> ElementCell { - match self { - Self::Sound { .. } => PrototypeElement::to_element(&"Sound", display), - Self::Attack => PrototypeElement::to_element(&"Attack", display), - Self::Unknown => PrototypeElement::to_element(&"Unknown", display), - } - } -} - -#[derive(Clone, Debug, new)] -pub struct AnimationState { - pub action: T, - pub start_time: ClientTick, - #[new(default)] - pub time: u32, - #[new(default)] - pub duration: Option, - #[new(default)] - pub factor: Option, -} - -impl AnimationState { - pub fn idle(&mut self, client_tick: ClientTick) { - self.action = ActionType::Idle; - self.start_time = client_tick; - self.duration = None; - self.factor = None; - } - - pub fn walk(&mut self, movement_speed: usize, client_tick: ClientTick) { - self.action = ActionType::Walk; - self.start_time = client_tick; - self.duration = None; - self.factor = Some(movement_speed as f32 * 100.0 / 150.0); - } - - pub fn dead(&mut self, client_tick: ClientTick) { - self.action = ActionType::Dead; - self.start_time = client_tick; - self.duration = None; - self.factor = None; - } -} - -impl AnimationState { - pub fn update(&mut self, client_tick: ClientTick) { - let mut time = client_tick.0.saturating_sub(self.start_time.0); - - // TODO: make everything have a duration so that we can update the start_time - // from time to time so that animations won't start to drop frames as - // soon as start_time - client_tick can no longer be stored in an f32 - // accurately. When fixed remove set_start_time in MouseCursor. - if let Some(duration) = self.duration - && time > duration - { - //self.action = self.next_action; - self.start_time = client_tick; - self.duration = None; - - time = 0; - } - - self.time = time; - } -} - -#[derive(Debug, PrototypeElement)] -pub struct Actions { - pub actions: Vec, - pub delays: Vec, - #[hidden_element] - pub events: Vec, - #[cfg(feature = "debug")] - actions_data: ActionsData, -} - -impl Actions { - pub fn render( - &self, - renderer: &impl SpriteRenderer, - sprite: &Sprite, - animation_state: &AnimationState, - position: ScreenPosition, - camera_direction: usize, - color: Color, - application: &InterfaceSettings, - ) where - T: Into + Copy, - { - let direction = camera_direction % 8; - let animation_action = animation_state.action.into() * 8 + direction; - let action = &self.actions[animation_action % self.actions.len()]; - let delay = self.delays[animation_action % self.delays.len()]; - - let factor = animation_state - .factor - .map(|factor| delay * (factor / 5.0)) - .unwrap_or_else(|| delay * 50.0); - - let frame = animation_state - .duration - .map(|duration| animation_state.time * action.motions.len() as u32 / duration) - .unwrap_or_else(|| (animation_state.time as f32 / factor) as u32); - // TODO: work out how to avoid losing digits when casting timing to an f32. When - // fixed remove set_start_time in MouseCursor. - - let motion = &action.motions[frame as usize % action.motions.len()]; - - for sprite_clip in &motion.sprite_clips { - // `get` instead of a direct index in case a fallback was loaded - let Some(texture) = sprite.textures.get(sprite_clip.sprite_number as usize) else { - return; - }; - - let offset = sprite_clip.position.map(|component| component as f32); - let dimesions = sprite_clip - .size - .unwrap_or_else(|| { - let image_size = texture.get_size(); - Vector2::new(image_size.width, image_size.height) - }) - .map(|component| component as f32); - let zoom = sprite_clip.zoom.unwrap_or(1.0) * application.get_scaling_factor(); - let zoom2 = sprite_clip.zoom2.unwrap_or_else(|| Vector2::from_value(1.0)); - - let final_size = dimesions.zip(zoom2, f32::mul) * zoom; - let final_position = Vector2::new(position.left, position.top) + offset - final_size / 2.0; - - let final_size = ScreenSize { - width: final_size.x, - height: final_size.y, - }; - - let final_position = ScreenPosition { - left: final_position.x, - top: final_position.y, - }; - - let screen_clip = ScreenClip { - left: 0.0, - top: 0.0, - right: f32::MAX, - bottom: f32::MAX, - }; - - renderer.render_sprite(texture.clone(), final_position, final_size, screen_clip, color, false); - } - } -} - -impl Cacheable for Actions { - fn size(&self) -> usize { - size_of_val(&self.actions) - } -} - pub struct ActionLoader { game_file_loader: Arc, audio_engine: Arc>, diff --git a/korangar/src/loaders/animation/mod.rs b/korangar/src/loaders/animation/mod.rs index 02da31b3..533ce41a 100644 --- a/korangar/src/loaders/animation/mod.rs +++ b/korangar/src/loaders/animation/mod.rs @@ -9,8 +9,8 @@ use korangar_util::container::SimpleCache; use num::Zero; use super::error::LoadError; -use crate::loaders::{ActionEvent, ActionLoader, SpriteLoader}; -use crate::world::{Animation, AnimationData, AnimationFrame, AnimationFramePart, AnimationPair}; +use crate::loaders::{ActionLoader, SpriteLoader}; +use crate::world::{ActionEvent, Animation, AnimationData, AnimationFrame, AnimationFramePart, AnimationPair}; use crate::{Color, EntityType}; const MAX_CACHE_COUNT: u32 = 256; diff --git a/korangar/src/main.rs b/korangar/src/main.rs index a03f460e..6c3ec9cf 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -994,9 +994,6 @@ impl Client { self.particle_holder.clear(); let _ = self.networking_system.map_loaded(); - // TODO: This is just a workaround until I find a better solution to make the - // cursor always look correct. - self.mouse_cursor.set_start_time(client_tick); self.game_timer.set_client_tick(client_tick); } NetworkEvent::CharacterCreated { character_information } => { @@ -1106,10 +1103,6 @@ impl Client { self.effect_holder.clear(); self.point_light_manager.clear(); let _ = self.networking_system.map_loaded(); - - // TODO: This is just a workaround until I find a better solution to make the - // cursor always look correct. - self.mouse_cursor.set_start_time(client_tick); } NetworkEvent::SetPlayerPosition(player_position) => { let player_position = Vector2::new(player_position.x, player_position.y); @@ -1195,7 +1188,7 @@ impl Client { } NetworkEvent::SkillTree(skill_information) => { self.player_skill_tree - .fill(&mut self.sprite_loader, &mut self.action_loader, skill_information); + .fill(&mut self.sprite_loader, &mut self.action_loader, skill_information, client_tick); } NetworkEvent::UpdateEquippedPosition { index, equipped_position } => { self.player_inventory.update_equipped_position(index, equipped_position); diff --git a/korangar/src/world/action/mod.rs b/korangar/src/world/action/mod.rs new file mode 100644 index 00000000..f9e08e57 --- /dev/null +++ b/korangar/src/world/action/mod.rs @@ -0,0 +1,131 @@ +use std::ops::Mul; + +use cgmath::{Array, Vector2}; +use derive_new::new; +use korangar_audio::SoundEffectKey; +use korangar_interface::elements::{ElementCell, PrototypeElement}; +use korangar_util::container::Cacheable; +use ragnarok_formats::action::{Action, ActionsData}; +use ragnarok_packets::ClientTick; + +use crate::graphics::Color; +use crate::interface::application::InterfaceSettings; +use crate::interface::layout::{ScreenClip, ScreenPosition, ScreenSize}; +use crate::loaders::Sprite; +use crate::renderer::SpriteRenderer; + +#[derive(Clone, Debug, new)] +pub struct SpriteAnimationState { + #[new(default)] + pub action_base_offset: usize, + pub start_time: ClientTick, + #[new(default)] + pub time: u32, +} + +impl SpriteAnimationState { + pub fn update(&mut self, client_tick: ClientTick) { + self.time = client_tick.0.wrapping_sub(self.start_time.0); + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum ActionEvent { + /// Start playing a WAV sound file. + Sound { key: SoundEffectKey }, + /// An attack event when the "flinch" animation is played. + Attack, + /// Start playing a WAV sound file. + Unknown, +} + +impl PrototypeElement for ActionEvent { + fn to_element(&self, display: String) -> ElementCell { + match self { + Self::Sound { .. } => PrototypeElement::to_element(&"Sound", display), + Self::Attack => PrototypeElement::to_element(&"Attack", display), + Self::Unknown => PrototypeElement::to_element(&"Unknown", display), + } + } +} + +#[derive(Debug, PrototypeElement)] +pub struct Actions { + pub actions: Vec, + pub delays: Vec, + #[hidden_element] + pub events: Vec, + #[cfg(feature = "debug")] + pub actions_data: ActionsData, +} + +impl Actions { + pub fn render_sprite( + &self, + renderer: &impl SpriteRenderer, + sprite: &Sprite, + animation_state: &SpriteAnimationState, + position: ScreenPosition, + camera_direction: usize, + color: Color, + application: &InterfaceSettings, + ) { + let direction = camera_direction % 8; + let animation_action = animation_state.action_base_offset * 8 + direction; + let action = &self.actions[animation_action % self.actions.len()]; + let delay = self.delays[animation_action % self.delays.len()]; + let factor = delay * 50.0; + + // We must use f64 here, so that the microsecond u32 value of + // `animation_state.time` can always be properly represented. + let frame = (f64::from(animation_state.time) / f64::from(factor)) as usize; + + let motion = &action.motions[frame % action.motions.len()]; + + for sprite_clip in &motion.sprite_clips { + // `get` instead of a direct index in case a fallback was loaded + let Some(texture) = sprite.textures.get(sprite_clip.sprite_number as usize) else { + return; + }; + + let offset = sprite_clip.position.map(|component| component as f32); + let dimensions = sprite_clip + .size + .unwrap_or_else(|| { + let image_size = texture.get_size(); + Vector2::new(image_size.width, image_size.height) + }) + .map(|component| component as f32); + let zoom = sprite_clip.zoom.unwrap_or(1.0) * application.get_scaling_factor(); + let zoom2 = sprite_clip.zoom2.unwrap_or_else(|| Vector2::from_value(1.0)); + + let final_size = dimensions.zip(zoom2, f32::mul) * zoom; + let final_position = Vector2::new(position.left, position.top) + offset - final_size / 2.0; + + let final_size = ScreenSize { + width: final_size.x, + height: final_size.y, + }; + + let final_position = ScreenPosition { + left: final_position.x, + top: final_position.y, + }; + + let screen_clip = ScreenClip { + left: 0.0, + top: 0.0, + right: f32::MAX, + bottom: f32::MAX, + }; + + renderer.render_sprite(texture.clone(), final_position, final_size, screen_clip, color, false); + } + } +} + +impl Cacheable for Actions { + fn size(&self) -> usize { + size_of_val(&self.actions) + } +} diff --git a/korangar/src/world/animation/mod.rs b/korangar/src/world/animation/mod.rs index 32762ea2..6e690cb5 100644 --- a/korangar/src/world/animation/mod.rs +++ b/korangar/src/world/animation/mod.rs @@ -3,17 +3,130 @@ use std::sync::Arc; use cgmath::{Array, Matrix4, Point3, Transform, Vector2, Vector3, Zero}; use korangar_interface::elements::PrototypeElement; use korangar_util::container::Cacheable; -use ragnarok_packets::{Direction, EntityId}; +use ragnarok_packets::{ClientTick, Direction, EntityId}; #[cfg(feature = "debug")] use crate::graphics::DebugRectangleInstruction; use crate::graphics::{Color, EntityInstruction}; -use crate::loaders::{ActionEvent, ActionType, Actions, AnimationState, Sprite}; -use crate::world::{Camera, EntityType}; +use crate::loaders::Sprite; +use crate::world::{ActionEvent, Actions, Camera, EntityType}; const TILE_SIZE: f32 = 10.0; const SPRITE_SCALE: f32 = 1.4; +#[allow(dead_code)] +#[derive(Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum AnimationActionType { + Attack1, + Attack2, + Attack3, + Die, + Freeze1, + Freeze2, + Hurt, + #[default] + Idle, + Pickup, + ReadyFight, + Sit, + Skill, + Special, + Walk, +} + +impl AnimationActionType { + pub fn action_base_offset(&self, entity_type: EntityType) -> usize { + match entity_type { + EntityType::Hidden | EntityType::Player => match self { + AnimationActionType::Idle => 0, + AnimationActionType::Walk => 1, + AnimationActionType::Sit => 2, + AnimationActionType::Pickup => 3, + AnimationActionType::ReadyFight => 4, + AnimationActionType::Attack1 => 5, + AnimationActionType::Hurt => 6, + AnimationActionType::Freeze1 => 7, + AnimationActionType::Die => 8, + AnimationActionType::Freeze2 => 9, + AnimationActionType::Attack2 => 10, + AnimationActionType::Attack3 => 11, + AnimationActionType::Skill => 12, + _ => 0, + }, + EntityType::Npc | EntityType::Monster => match self { + AnimationActionType::Idle => 0, + AnimationActionType::Walk => 1, + AnimationActionType::Attack1 => 2, + AnimationActionType::Hurt => 3, + AnimationActionType::Die => 4, + _ => 0, + }, + EntityType::Warp => 0, + } + } +} + +#[derive(Clone, Debug)] +pub struct AnimationState { + pub action_type: AnimationActionType, + pub action_base_offset: usize, + pub start_time: ClientTick, + pub time: u32, + pub duration: Option, + pub factor: Option, +} + +impl AnimationState { + pub fn new(start_time: ClientTick) -> Self { + Self { + action_type: AnimationActionType::Idle, + action_base_offset: 0, + start_time, + time: 0, + duration: None, + factor: None, + } + } + + pub fn idle(&mut self, entity_type: EntityType, client_tick: ClientTick) { + self.action_type = AnimationActionType::Idle; + self.action_base_offset = self.action_type.action_base_offset(entity_type); + self.start_time = client_tick; + self.duration = None; + self.factor = None; + } + + pub fn walk(&mut self, entity_type: EntityType, movement_speed: usize, client_tick: ClientTick) { + self.action_type = AnimationActionType::Walk; + self.action_base_offset = self.action_type.action_base_offset(entity_type); + self.start_time = client_tick; + self.duration = None; + self.factor = Some(movement_speed as f32 * 100.0 / 150.0 / 5.0); + } + + pub fn dead(&mut self, entity_type: EntityType, client_tick: ClientTick) { + self.action_type = AnimationActionType::Die; + self.action_base_offset = self.action_type.action_base_offset(entity_type); + self.start_time = client_tick; + self.duration = None; + self.factor = None; + } + + pub fn update(&mut self, client_tick: ClientTick) { + self.time = client_tick.0.wrapping_sub(self.start_time.0); + } + + pub fn is_finished(&self) -> bool { + if let Some(duration) = self.duration + && self.time > duration + { + true + } else { + false + } + } +} + #[derive(Clone, PrototypeElement)] pub struct AnimationData { pub animation_pair: Vec, @@ -84,8 +197,8 @@ impl Default for AnimationFramePart { impl AnimationData { pub fn get_frame(&self, animation_state: &AnimationState, camera: &dyn Camera, direction: Direction) -> &AnimationFrame { let camera_direction = camera.camera_direction(); - let direction_usize = (camera_direction + usize::from(direction)) & 7; - let animation_action_index = animation_state.action as usize * 8 + direction_usize; + let direction = (camera_direction + usize::from(direction)) & 7; + let animation_action_index = animation_state.action_type.action_base_offset(self.entity_type) * 8 + direction; let delay_index = animation_action_index % self.delays.len(); let animation_index = animation_action_index % self.animations.len(); @@ -93,22 +206,17 @@ impl AnimationData { let delay = self.delays[delay_index]; let animation = &self.animations[animation_index]; - let factor = animation_state - .factor - .map(|factor| delay * (factor / 5.0)) - .unwrap_or_else(|| delay * 50.0); + let factor = animation_state.factor.map(|factor| delay * factor).unwrap_or_else(|| delay * 50.0); let frame_time = animation_state .duration .map(|duration| animation_state.time * animation.frames.len() as u32 / duration) .unwrap_or_else(|| (animation_state.time as f32 / factor) as u32); - // TODO: Work out how to avoid losing digits when casting time to an f32. When - // fixed remove set_start_time in MouseCursor. let frame_index = frame_time as usize % animation.frames.len(); // Remove Doridori animation from Player - if self.entity_type == EntityType::Player && animation_state.action == ActionType::Idle { + if self.entity_type == EntityType::Player && animation_state.action_type == AnimationActionType::Idle { &animation.frames[0] } else { &animation.frames[frame_index] diff --git a/korangar/src/world/entity/mod.rs b/korangar/src/world/entity/mod.rs index c22c5466..2ddadd9d 100644 --- a/korangar/src/world/entity/mod.rs +++ b/korangar/src/world/entity/mod.rs @@ -22,13 +22,13 @@ use crate::interface::application::InterfaceSettings; use crate::interface::layout::{ScreenPosition, ScreenSize}; use crate::interface::theme::GameTheme; use crate::interface::windows::WindowCache; -use crate::loaders::{ActionEvent, ActionLoader, ActionType, AnimationLoader, AnimationState, GameFileLoader, ScriptLoader, SpriteLoader}; +use crate::loaders::{ActionLoader, AnimationLoader, GameFileLoader, ScriptLoader, SpriteLoader}; use crate::renderer::GameInterfaceRenderer; #[cfg(feature = "debug")] use crate::renderer::MarkerRenderer; #[cfg(feature = "debug")] use crate::world::MarkerIdentifier; -use crate::world::{AnimationData, Camera, Map}; +use crate::world::{ActionEvent, AnimationActionType, AnimationData, AnimationState, Camera, Map}; #[cfg(feature = "debug")] use crate::{Buffer, Color, ModelVertex}; @@ -69,13 +69,26 @@ pub struct Step { arrival_timestamp: u32, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EntityType { - Warp, Hidden, - Player, - Npc, Monster, + Npc, + Player, + Warp, +} + +impl From for EntityType { + fn from(value: usize) -> Self { + match value { + 45 => EntityType::Warp, + 111 => EntityType::Hidden, // TODO: check that this is correct + 0..=44 | 4000..=5999 => EntityType::Player, + 46..=999 | 10000..=19999 => EntityType::Npc, + 1000..=3999 | 20000..=29999 => EntityType::Monster, + _ => EntityType::Npc, + } + } } #[derive(Copy, Clone, Default)] @@ -354,23 +367,14 @@ impl Common { let sex = entity_data.sex; let active_movement = None; - - let entity_type = match job_id { - 45 => EntityType::Warp, - 111 => EntityType::Hidden, // TODO: check that this is correct - // 111 | 139 => None, - 0..=44 | 4000..=5999 => EntityType::Player, - 46..=999 | 10000..=19999 => EntityType::Npc, - 1000..=3999 | 20000..=29999 => EntityType::Monster, - _ => EntityType::Npc, - }; + let entity_type = job_id.into(); let entity_part_files = get_entity_part_files(script_loader, entity_type, job_id, sex, Some(head)); let animation_data = animation_loader .get(sprite_loader, action_loader, entity_type, &entity_part_files) .unwrap(); let details = ResourceState::Unavailable; - let animation_state = AnimationState::new(ActionType::Idle, client_tick); + let animation_state = AnimationState::new(client_tick); let mut common = Self { grid_position, @@ -474,7 +478,7 @@ impl Common { self.grid_position = position; self.position = map.get_world_position(position); self.active_movement = None; - self.animation_state.idle(client_tick); + self.animation_state.idle(self.entity_type, client_tick); } pub fn move_from_to( @@ -531,8 +535,8 @@ impl Common { if steps.len() > 1 { self.active_movement = Movement::new(steps, starting_timestamp.0).into(); - if self.animation_state.action != ActionType::Walk { - self.animation_state.walk(self.movement_speed, starting_timestamp); + if self.animation_state.action_type != AnimationActionType::Walk { + self.animation_state.walk(self.entity_type, self.movement_speed, starting_timestamp); } } } @@ -1058,9 +1062,8 @@ impl Entity { } pub fn set_hair(&mut self, hair_id: usize) { - match self { - Self::Player(player) => player.hair_id = hair_id, - _ => (), + if let Self::Player(player) = self { + player.hair_id = hair_id } } @@ -1104,11 +1107,13 @@ impl Entity { } pub fn set_dead(&mut self, client_tick: ClientTick) { - self.get_common_mut().animation_state.dead(client_tick); + let entity_type = self.get_entity_type(); + self.get_common_mut().animation_state.dead(entity_type, client_tick); } pub fn set_idle(&mut self, client_tick: ClientTick) { - self.get_common_mut().animation_state.idle(client_tick); + let entity_type = self.get_entity_type(); + self.get_common_mut().animation_state.idle(entity_type, client_tick); } pub fn update_health(&mut self, health_points: usize, maximum_health_points: usize) { diff --git a/korangar/src/world/mod.rs b/korangar/src/world/mod.rs index 0f1688ca..30000f80 100644 --- a/korangar/src/world/mod.rs +++ b/korangar/src/world/mod.rs @@ -1,3 +1,4 @@ +mod action; mod animation; mod cameras; mod effect; @@ -9,6 +10,7 @@ mod object; mod particles; mod sound; +pub use self::action::*; pub use self::animation::*; pub use self::cameras::*; pub use self::effect::*;