rpg_base/rpg_components/src/items/ability_book.rs

374 lines
10 KiB
Rust
Raw Normal View History

2025-02-28 07:43:35 +00:00
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<AssetPath>) -> Result<Self>;
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<f32>,
direction: Vector2<f32>,
}
#[derive(Clone)]
pub struct AbilityBook<A: Ability> {
ability: A,
// meta
icon: Arc<Image>,
rarity: Rarities,
// addons
addons: AbilityAddonCollection,
level: u32,
ability_level_settings: AbilityLevel,
// cool down
last_cast: Option<CastInformation>,
}
impl<A: Ability> AbilityBook<A> {
pub fn new(
ability: A,
icon: Arc<Image>,
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<Image>,
rarity: Rarities,
addons: Vec<Option<AbilityAddon>>,
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<bool> {
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<bool> {
// 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<CastInformation> {
&self.last_cast
}
pub fn check_cool_down(&mut self, now: Duration) -> Option<f32> {
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 = &'a str>,
item_system: &ItemSystem<A>,
) -> Result<Self> {
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<GuiHandler>,
statistics: &Statistics,
position: (i32, i32),
) -> Result<Tooltip> {
let gui = GuiBuilder::from_str(
gui_handler,
include_str!("../../resources/book_snippet.xml"),
)?;
let ability_name: Arc<Label> = gui.element("ability_name")?;
let rarity_label: Arc<Label> = gui.element("rarity_label")?;
let inspector_grid: Arc<Grid> = gui.element("item_grid")?;
let ability_icon: Arc<Icon> = gui.element("ability_icon")?;
let slot_info: Arc<Label> = gui.element("slot_info")?;
let level: Arc<Label> = gui.element("level")?;
inspector_grid.change_position_unscaled(position.0, position.1)?;
ability_icon.set_icon(&self.icon())?;
ability_name.set_text(self.ability().name())?;
rarity_label.set_text(&format!("{}", self.rarity()))?;
level.set_text(&format!("Lvl: {}", self.level()))?;
slot_info.set_text(&format!(
"Slots: {}/{}",
self.attached_count(),
self.addons().len()
))?;
let mana_costs: Arc<Label> = gui.element("mana_costs")?;
let damage: Arc<Label> = gui.element("damage")?;
let cooldown: Arc<Label> = gui.element("cooldown")?;
mana_costs.set_text(self.mana_cost())?;
damage.set_text(self.damage(statistics))?;
damage.set_text_color(self.ability.damage_type().into())?;
cooldown.set_text(format!(
"{:.1} s",
self.ability.cool_down().as_secs_f32() * (1.0 - self.addons().cool_down_reduction())
))?;
Ok(Tooltip::new(inspector_grid, gui, gui_handler.clone()))
}
}
impl<A: Ability> Debug for AbilityBook<A> {
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<A: Ability> PartialEq for AbilityBook<A> {
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<A: Ability> Storable for AbilityBook<A> {
fn rarity(&self) -> Rarities {
self.rarity
}
fn icon(&self) -> Arc<Image> {
self.icon.clone()
}
}