397 lines
10 KiB
Rust
397 lines
10 KiB
Rust
|
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)
|
||
|
}
|
||
|
}
|