use anyhow::Result; use assetpath::AssetPath; use cgmath::{Vector2, Vector3}; use engine::prelude::*; use std::{fmt::Debug, str::FromStr, sync::Arc, time::Duration}; use crate::{ components::{ character_status::CharacterStatus, crafting_materials::CraftingMaterials, inventory::Storable, statistics::Statistics, }, config::abilities::{AbilityLevel, AbilitySettings}, damage_type::DamageType, }; use super::{ ItemSystem, Rarities, Tooltip, ability_addon::{AbilityAddon, AbilityAddonCollection, AbilityAddonTypes}, }; pub trait Ability: Send + Sync + Clone + Default { fn create(context: &Context, asset_path: impl Into) -> Result; fn name(&self) -> &str; fn icon_path(&self) -> &AssetPath; fn cool_down(&self) -> Duration; fn mana_cost(&self) -> u32; fn mana_cost_per_level(&self) -> u32; fn damage_type(&self) -> DamageType; fn base_damage(&self) -> u32; } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CastInformation { time: PersistentDuration, location: Vector3, direction: Vector2, } #[derive(Clone)] pub struct AbilityBook { ability: A, // meta icon: Arc, rarity: Rarities, // addons addons: AbilityAddonCollection, level: u32, ability_level_settings: AbilityLevel, // cool down last_cast: Option, } impl AbilityBook { pub fn new( ability: A, icon: Arc, rarity: Rarities, ability_settings: &AbilitySettings, ) -> Self { AbilityBook { ability, // meta icon, rarity, // addons addons: AbilityAddonCollection::new(rarity, ability_settings), level: 1, ability_level_settings: ability_settings.level.clone(), // cool down last_cast: None, } } pub fn load( ability: A, icon: Arc, rarity: Rarities, addons: Vec>, ability_settings: &AbilitySettings, level: u32, ) -> Self { AbilityBook { ability, icon, rarity, addons: AbilityAddonCollection::load(addons, rarity, ability_settings), level, ability_level_settings: ability_settings.level.clone(), last_cast: None, } } pub fn check_mana(&self, character_status: &mut CharacterStatus) -> Result { let mana_costs = self.mana_cost(); Ok(character_status.use_ability(mana_costs as f32)) } #[allow(clippy::too_many_arguments)] pub fn validate_use( &mut self, now: Duration, character_status: &mut CharacterStatus, location: &Location, ) -> Result { // don't allow anything while being animation locked if let Some(cast_information) = &self.last_cast { let total_cool_down = Duration::from_secs_f32({ let d = self.ability.cool_down(); d.as_secs_f32() * (1.0 - self.addons.cool_down_reduction()) }); if (now - cast_information.time.into()) <= total_cool_down { return Ok(false); } } if !self.check_mana(character_status)? { return Ok(false); } // { // // TODO: further separation of animation types (bows, ...) // let animation_type = match self.ability.data().settings.parameter.damage_type { // DamageType::Physical => AnimationType::Attack, // _ => AnimationType::Cast, // }; // animation_info.set_animation( // animation, // draw, // Some(animation_type), // now, // true, // false, // )?; // } self.set_cast(now, location); Ok(true) } fn set_cast(&mut self, now: Duration, location: &Location) { self.last_cast = Some(CastInformation { time: now.into(), location: location.position(), direction: location.direction(), }); } pub fn last_cast(&self) -> &Option { &self.last_cast } pub fn check_cool_down(&mut self, now: Duration) -> Option { match &self.last_cast { Some(cast_information) => { let total_cool_down = Duration::from_secs_f32( self.ability.cool_down().as_secs_f32() * (1.0 - self.addons.cool_down_reduction()), ); let diff = now - cast_information.time.into(); if diff <= total_cool_down { Some((total_cool_down - diff).as_secs_f32()) } else { self.last_cast = None; None } } None => None, } } pub fn ability(&self) -> &A { &self.ability } pub fn ability_mut(&mut self) -> &mut A { &mut self.ability } pub fn addons(&self) -> &AbilityAddonCollection { &self.addons } pub fn addons_mut(&mut self) -> &mut AbilityAddonCollection { &mut self.addons } pub fn has_free_addon_slots(&self) -> bool { self.addons.has_free_addon_slots() } pub fn attached_count(&self) -> usize { self.addons.attached_count() } pub fn level(&self) -> u32 { self.level } pub fn upgrade_cost(&self) -> u32 { self.ability_level_settings.starting_cost + (self.level - 1) * self.ability_level_settings.cost_per_level } pub fn upgrade(&mut self, crafting_materials: &mut CraftingMaterials) -> bool { if crafting_materials.consume(self.rarity, self.upgrade_cost()) { self.level += 1; true } else { false } } // only used at a copy for ability upgrade tool tip pub fn dummy_uprade(&mut self) { self.level += 1; } pub fn mana_cost(&self) -> u32 { self.ability.mana_cost() + self.ability.mana_cost_per_level() * (self.level - 1) } pub fn damage(&self, statistics: &Statistics) -> u32 { // calculate damage of base ability let ability_base_damage = self.ability.base_damage(); // get bonus damage from statistics let stats_damage = match self.ability.damage_type() { DamageType::Air => statistics.air_damage.raw(), DamageType::Fire => statistics.fire_damage.raw(), DamageType::Water => statistics.water_damage.raw(), DamageType::Physical => statistics.physical_damage.raw(), }; // damage from addons multiplied with level let addon_damage = self.addons.damage() * self.level; // sum up ability_base_damage + (stats_damage * self.level) + addon_damage } pub fn into_persistent(&self) -> String { let mut base = format!("{}|{}|{}", self.ability().name(), self.rarity(), self.level); for addon in self.addons.iter().flatten() { base = format!("{}|{}_{}", base, addon.addon_type(), addon.rarity()); } base } pub fn from_persistent<'a>( mut split: impl Iterator, item_system: &ItemSystem, ) -> Result { let name = split.next().unwrap(); let rarity = Rarities::from_str(split.next().unwrap())?; let level = u32::from_str(split.next().unwrap())?; let mut addons = Vec::new(); for addon in split { let mut addon_split = addon.split('_'); let addon_type = AbilityAddonTypes::from_str(addon_split.next().unwrap())?; let addon_rarity = Rarities::from_str(addon_split.next().unwrap())?; addons.push(Some(item_system.addon(addon_rarity, addon_type))); } Ok(item_system.ability_book(name, rarity, addons, level)) } pub fn create_tooltip( &self, gui_handler: &Arc, statistics: &Statistics, position: (i32, i32), ) -> Result { let gui = GuiBuilder::from_str( gui_handler, include_str!("../../resources/book_snippet.xml"), )?; let ability_name: Arc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AbilityBook") .field("rarity", &self.rarity) .field("attached_addons", &self.attached_count()) .field("addons", &self.addons) .field("last_cast", &self.last_cast) .finish() } } impl PartialEq for AbilityBook { fn eq(&self, other: &Self) -> bool { self.ability.name() == other.ability.name() && self.rarity == other.rarity && self.addons == other.addons && self.level == other.level } } impl Storable for AbilityBook { fn rarity(&self) -> Rarities { self.rarity } fn icon(&self) -> Arc { self.icon.clone() } }