diff --git a/assets/common/abilities/axe/doublestrike.ron b/assets/common/abilities/axe/doublestrike.ron index 932a87780e..042e9f0b72 100644 --- a/assets/common/abilities/axe/doublestrike.ron +++ b/assets/common/abilities/axe/doublestrike.ron @@ -2,7 +2,7 @@ ComboMelee( stage_data: [ ( stage: 1, - base_damage: 90, + base_damage: 110, base_poise_damage: 12, damage_increase: 10, poise_damage_increase: 0, diff --git a/assets/common/abilities/axesimple/dash.ron b/assets/common/abilities/axesimple/dash.ron index b9e0e8f05a..23074786f2 100644 --- a/assets/common/abilities/axesimple/dash.ron +++ b/assets/common/abilities/axesimple/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 3.0, swing_duration: 0.35, recover_duration: 1.2, + ori_modifier: 0.3, charge_through: false, is_interruptible: true, damage_kind: Slashing, diff --git a/assets/common/abilities/custom/basilisk/dash.ron b/assets/common/abilities/custom/basilisk/dash.ron index b95514175c..f4e8c56c40 100644 --- a/assets/common/abilities/custom/basilisk/dash.ron +++ b/assets/common/abilities/custom/basilisk/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.0, swing_duration: 0.1, recover_duration: 1.0, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/birdlargebasic/dash.ron b/assets/common/abilities/custom/birdlargebasic/dash.ron index 0760657973..7e600ce698 100644 --- a/assets/common/abilities/custom/birdlargebasic/dash.ron +++ b/assets/common/abilities/custom/birdlargebasic/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 3.0, swing_duration: 0.1, recover_duration: 0.7, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/minotaur/charge.ron b/assets/common/abilities/custom/minotaur/charge.ron index e313f18a92..b8324ee500 100644 --- a/assets/common/abilities/custom/minotaur/charge.ron +++ b/assets/common/abilities/custom/minotaur/charge.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 4.0, swing_duration: 0.1, recover_duration: 0.5, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Piercing, diff --git a/assets/common/abilities/custom/quadlowbreathe/dash.ron b/assets/common/abilities/custom/quadlowbreathe/dash.ron index d84f614549..887298f7a5 100644 --- a/assets/common/abilities/custom/quadlowbreathe/dash.ron +++ b/assets/common/abilities/custom/quadlowbreathe/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.0, swing_duration: 0.1, recover_duration: 1.0, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/quadlowquick/dash.ron b/assets/common/abilities/custom/quadlowquick/dash.ron index 13fb06b122..42838c2b98 100644 --- a/assets/common/abilities/custom/quadlowquick/dash.ron +++ b/assets/common/abilities/custom/quadlowquick/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 0.8, swing_duration: 0.1, recover_duration: 1.0, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/quadmedcharge/dash.ron b/assets/common/abilities/custom/quadmedcharge/dash.ron index 95f94277c1..75a399c0cd 100644 --- a/assets/common/abilities/custom/quadmedcharge/dash.ron +++ b/assets/common/abilities/custom/quadmedcharge/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.2, swing_duration: 0.1, recover_duration: 1.1, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/quadmedquick/dash.ron b/assets/common/abilities/custom/quadmedquick/dash.ron index ded884beb1..1bb33c9825 100644 --- a/assets/common/abilities/custom/quadmedquick/dash.ron +++ b/assets/common/abilities/custom/quadmedquick/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.0, swing_duration: 0.1, recover_duration: 1.0, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/theropodbasic/dash.ron b/assets/common/abilities/custom/theropodbasic/dash.ron index 0835777247..8552a30b75 100644 --- a/assets/common/abilities/custom/theropodbasic/dash.ron +++ b/assets/common/abilities/custom/theropodbasic/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.2, swing_duration: 0.1, recover_duration: 1.1, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/custom/tidalwarrior/scuttle.ron b/assets/common/abilities/custom/tidalwarrior/scuttle.ron index 28552b4f46..31499e4f9d 100644 --- a/assets/common/abilities/custom/tidalwarrior/scuttle.ron +++ b/assets/common/abilities/custom/tidalwarrior/scuttle.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 2.0, swing_duration: 0.1, recover_duration: 0.5, + ori_modifier: 0.3, charge_through: false, is_interruptible: false, damage_kind: Crushing, diff --git a/assets/common/abilities/hammer/charged.ron b/assets/common/abilities/hammer/charged.ron index 73871798a8..0820ef5bdf 100644 --- a/assets/common/abilities/hammer/charged.ron +++ b/assets/common/abilities/hammer/charged.ron @@ -3,8 +3,8 @@ ChargedMelee( energy_drain: 300, initial_damage: 10, scaled_damage: 160, - initial_poise_damage: 20, - scaled_poise_damage: 60, + initial_poise_damage: 5, + scaled_poise_damage: 75, initial_knockback: 5.0, scaled_knockback: 20.0, range: 3.5, diff --git a/assets/common/abilities/spear/dash.ron b/assets/common/abilities/spear/dash.ron index 837a91b629..1914b1f67d 100644 --- a/assets/common/abilities/spear/dash.ron +++ b/assets/common/abilities/spear/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.0, swing_duration: 0.1, recover_duration: 0.8, + ori_modifier: 0.3, charge_through: false, is_interruptible: true, damage_kind: Piercing, diff --git a/assets/common/abilities/sword/dash.ron b/assets/common/abilities/sword/dash.ron index 5c932b85aa..6ae5fdba0e 100644 --- a/assets/common/abilities/sword/dash.ron +++ b/assets/common/abilities/sword/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.2, swing_duration: 0.1, recover_duration: 0.5, + ori_modifier: 0.3, charge_through: true, is_interruptible: true, damage_kind: Piercing, diff --git a/assets/common/abilities/swordsimple/dash.ron b/assets/common/abilities/swordsimple/dash.ron index f231f2500b..b9ae042353 100644 --- a/assets/common/abilities/swordsimple/dash.ron +++ b/assets/common/abilities/swordsimple/dash.ron @@ -14,6 +14,7 @@ DashMelee( charge_duration: 1.2, swing_duration: 0.1, recover_duration: 0.9, + ori_modifier: 0.3, charge_through: false, is_interruptible: true, damage_kind: Piercing, diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 207940c007..7c1a2d6ebf 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -1,6 +1,7 @@ use crate::{ assets, comp::{self, buff::BuffKind, inventory::item::try_all_item_defs, AdminRole as Role, Skill}, + generation::try_all_entity_configs, npc, terrain, }; use assets::AssetExt; @@ -76,6 +77,7 @@ pub enum ChatCommand { Lantern, Light, MakeBlock, + MakeNpc, MakeSprite, Motd, Object, @@ -242,6 +244,15 @@ lazy_static! { items }; + /// List of all entity configs. Useful for tab completing + static ref ENTITY_CONFIGS: Vec = { + try_all_entity_configs() + .unwrap_or_else(|e| { + warn!(?e, "Failed to load entity configs"); + Vec::new() + }) + }; + static ref KITS: Vec = { if let Ok(kits) = KitManifest::load(KIT_MANIFEST_PATH) { kits.read().0.keys().cloned().collect() @@ -461,6 +472,14 @@ impl ChatCommand { "Make a block at your location with a color", Some(Admin), ), + ChatCommand::MakeNpc => cmd( + vec![ + Enum("entity_config", ENTITY_CONFIGS.clone(), Required), + Integer("num", 1, Optional), + ], + "Spawn entity from config near you", + Some(Admin), + ), ChatCommand::MakeSprite => cmd( vec![Enum("sprite", SPRITE_KINDS.clone(), Required)], "Make a sprite at your location", @@ -521,8 +540,8 @@ impl ChatCommand { "Set the server description", Some(Admin), ), - // Uses Message because site names can contain spaces, which would be assumed to be - // separators otherwise + // Uses Message because site names can contain spaces, + // which would be assumed to be separators otherwise ChatCommand::Site => cmd( vec![Message(Required)], "Teleport to a site", @@ -634,6 +653,7 @@ impl ChatCommand { ChatCommand::Lantern => "lantern", ChatCommand::Light => "light", ChatCommand::MakeBlock => "make_block", + ChatCommand::MakeNpc => "make_npc", ChatCommand::MakeSprite => "make_sprite", ChatCommand::Motd => "motd", ChatCommand::Object => "object", @@ -788,7 +808,6 @@ pub enum ArgumentSpec { } impl ArgumentSpec { - #[allow(clippy::nonstandard_macro_braces)] //tmp as of false positive !? pub fn usage_string(&self) -> String { match self { ArgumentSpec::PlayerName(req) => { @@ -836,9 +855,9 @@ impl ArgumentSpec { ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(), ArgumentSpec::Enum(label, _, req) => { if &Requirement::Required == req { - format! {"<{}>", label} + format!("<{}>", label) } else { - format! {"[{}]", label} + format!("[{}]", label) } }, ArgumentSpec::Boolean(label, _, req) => { diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 3295e3b821..1415a74461 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -119,6 +119,7 @@ pub enum CharacterAbility { charge_duration: f32, swing_duration: f32, recover_duration: f32, + ori_modifier: f32, charge_through: bool, is_interruptible: bool, damage_kind: DamageKind, @@ -497,6 +498,7 @@ impl CharacterAbility { charge_duration: _, ref mut swing_duration, ref mut recover_duration, + ori_modifier: _, charge_through: _, is_interruptible: _, damage_kind: _, @@ -1531,6 +1533,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { charge_duration, swing_duration, recover_duration, + ori_modifier, charge_through, is_interruptible, damage_kind, @@ -1552,6 +1555,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { charge_duration: Duration::from_secs_f32(*charge_duration), swing_duration: Duration::from_secs_f32(*swing_duration), recover_duration: Duration::from_secs_f32(*recover_duration), + ori_modifier: *ori_modifier, is_interruptible: *is_interruptible, damage_effect: *damage_effect, ability_info, diff --git a/common/src/generation.rs b/common/src/generation.rs index 00598e3877..2989e98249 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -1,5 +1,5 @@ use crate::{ - assets::{self, AssetExt}, + assets::{self, AssetExt, Error}, comp::{ self, agent, humanoid, inventory::loadout_builder::{ItemSpec, LoadoutBuilder}, @@ -140,11 +140,16 @@ impl EntityConfig { } } +/// Return all entity config specifiers +pub fn try_all_entity_configs() -> Result, Error> { + let configs = assets::load_dir::("common.entity", true)?; + Ok(configs.ids().map(|id| id.to_owned()).collect()) +} + #[derive(Clone)] pub struct EntityInfo { pub pos: Vec3, pub is_waypoint: bool, // Edge case, overrides everything else - pub is_giant: bool, pub has_agency: bool, pub alignment: Alignment, pub agent_mark: Option, @@ -170,7 +175,6 @@ impl EntityInfo { Self { pos, is_waypoint: false, - is_giant: false, has_agency: true, alignment: Alignment::Wild, agent_mark: None, @@ -307,11 +311,6 @@ impl EntityInfo { self } - pub fn into_giant(mut self) -> Self { - self.is_giant = true; - self - } - pub fn with_alignment(mut self, alignment: Alignment) -> Self { self.alignment = alignment; self @@ -382,7 +381,7 @@ impl EntityInfo { pub fn with_automatic_name(mut self) -> Self { let npc_names = NPC_NAMES.read(); - self.name = match &self.body { + let name = match &self.body { Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)), Body::QuadrupedMedium(body) => { Some(get_npc_name(&npc_names.quadruped_medium, body.species)) @@ -400,14 +399,8 @@ impl EntityInfo { Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)), Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)), _ => None, - } - .map(|s| { - if self.is_giant { - format!("Giant {}", s) - } else { - s.to_string() - } - }); + }; + self.name = name.map(str::to_owned); self } @@ -554,10 +547,10 @@ mod tests { #[test] fn test_all_entity_assets() { - // It just load everything that could - let entity_configs = assets::load_dir::("common.entity", true) - .expect("Failed to access entity directory"); - for config_asset in entity_configs.ids() { + // Get list of entity configs, load everything, validate content. + let entity_configs = + try_all_entity_configs().expect("Failed to access entity configs directory"); + for config_asset in entity_configs { // print asset name so we don't need to find errors everywhere // it'll be ignored by default so you'll see it only in case of errors // @@ -568,7 +561,7 @@ mod tests { // 2) Add try_from_asset() for LoadoutBuilder and // SkillSet builder which will return Result and we will happily // panic in validate_meta() with the name of config_asset - println!("{}:", config_asset); + println!("{}:", &config_asset); let EntityConfig { hands, @@ -577,12 +570,12 @@ mod tests { loot, meta, alignment: _alignment, // can't fail if serialized, it's a boring enum - } = EntityConfig::from_asset_expect(config_asset); + } = EntityConfig::from_asset_expect(&config_asset); - validate_hands(hands, config_asset); - validate_body_and_name(body, name, config_asset); - validate_loot(loot, config_asset); - validate_meta(meta, config_asset); + validate_hands(hands, &config_asset); + validate_body_and_name(body, name, &config_asset); + validate_loot(loot, &config_asset); + validate_meta(meta, &config_asset); } } } diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs index 17a42fec74..fe086ec68e 100644 --- a/common/src/states/dash_melee.rs +++ b/common/src/states/dash_melee.rs @@ -43,6 +43,8 @@ pub struct StaticData { pub swing_duration: Duration, /// How long the state has until exiting pub recover_duration: Duration, + /// How fast can you turn during charge + pub ori_modifier: f32, /// Whether the state can be interrupted by other abilities pub is_interruptible: bool, /// Adds an effect onto the main damage of the attack @@ -107,13 +109,14 @@ impl CharacterBehavior for Data { / self.static_data.charge_duration.as_secs_f32()) .min(1.0); - handle_orientation(data, &mut update, 0.6); + handle_orientation(data, &mut update, self.static_data.ori_modifier); handle_forced_movement(data, &mut update, ForcedMovement::Forward { strength: self.static_data.forward_speed * charge_frac.sqrt(), }); - // This logic basically just decides if a charge should end, and prevents the - // character state spamming attacks while checking if it has hit something + // This logic basically just decides if a charge should end, + // and prevents the character state spamming attacks + // while checking if it has hit something. if !self.exhausted { // Hit attempt let poise = AttackEffect::new( diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 1d3ed22303..960044b230 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1,11 +1,12 @@ //! # Implementing new commands. -//! To implement a new command, add an instance of `ChatCommand` to -//! `CHAT_COMMANDS` and provide a handler function. +//! To implement a new command provide a handler function +//! in [do_command]. use crate::{ settings::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, + sys::terrain::NpcData, wiring::{Logic, OutputFormula}, Server, SpawnPoint, StateExt, }; @@ -28,6 +29,7 @@ use common::{ depot, effect::Effect, event::{EventBus, ServerEvent}, + generation::EntityInfo, npc::{self, get_npc_name}, resources::{PlayerPhysicsSettings, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, @@ -68,6 +70,7 @@ impl ChatCommandExt for ChatCommand { } type CmdResult = Result; + /// Handler function called when the command is executed. /// # Arguments /// * `&mut Server` - the `Server` instance executing the command. @@ -140,6 +143,7 @@ fn do_command( ChatCommand::Lantern => handle_lantern, ChatCommand::Light => handle_light, ChatCommand::MakeBlock => handle_make_block, + ChatCommand::MakeNpc => handle_make_npc, ChatCommand::MakeSprite => handle_make_sprite, ChatCommand::Motd => handle_motd, ChatCommand::Object => handle_object, @@ -547,6 +551,93 @@ fn handle_make_block( } } +fn handle_make_npc( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ChatCommand, +) -> CmdResult<()> { + let (entity_config, number) = parse_args!(args, String, i8); + + let entity_config = entity_config.ok_or_else(|| action.help_string())?; + let number = match number { + Some(i8::MIN..=0) => { + return Err("Number of entities should be at least 1".to_owned()); + }, + Some(50..=i8::MAX) => { + return Err("Number of entities should be less than 50".to_owned()); + }, + Some(number) => number, + None => 1, + }; + + let rng = &mut rand::thread_rng(); + for _ in 0..number { + let comp::Pos(pos) = position(server, target, "target")?; + let entity_info = EntityInfo::at(pos).with_asset_expect(&entity_config); + match NpcData::from_entity_info(entity_info, rng) { + NpcData::Waypoint(_) => { + return Err("Waypoint spawning is not implemented".to_owned()); + }, + NpcData::Data { + loadout, + pos, + stats, + skill_set, + poise, + health, + body, + agent, + alignment, + scale, + drop_item, + } => { + let inventory = Inventory::new_with_loadout(loadout); + + let mut entity_builder = server + .state + .create_npc(pos, stats, skill_set, health, poise, inventory, body) + .with(alignment) + .with(scale) + .with(comp::Vel(Vec3::new(0.0, 0.0, 0.0))) + .with(comp::MountState::Unmounted); + + if let Some(agent) = agent { + entity_builder = entity_builder.with(agent); + } + + if let Some(drop_item) = drop_item { + entity_builder = entity_builder.with(comp::ItemDrop(drop_item)); + } + + // Some would say it's a hack, some would say it's incomplete + // simulation. But this is what we do to avoid PvP between npc. + use comp::Alignment; + let npc_group = match alignment { + Alignment::Enemy => Some(comp::group::ENEMY), + Alignment::Npc | Alignment::Tame => Some(comp::group::NPC), + Alignment::Wild | Alignment::Passive | Alignment::Owned(_) => None, + }; + if let Some(group) = npc_group { + entity_builder = entity_builder.with(group); + } + entity_builder.build(); + }, + }; + } + + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!("Spawned {} entities from config: {}", number, entity_config), + ), + ); + + Ok(()) +} + fn handle_make_sprite( server: &mut Server, _client: EcsEntity, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index ced500bbd2..de9ad0a201 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -10,10 +10,9 @@ use crate::{ SpawnPoint, Tick, }; use common::{ - comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos, Waypoint}, + comp::{self, agent, bird_medium, BehaviorCapability, ForceUpdate, Pos, Waypoint}, event::{EventBus, ServerEvent}, - generation::{get_npc_name, EntityInfo}, - npc::NPC_NAMES, + generation::EntityInfo, resources::Time, terrain::TerrainGrid, LoadoutBuilder, SkillSetBuilder, @@ -22,6 +21,7 @@ use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{SerializedTerrainChunk, ServerGeneral}; use common_state::TerrainChanges; use comp::Behavior; +use rand::Rng; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect, WriteStorage}; use std::sync::Arc; use vek::*; @@ -199,145 +199,43 @@ impl<'a> System<'a> for Sys { "Chunk spawned entity that wasn't nearby", ); - if entity.is_waypoint { - server_emitter.emit(ServerEvent::CreateWaypoint(entity.pos)); - continue; - } - - let mut body = entity.body; - let name = entity.name.unwrap_or_else(|| "Unnamed".to_string()); - let alignment = entity.alignment; - let mut stats = comp::Stats::new(name); - - let mut scale = entity.scale; - - // Replace stuff if it's a boss - if entity.is_giant { - if rand::random::() < 0.65 && entity.alignment != Alignment::Enemy { - let body_new = comp::humanoid::Body::random(); - let npc_names = NPC_NAMES.read(); - - body = comp::Body::Humanoid(body_new); - stats = comp::Stats::new(format!( - "Gentle Giant {}", - get_npc_name(&npc_names.humanoid, body_new.species) - )); - } - scale = 2.0 + rand::random::(); - } - - let EntityInfo { - skillset_asset, - main_tool, - second_tool, - loadout_asset, - make_loadout, - trading_information: economy, - .. - } = entity; - - let skill_set = { - let skillset_builder = SkillSetBuilder::default(); - if let Some(skillset_asset) = skillset_asset { - skillset_builder.with_asset_expect(&skillset_asset).build() - } else { - skillset_builder.build() - } - }; - - let loadout = { - let mut loadout_builder = LoadoutBuilder::empty(); - let rng = &mut rand::thread_rng(); - - // If main tool is passed, use it. Otherwise fallback to default tool - if let Some(main_tool) = main_tool { - loadout_builder = loadout_builder.active_mainhand(Some(main_tool)); - } else { - loadout_builder = loadout_builder.with_default_maintool(&body); - } - - // If second tool is passed, use it as well - if let Some(second_tool) = second_tool { - loadout_builder = loadout_builder.active_offhand(Some(second_tool)); - } - - // If there is config, apply it. - // If not, use default equipement for this body. - if let Some(asset) = loadout_asset { - loadout_builder = loadout_builder.with_asset_expect(&asset, rng); - } else { - loadout_builder = loadout_builder.with_default_equipment(&body); - } - - // Evaluate lazy function for loadout creation - if let Some(make_loadout) = make_loadout { - loadout_builder = - loadout_builder.with_creator(make_loadout, economy.as_ref()); - } - loadout_builder.build() - }; - - let health = Some(comp::Health::new(body, entity.level.unwrap_or(0))); - let poise = comp::Poise::new(body); - - let can_speak = match body { - comp::Body::Humanoid(_) => true, - comp::Body::BirdMedium(bird_medium) => match bird_medium.species { - // Parrots like to have a word in this, too... - bird_medium::Species::Parrot => alignment == comp::Alignment::Npc, - _ => false, + let rng = &mut rand::thread_rng(); + let data = NpcData::from_entity_info(entity, rng); + match data { + NpcData::Waypoint(pos) => { + server_emitter.emit(ServerEvent::CreateWaypoint(pos)); }, - _ => false, - }; - let trade_for_site = if matches!(entity.agent_mark, Some(agent::Mark::Merchant)) { - economy.map(|e| e.id) - } else { - None - }; - - // TODO: This code sets an appropriate base_damage for the enemy. This doesn't - // work because the damage is now saved in an ability - /* - if let Some(item::ItemKind::Tool(item::ToolData { base_damage, .. })) = - &mut loadout.active_item.map(|i| i.item.kind) - { - *base_damage = stats.level.level() as u32 * 3; - } - */ - server_emitter.emit(ServerEvent::CreateNpc { - pos: Pos(entity.pos), - stats, - skill_set, - health, - poise, - loadout, - agent: if entity.has_agency { - Some( - comp::Agent::from_body(&body) - .with_behavior( - Behavior::default() - .maybe_with_capabilities( - can_speak.then(|| BehaviorCapability::SPEAK), - ) - .with_trade_site(trade_for_site), - ) - .with_patrol_origin(entity.pos) - .with_no_flee(!matches!( - entity.agent_mark, - Some(agent::Mark::Guard) - )), - ) - } else { - None + NpcData::Data { + pos, + stats, + skill_set, + health, + poise, + loadout, + agent, + body, + alignment, + scale, + drop_item, + } => { + server_emitter.emit(ServerEvent::CreateNpc { + pos, + stats, + skill_set, + health, + poise, + loadout, + agent, + body, + alignment, + scale, + anchor: Some(comp::Anchor::Chunk(key)), + drop_item, + rtsim_entity: None, + projectile: None, + }); }, - body, - alignment, - scale: comp::Scale(scale), - anchor: Some(comp::Anchor::Chunk(key)), - drop_item: entity.loot_drop, - rtsim_entity: None, - projectile: None, - }) + } } // Insert a safezone if chunk contains the spawn position @@ -442,6 +340,144 @@ impl<'a> System<'a> for Sys { } } +/// Convinient structure to use when you need to create new npc +/// from EntityInfo +// TODO: better name? +pub enum NpcData { + Data { + pos: Pos, + stats: comp::Stats, + skill_set: comp::SkillSet, + health: Option, + poise: comp::Poise, + loadout: comp::inventory::loadout::Loadout, + agent: Option, + body: comp::Body, + alignment: comp::Alignment, + scale: comp::Scale, + drop_item: Option, + }, + Waypoint(Vec3), +} + +impl NpcData { + pub fn from_entity_info(entity: EntityInfo, rng: &mut impl Rng) -> Self { + let EntityInfo { + // flags + is_waypoint, + has_agency, + agent_mark, + alignment, + // stats + body, + name, + scale, + pos, + level, + loot_drop, + // tools and skills + skillset_asset, + main_tool, + second_tool, + loadout_asset, + make_loadout, + trading_information: economy, + // unused + pet: _, // TODO: I had no idea we have this. + } = entity; + + if is_waypoint { + return Self::Waypoint(pos); + } + + let name = name.unwrap_or_else(|| "Unnamed".to_string()); + let stats = comp::Stats::new(name); + + let skill_set = { + let skillset_builder = SkillSetBuilder::default(); + if let Some(skillset_asset) = skillset_asset { + skillset_builder.with_asset_expect(&skillset_asset).build() + } else { + skillset_builder.build() + } + }; + + let loadout = { + let mut loadout_builder = LoadoutBuilder::empty(); + + // If main tool is passed, use it. Otherwise fallback to default tool + if let Some(main_tool) = main_tool { + loadout_builder = loadout_builder.active_mainhand(Some(main_tool)); + } else { + loadout_builder = loadout_builder.with_default_maintool(&body); + } + + // If second tool is passed, use it as well + if let Some(second_tool) = second_tool { + loadout_builder = loadout_builder.active_offhand(Some(second_tool)); + } + + // If there is config, apply it. + // If not, use default equipement for this body. + if let Some(asset) = loadout_asset { + loadout_builder = loadout_builder.with_asset_expect(&asset, rng); + } else { + loadout_builder = loadout_builder.with_default_equipment(&body); + } + + // Evaluate lazy function for loadout creation + if let Some(make_loadout) = make_loadout { + loadout_builder = loadout_builder.with_creator(make_loadout, economy.as_ref()); + } + loadout_builder.build() + }; + + let health = Some(comp::Health::new(body, level.unwrap_or(0))); + let poise = comp::Poise::new(body); + + let can_speak = match body { + comp::Body::Humanoid(_) => true, + comp::Body::BirdMedium(bird_medium) => match bird_medium.species { + // Parrots like to have a word in this, too... + bird_medium::Species::Parrot => alignment == comp::Alignment::Npc, + _ => false, + }, + _ => false, + }; + + let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) { + economy.map(|e| e.id) + } else { + None + }; + + let agent = has_agency.then(|| { + comp::Agent::from_body(&body) + .with_behavior( + Behavior::default() + .maybe_with_capabilities(can_speak.then(|| BehaviorCapability::SPEAK)) + .with_trade_site(trade_for_site), + ) + .with_patrol_origin(pos) + .with_no_flee(!matches!(agent_mark, Some(agent::Mark::Guard))) + }); + + NpcData::Data { + pos: Pos(pos), + stats, + skill_set, + health, + poise, + loadout, + agent, + body, + alignment, + scale: comp::Scale(scale), + drop_item: loot_drop, + } + } +} + pub fn chunk_in_vd( player_pos: Vec3, chunk_pos: Vec2,