engine/gavania-core/src/game/content/objects/npc.rs

397 lines
10 KiB
Rust
Raw Normal View History

2024-08-23 11:22:09 +00:00
use std::{
collections::{hash_map::Iter, HashMap},
path::Path,
};
use crate::game::content::prelude::*;
use crate::{game::game::GameHandle, Rarities};
use anyhow::Result;
use assetpath::AssetPath;
use cgmath::{vec3, Deg, InnerSpace, Vector3, Zero};
use engine::prelude::*;
const NPC_BASE_STATS: u32 = 2;
create_settings_section!(
NPCMetaInfo,
"Meta",
{
entity_file: String,
name: String,
movement_speed: f32,
aggro_radius: f32,
deaggro_radius: f32,
scale: f32,
mob_type: NPCType,
strength_per_level: f32,
intelligence_per_level: f32,
agility_per_level: f32,
}
);
create_settings_section!(
AbilitySlot0,
"AbilitySlot_0",
{
used: bool,
name: String,
rarity: Rarities,
level: f32,
[addons: String],
}
);
create_settings_section!(
AbilitySlot1,
"AbilitySlot_1",
{
used: bool,
name: String,
rarity: Rarities,
level: f32,
[addons: String],
}
);
create_settings_section!(
AbilitySlot2,
"AbilitySlot_2",
{
used: bool,
name: String,
rarity: Rarities,
level: f32,
[addons: String],
}
);
create_settings_section!(
AbilitySlot3,
"AbilitySlot_3",
{
used: bool,
name: String,
rarity: Rarities,
level: f32,
[addons: String],
}
);
pub trait NPCAbilitySlot {
fn used(&mut self) -> &mut bool;
fn name(&mut self) -> &mut String;
fn rarity(&mut self) -> &mut Rarities;
fn level(&mut self) -> &mut f32;
fn addons(&mut self) -> &mut [String];
}
impl NPCAbilitySlot for AbilitySlot0 {
fn used(&mut self) -> &mut bool {
&mut self.used
}
fn name(&mut self) -> &mut String {
&mut self.name
}
fn rarity(&mut self) -> &mut Rarities {
&mut self.rarity
}
fn level(&mut self) -> &mut f32 {
&mut self.level
}
fn addons(&mut self) -> &mut [String] {
&mut self.addons
}
}
impl NPCAbilitySlot for AbilitySlot1 {
fn used(&mut self) -> &mut bool {
&mut self.used
}
fn name(&mut self) -> &mut String {
&mut self.name
}
fn rarity(&mut self) -> &mut Rarities {
&mut self.rarity
}
fn level(&mut self) -> &mut f32 {
&mut self.level
}
fn addons(&mut self) -> &mut [String] {
&mut self.addons
}
}
impl NPCAbilitySlot for AbilitySlot2 {
fn used(&mut self) -> &mut bool {
&mut self.used
}
fn name(&mut self) -> &mut String {
&mut self.name
}
fn rarity(&mut self) -> &mut Rarities {
&mut self.rarity
}
fn level(&mut self) -> &mut f32 {
&mut self.level
}
fn addons(&mut self) -> &mut [String] {
&mut self.addons
}
}
impl NPCAbilitySlot for AbilitySlot3 {
fn used(&mut self) -> &mut bool {
&mut self.used
}
fn name(&mut self) -> &mut String {
&mut self.name
}
fn rarity(&mut self) -> &mut Rarities {
&mut self.rarity
}
fn level(&mut self) -> &mut f32 {
&mut self.level
}
fn addons(&mut self) -> &mut [String] {
&mut self.addons
}
}
create_settings_container!(
NPCSettings,
{
meta: NPCMetaInfo,
ability_0: AbilitySlot0,
ability_1: AbilitySlot1,
ability_2: AbilitySlot2,
ability_3: AbilitySlot3,
}
);
pub struct NPCFactory {
elite_npcs: HashMap<String, NPCSettings>,
normal_npcs: HashMap<String, NPCSettings>,
boss_npcs: HashMap<String, NPCSettings>,
}
impl NPCFactory {
pub fn new(npc_directory: AssetPath) -> Result<Self> {
let files = search_dir_recursively(&npc_directory.full_path(), ".npc")?;
let mut elite_npcs = HashMap::new();
let mut normal_npcs = HashMap::new();
let mut boss_npcs = HashMap::new();
for npc_file in files {
let npc_name = Path::new(&npc_file.full_path())
.file_stem()
.unwrap()
.to_str()
.unwrap()
.to_string();
let npc_setting = NPCSettings::load(npc_file)?;
match npc_setting.meta.mob_type {
NPCType::Normal => normal_npcs.insert(npc_name, npc_setting),
NPCType::Elite => elite_npcs.insert(npc_name, npc_setting),
NPCType::Boss => boss_npcs.insert(npc_name, npc_setting),
};
}
Ok(Self {
elite_npcs,
normal_npcs,
boss_npcs,
})
}
pub fn npc(&self, name: &String, npc_type: impl Into<NPCType>) -> Option<&NPCSettings> {
match npc_type.into() {
NPCType::Normal => self.normal_npcs.get(name),
NPCType::Elite => self.elite_npcs.get(name),
NPCType::Boss => self.boss_npcs.get(name),
}
}
pub fn npcs(&self, npc_type: impl Into<NPCType>) -> Iter<'_, String, NPCSettings> {
match npc_type.into() {
NPCType::Normal => self.normal_npcs.iter(),
NPCType::Elite => self.elite_npcs.iter(),
NPCType::Boss => self.boss_npcs.iter(),
}
}
fn find_setting(&self, npc: &String) -> Result<&NPCSettings> {
self.normal_npcs
.get(npc)
.or(self.elite_npcs.get(npc))
.or(self.boss_npcs.get(npc))
.ok_or(anyhow::Error::msg(format!("NPC file not found {}", npc)))
}
pub fn create_npc(
&self,
entity_manager: &mut EntityManager,
game_handle: &GameHandle,
npc: &String,
level: u32,
position: Vector3<f32>,
) -> Result<EntityObject> {
let game = game_handle.upgrade();
let npc_settings = self.find_setting(npc)?;
let mut entity_object =
entity_manager.load_entity(game.engine().assets(), &npc_settings.meta.entity_file)?;
// ========== create components ==========
// location
let location = Location::new_and_setup(&mut entity_object)?;
location.update_raw(position, Deg(Random::range_f32(0.0, 360.0)));
// scale
let scale = npc_settings.meta.scale;
location.set_scale(vec3(scale, scale, scale));
let hitbox = entity_object.get_component_mut::<HitBox>()?;
hitbox.set_radius(hitbox.radius() * scale);
hitbox.set_height(hitbox.height() * scale);
// movement
let movement = Movement::new(
npc_settings.meta.movement_speed,
entity_object.get_component_mut::<Audio>().ok(),
);
// attributes
let strength = (level as f32 * npc_settings.meta.strength_per_level) as u32;
let intelligence = (level as f32 * npc_settings.meta.intelligence_per_level) as u32;
let agility = (level as f32 * npc_settings.meta.agility_per_level) as u32;
let mut attributes = Attributes::load(
NPC_BASE_STATS + strength,
NPC_BASE_STATS + agility,
NPC_BASE_STATS + intelligence,
);
// statistics
let mut stats = Statistics::default();
stats.update(&mut attributes, &game.attribute_settings, None);
// character status
let character_status = CharacterStatus::new_full(&stats);
// name
let npc_name = NPCName::new(npc);
// entity faction
entity_object.insert_component(FactionNPC);
// npc type
match npc_settings.meta.mob_type {
NPCType::Normal => {
entity_object.insert_component(NPCNormal);
}
NPCType::Elite => {
entity_object.insert_component(NPCElite);
}
NPCType::Boss => {
entity_object.insert_component(NPCBoss);
}
}
// health bar
match npc_settings.meta.mob_type {
NPCType::Boss => {
let name = npc_name.name.clone();
entity_object.insert_component(BossHealthBar::new(&game, name)?);
}
_ => (),
}
// level
let level = Level::load(level, 0, &game.experience_settings);
// ai
let ai = SimpleAI::new(6.0, 9.0, entity_object.as_entity());
// abilities
let abilities =
AbilitySlots::load_for_npc(level.level(), npc_settings, game_handle.clone())?;
// ========== insert components ==========
entity_object.insert_component(movement);
entity_object.insert_component(attributes);
entity_object.insert_component(stats);
entity_object.insert_component(character_status);
entity_object.insert_component(level);
entity_object.insert_component(abilities);
entity_object.insert_component(npc_name);
entity_object.insert_component(ai);
// set hit box event
entity_object
.get_component_mut::<HitBox>()
.unwrap()
.set_event(move |scene, me, collider| {
let mut entity_multi_mut = scene.entities_multi_mut();
if let Ok(my_object) = entity_multi_mut.get(me) {
if let Ok(collider_object) = entity_multi_mut.get(collider) {
if collider_object.get_component::<FactionNPC>().is_ok() {
// if we hit an allied NPC, move both in parallel
let my_movement = my_object.get_component_mut::<Movement>().unwrap();
let other_movement =
collider_object.get_component_mut::<Movement>().unwrap();
if my_movement.direction().is_zero()
|| other_movement.direction().is_zero()
{
return Ok(CollisionEventType::Block);
}
let common_dir =
(my_movement.direction() + other_movement.direction()).normalize();
my_movement.set_direction(common_dir);
other_movement.set_direction(common_dir);
return Ok(CollisionEventType::Ignore);
} else if collider_object.get_component::<FactionFriendly>().is_ok() {
// friendly NPCs are ignored
return Ok(CollisionEventType::Ignore);
}
}
}
Ok(CollisionEventType::Block)
});
Ok(entity_object)
}
}