engine/gavania-core/src/game/content/abilities/projectile.rs
2024-08-25 09:11:52 +02:00

371 lines
13 KiB
Rust

use engine::prelude::*;
use entity_manager::*;
use rpg_components::{
components::{abilityloader::AbilityLoader, statistics::Statistics},
damage_type::DamageType,
items::{ability_addon::AbilityAddonCollection, ability_book::AbilityBook},
};
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<f32>; 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<f32>; 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;
impl Projectile {
fn hit_event(
me: Entity,
collider: Entity,
owner: Entity,
level: u32,
addons: &AbilityAddonCollection,
game_handle: &GameHandle,
scene: &mut SceneContents<'_>,
) -> Result<CollisionEventType> {
// 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::<AbilityLoader>()?;
// 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::<Location>().unwrap().position();
let movement = projectile_components.get::<Movement>().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::<AbilityLocationInfo>()?;
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::<Statistics>()?;
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::<Location>().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<ParticleSystem> {
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 Projectile {
pub fn execute(
ability: &AbilityLoader,
_owner: Entity,
owner_components: &mut MultiMut<'_>,
direction: Vector2<f32>,
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<f32>] = 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::<HitBox>()?;
let owner_location = owner_components.get::<Location>()?;
let projectile_size = book.addons().size();
let projectile_speed = book.addons().projectile_speed();
let projectile_distance = ability.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(), &ability.settings.meta.entity)?;
{
// alter the hitbox parameters
let projectile_hitbox = projectile_entity.get_component_mut::<HitBox>()?;
// 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(
ability.settings.parameter.speed + projectile_speed,
projectile_entity.get_component_mut::<Audio>().ok(),
);
if !ability_data.settings.parameter.idle_sound.is_empty() {
let audio = projectile_entity.get_component_mut::<Audio>().unwrap();
audio.add_custom_sound(
game.build_data_path(&ability_data.settings.parameter.idle_sound),
"idle_sound",
)?;
};
// set movement
projectile_movement.set_direction(direction);
let ability_location_info = AbilityLocationInfo::new(
position,
projectile_distance,
projectile_entity.get_component_mut::<Audio>().unwrap(),
);
if !ability_data.settings.parameter.trail.is_empty() {
events.write_event(ParticleSpawn {
entity: projectile_entity.as_entity(),
info: ability_data.clone(),
executor: ParticleSpawnExecutor::Projectile,
});
}
projectile_entity.insert_component(ability_location_info);
projectile_entity.insert_component(ability.clone());
projectile_entity.insert_component(projectile_movement);
projectile_entity.insert_component(ProjectileMarker);
Faction::copy(owner_components, &mut projectile_entity);
}
entities.add_entity(projectile_entity)?;
Ok(())
})?;
Ok(())
}
}