use engine::prelude::*; use crate::game::content::abilities::handle_npc_death; use anyhow::Result; use cgmath::{vec3, Deg, Matrix2, Vector2}; use super::{ super::prelude::*, particle_spawn::{ParticleSpawn, ParticleSpawnExecutor}, }; use crate::game::game::GameHandle; use std::time::Duration; const ODD_ANGLES: [Deg; 11] = [ Deg(0.0), Deg(10.0), Deg(-10.0), Deg(20.0), Deg(-20.0), Deg(30.0), Deg(-30.0), Deg(40.0), Deg(-40.0), Deg(50.0), Deg(-50.0), ]; const EVEN_ANGLES: [Deg; 10] = [ Deg(5.0), Deg(-5.0), Deg(15.0), Deg(-15.0), Deg(25.0), Deg(-25.0), Deg(35.0), Deg(-35.0), Deg(45.0), Deg(-45.0), ]; pub struct ProjectileMarker; impl EntityComponent for ProjectileMarker { fn name(&self) -> &str { Self::debug_name() } } impl ComponentDebug for ProjectileMarker { fn debug_name() -> &'static str { "ProjectileMarker" } } #[derive(Clone)] pub struct Projectile { data: AbilityLoader, } impl Projectile { pub fn new(data: AbilityLoader) -> Result { Ok(Projectile { data }) } fn hit_event( me: Entity, collider: Entity, owner: Entity, level: u32, addons: &AbilityAddonCollection, game_handle: &GameHandle, scene: &mut SceneContents<'_>, ) -> Result { // this handle is only needed for calculating loot bonus when creating a loot stash // it only queries all users and their loot bonus passive let s = unsafe { remove_life_time_mut(scene) }; let game = game_handle.upgrade(); let mut entity_multi_mut = scene.entities_multi_mut(); let collider_object = match entity_multi_mut.get(collider) { Ok(collider) => collider, Err(_) => return Ok(CollisionEventType::Ignore), }; let projectile_object = match entity_multi_mut.get(me) { Ok(projectile_object) => projectile_object, Err(_) => return Ok(CollisionEventType::Ignore), }; let mut collider_components = collider_object.multi_mut(); let mut projectile_components = projectile_object.multi_mut(); let my_faction = Faction::get(&mut projectile_components).unwrap(); let ability_info = projectile_components.get::()?; // don't collide with same 'Team' or neutral NPC's let collider_faction = Faction::get(&mut collider_components); if !my_faction.check_collision(collider_faction) { // if there is no entity faction, that means we hit an environment object // e.g. a tree or a stone. // we then bounce or destroy the projectile if addons.bounce() { let position = projectile_components.get::().unwrap().position(); let movement = projectile_components.get::().unwrap(); let direction = movement.direction(); let inverted = -direction; let random_angle = Deg(Random::range_f32(-60.0, 60.0)); let new_direction = Matrix2::from_angle(random_angle) * inverted; movement.set_direction(new_direction); let location_info = projectile_components.get::()?; location_info.new_direction_at(position); } return Ok(CollisionEventType::Ignore); } // skip any damage related calculation when the owner isn't present anymore if let Ok(owner_object) = entity_multi_mut.get(owner) { let owner_stats = owner_object.get_component::()?; let mut base_damage = ability_info.damage(level, addons, owner_stats); let explosion_radius = ability_info.settings.parameter.target_radius + addons.explosion_radius(); let npc_location = projectile_components.get::().unwrap(); // don't cast an explosion when the radius is 0 if explosion_radius == 0.0 { base_damage = AbilityLoader::check_for_crit(base_damage, owner_stats); super::damage_and_experience( base_damage, ability_info.settings.parameter.damage_type, &mut collider_components, owner_object.multi_mut(), s, )?; unsafe { collider_components.clear_all_usages(); } // spawn on hit particle effects if !ability_info.settings.parameter.on_hit_particles.is_empty() { let file_name = game.build_data_path(&ability_info.settings.parameter.on_hit_particles); let info = ParticleSystemInfo::load(file_name)?; s.write_event(OnHitParticles { particle_system_info: info, collision_sound: ability_info.settings.parameter.on_collision_sound.clone(), position: npc_location.position(), offset: npc_location.offset(), arc_info: None, }); } unsafe { collider_components.clear_all_usages(); } handle_npc_death(&game, s, collider, collider_components)?; } // if there is a explosion radius, create an explosion according to its radius else { let file_name = match ability_info.settings.parameter.damage_type { DamageType::Physical => todo!(), DamageType::Fire => game.build_data_path("particles/fire_explosion.particle"), DamageType::Water => game.build_data_path("particles/ice_explosion.particle"), DamageType::Air => todo!(), }; let mut info = ParticleSystemInfo::load(file_name)?; info.particles.minimum_velocity = explosion_radius / info.system.duration.as_secs_f32(); info.particles.maximum_velocity = info.particles.minimum_velocity; // spawn on hit particle effects s.write_event(OnHitParticles { particle_system_info: info, collision_sound: ability_info.settings.parameter.on_collision_sound.clone(), position: npc_location.position(), offset: npc_location.offset(), arc_info: Some(ArcInfo { radius: explosion_radius, base_damage, damage_type: ability_info.settings.parameter.damage_type, owner, }), }); } } scene.remove_entity(me)?; Ok(CollisionEventType::Ignore) } pub fn create_particles( info: &AbilityLoader, game_handle: &GameHandle, draw: &mut Draw, particle_system_vulkan_objects: &ParticleSystemVulkanObjects, ) -> Result { let game = game_handle.upgrade(); let file_name = game.build_data_path(&info.settings.parameter.trail); let mut particle_info = ParticleSystemInfo::load(file_name)?; particle_info.system.duration = Duration::from_secs(300).into(); particle_info.particles.minimum_velocity = info.settings.parameter.speed / 3.0; particle_info.particles.maximum_velocity = info.settings.parameter.speed / 2.0; ParticleSystem::new_with_vk_objects( particle_info, game.engine(), particle_system_vulkan_objects, draw, ) } } impl Ability for Projectile { fn data(&self) -> &AbilityLoader { &self.data } fn name(&self) -> &str { self.data.name() } fn execute( &self, _owner: Entity, owner_components: &mut MultiMut<'_>, direction: Vector2, book: &AbilityBook, game_handle: &GameHandle, entities: &mut Entities<'_>, events: &mut ContentEvents<'_>, ) -> Result<()> { let mut projectile_count = 1 + book.addons().additional_projectiles() as usize; let angles: &[Deg] = if (projectile_count % 2) == 0 { projectile_count = projectile_count.min(EVEN_ANGLES.len()); &EVEN_ANGLES } else { projectile_count = projectile_count.min(ODD_ANGLES.len()); &ODD_ANGLES }; let owner_hitbox = owner_components.get::()?; let owner_location = owner_components.get::()?; let projectile_size = book.addons().size(); let projectile_speed = book.addons().projectile_speed(); let projectile_distance = self.data.settings.parameter.distance + book.addons().distance(); let ability_data = book.ability().data(); let game = game_handle.upgrade(); (0..projectile_count) .into_iter() .try_for_each(|i| -> Result<()> { let angle = &angles[i]; let mut projectile_entity = game .entity_manager() .load_entity(game.engine().assets(), &self.data.settings.meta.entity)?; { // alter the hitbox parameters let projectile_hitbox = projectile_entity.get_component_mut::()?; // Multiply the radius with potential ability modifications projectile_hitbox.set_radius(projectile_size * projectile_hitbox.radius()); // set on hit event for the hitbox (only the server side will check for it) projectile_hitbox.set_event({ let owner = _owner; let level = book.level(); let addons = book.addons().clone(); let game_handle = game_handle.clone(); move |scene, me, collider| { Self::hit_event( me, collider, owner, level, &addons, &game_handle, scene, ) } }); let hitbox_radius = projectile_hitbox.radius(); let projectile_location = Location::new_and_setup(&mut projectile_entity)?; // set projectile to owner location and set rotation accordingly projectile_location.set_scale_uniform(projectile_size); let direction = Matrix2::from_angle(*angle) * direction; let position = owner_location.position() + direction.extend(0.0) * (hitbox_radius * 1.1 + owner_hitbox.radius()); projectile_location.update(position, direction); // apply height offset projectile_location.set_offset(vec3( 0.0, 0.0, ability_data.settings.parameter.height_offset, )); let mut projectile_movement = Movement::new( self.data.settings.parameter.speed + projectile_speed, projectile_entity.get_component_mut::