2025-02-28 07:43:35 +00:00
|
|
|
use anyhow::Result;
|
|
|
|
use assetpath::AssetPath;
|
|
|
|
use cgmath::{Vector2, Vector3};
|
|
|
|
use engine::prelude::*;
|
2025-03-03 18:06:15 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-02-28 07:43:35 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|