diff --git a/assets/common/entity/test.ron b/assets/common/entity/test.ron index 64b15c7be2..2a70be337c 100644 --- a/assets/common/entity/test.ron +++ b/assets/common/entity/test.ron @@ -6,7 +6,7 @@ /// Can be Exact (Body with all fields e.g BodyType, Species, Hair color and such) /// or Random (will use random if available for this Body) /// or RandomWith (will use random_with if available for this Body) - body: Humanoid(Random), + // body: Humanoid(Random), /// Loot /// Can be Item (with asset_specifier for item) @@ -24,8 +24,8 @@ loadout_config: Some(Loadout("common.loadout.village.merchant")), /// Skillset Config as Option (with asset_specifier for skillset) - skillset_config: None, + // skillset_config: None, /// Meta Info (level, alignment, agency, etc) - meta: {}, + // meta: {}, } diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 3fade71b8c..8b792f361f 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -32,6 +32,12 @@ pub enum Alignment { Passive, } +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Mark { + Merchant, + Guard, +} + impl Alignment { // Always attacks pub fn hostile_towards(self, other: Alignment) -> bool { diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 5453a9b0c2..8e49b3c4ae 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -7,12 +7,11 @@ use crate::{ inventory::{ loadout::Loadout, slot::{ArmorSlot, EquipSlot}, - trade_pricing::TradePricing, }, - item::{tool::ToolKind, Item, ItemKind}, + item::Item, object, quadruped_low, quadruped_medium, theropod, Body, }, - trade::{Good, SiteInformation}, + trade::SiteInformation, }; use hashbrown::HashMap; use rand::{self, distributions::WeightedError, seq::SliceRandom, Rng}; @@ -41,20 +40,9 @@ use tracing::warn; #[derive(Clone)] pub struct LoadoutBuilder(Loadout); -#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Debug, EnumIter)] -pub enum LoadoutConfig { - Gnarling, - Adlet, - Sahagin, - Haniwa, - Myrmidon, +#[derive(Copy, Clone, PartialEq, Deserialize, Serialize, Debug, EnumIter)] +pub enum LoadoutPreset { Husk, - Beastmaster, - Warlord, - Warlock, - Villager, - Guard, - Merchant, } #[derive(Debug, Deserialize, Clone)] @@ -82,7 +70,9 @@ impl ItemSpec { .as_ref() .and_then(|e| match e { entry @ ItemSpec::Item { .. } => entry.try_to_item(asset_specifier, rng), - choice @ ItemSpec::Choice { .. } => choice.try_to_item(asset_specifier, rng), + choice @ ItemSpec::Choice { .. } => { + choice.try_to_item(asset_specifier, rng) + }, }) }, } @@ -165,7 +155,7 @@ pub fn make_potion_bag(quantity: u32) -> Item { // Also we are using default tools for un-specified species so // it's fine to have wildcards #[allow(clippy::too_many_lines, clippy::match_wildcard_for_single_variants)] -pub fn default_main_tool(body: &Body) -> Option { +fn default_main_tool(body: &Body) -> Option { match body { Body::Golem(golem) => match golem.species { golem::Species::StoneGolem => Some(Item::new_from_asset_expect( @@ -350,63 +340,108 @@ impl LoadoutBuilder { pub fn new() -> Self { Self(Loadout::new_empty()) } #[must_use] - fn with_default_equipment(body: &Body, active_item: Option) -> Self { - let mut builder = Self::new(); - builder = match body { - Body::BipedLarge(biped_large::Body { - species: biped_large::Species::Mindflayer, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.biped_large.mindflayer", - ))), - Body::BipedLarge(biped_large::Body { - species: biped_large::Species::Minotaur, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.biped_large.minotaur", - ))), - Body::BipedLarge(biped_large::Body { - species: biped_large::Species::Tidalwarrior, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.biped_large.tidal_warrior", - ))), - Body::BipedLarge(biped_large::Body { - species: biped_large::Species::Yeti, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.biped_large.yeti", - ))), - Body::BipedLarge(biped_large::Body { - species: biped_large::Species::Harvester, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.biped_large.harvester", - ))), - Body::Golem(golem::Body { - species: golem::Species::ClayGolem, - .. - }) => builder.chest(Some(Item::new_from_asset_expect( - "common.items.npc_armor.golem.claygolem", - ))), - _ => builder, - }; - - builder.active_mainhand(active_item) - } - - #[must_use] + /// Construct new `LoadoutBuilder` from `asset_specifier` + /// Will panic if asset is broken pub fn from_asset_expect(asset_specifier: &str, rng: Option<&mut impl Rng>) -> Self { + // It's impossible to use lambdas because `loadout` is used by value + #![allow(clippy::option_if_let_else)] let loadout = Self::new(); if let Some(rng) = rng { loadout.with_asset_expect(asset_specifier, rng) } else { - let rng = &mut rand::thread_rng(); - loadout.with_asset_expect(asset_specifier, rng) + let fallback_rng = &mut rand::thread_rng(); + loadout.with_asset_expect(asset_specifier, fallback_rng) } } + #[must_use] + /// Construct new default `LoadoutBuilder` for corresponding `body` + /// + /// NOTE: make sure that you check what is default for this body + /// Use it if you don't care much about it, for example in "/spawn" command + pub fn from_default(body: &Body) -> Self { + let loadout = Self::new(); + loadout + .with_default_maintool(body) + .with_default_equipment(body) + } + + #[must_use] + /// Set default active mainhand weapon based on `body` + pub fn with_default_maintool(self, body: &Body) -> Self { + self.active_mainhand(default_main_tool(body)) + } + + #[must_use] + /// Set default equipement based on `body` + pub fn with_default_equipment(mut self, body: &Body) -> Self { + self = match body { + Body::BipedLarge(biped_large::Body { + species: biped_large::Species::Mindflayer, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.biped_large.mindflayer", + ))), + Body::BipedLarge(biped_large::Body { + species: biped_large::Species::Minotaur, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.biped_large.minotaur", + ))), + Body::BipedLarge(biped_large::Body { + species: biped_large::Species::Tidalwarrior, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.biped_large.tidal_warrior", + ))), + Body::BipedLarge(biped_large::Body { + species: biped_large::Species::Yeti, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.biped_large.yeti", + ))), + Body::BipedLarge(biped_large::Body { + species: biped_large::Species::Harvester, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.biped_large.harvester", + ))), + Body::Golem(golem::Body { + species: golem::Species::ClayGolem, + .. + }) => self.chest(Some(Item::new_from_asset_expect( + "common.items.npc_armor.golem.claygolem", + ))), + _ => self, + }; + + self + } + + #[must_use] + pub fn with_preset(mut self, preset: LoadoutPreset) -> Self { + let rng = &mut rand::thread_rng(); + match preset { + LoadoutPreset::Husk => { + self = self.with_asset_expect("common.loadout.dungeon.tier-5.husk", rng) + }, + } + + self + } + + #[must_use] + pub fn with_creator( + mut self, + creator: fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder, + economy: Option<&SiteInformation>, + ) -> LoadoutBuilder { + self = creator(self, economy); + + self + } + /// # Usage /// Creates new `LoadoutBuilder` with all field replaced from /// `asset_specifier` which corresponds to loadout config @@ -497,219 +532,6 @@ impl LoadoutBuilder { self.with_asset_expect("common.loadout.default", rng) } - /// Builds loadout of creature when spawned - #[must_use] - // The reason why this function is so long is creating merchant inventory - // with all items to sell. - // Maybe we should do it on the caller side? - #[allow( - clippy::too_many_lines, - clippy::cast_precision_loss, - clippy::cast_sign_loss, - clippy::cast_possible_truncation - )] - pub fn build_loadout( - body: Body, - mut main_tool: Option, - config: Option, - economy: Option<&SiteInformation>, - ) -> Self { - // If no main tool is passed in, checks if species has a default main tool - if main_tool.is_none() { - main_tool = default_main_tool(&body); - } - - // Constructs ItemConfig from Item - let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(Item::kind) { - main_tool - } else { - Some(Item::empty()) - }; - let active_tool_kind = active_item.as_ref().and_then(|i| { - if let ItemKind::Tool(tool) = &i.kind() { - Some(tool.kind) - } else { - None - } - }); - // Creates rest of loadout - let rng = &mut rand::thread_rng(); - let loadout_builder = if let Some(config) = config { - let builder = Self::new().active_mainhand(active_item); - // NOTE: we apply asset after active mainhand so asset has ability override it - match config { - LoadoutConfig::Gnarling => match active_tool_kind { - Some(ToolKind::Bow | ToolKind::Staff | ToolKind::Spear) => { - builder.with_asset_expect("common.loadout.dungeon.tier-0.gnarling", rng) - }, - _ => builder, - }, - LoadoutConfig::Adlet => match active_tool_kind { - Some(ToolKind::Bow) => { - builder.with_asset_expect("common.loadout.dungeon.tier-1.adlet_bow", rng) - }, - Some(ToolKind::Spear | ToolKind::Staff) => { - builder.with_asset_expect("common.loadout.dungeon.tier-1.adlet_spear", rng) - }, - _ => builder, - }, - LoadoutConfig::Sahagin => { - builder.with_asset_expect("common.loadout.dungeon.tier-2.sahagin", rng) - }, - LoadoutConfig::Haniwa => { - builder.with_asset_expect("common.loadout.dungeon.tier-3.haniwa", rng) - }, - LoadoutConfig::Myrmidon => { - builder.with_asset_expect("common.loadout.dungeon.tier-4.myrmidon", rng) - }, - LoadoutConfig::Husk => { - builder.with_asset_expect("common.loadout.dungeon.tier-5.husk", rng) - }, - LoadoutConfig::Beastmaster => { - builder.with_asset_expect("common.loadout.dungeon.tier-5.beastmaster", rng) - }, - LoadoutConfig::Warlord => { - builder.with_asset_expect("common.loadout.dungeon.tier-5.warlord", rng) - }, - LoadoutConfig::Warlock => { - builder.with_asset_expect("common.loadout.dungeon.tier-5.warlock", rng) - }, - LoadoutConfig::Villager => builder - .with_asset_expect("common.loadout.village.villager", rng) - .bag(ArmorSlot::Bag1, Some(make_potion_bag(10))), - LoadoutConfig::Guard => builder - .with_asset_expect("common.loadout.village.guard", rng) - .bag(ArmorSlot::Bag1, Some(make_potion_bag(25))), - LoadoutConfig::Merchant => { - let mut backpack = - Item::new_from_asset_expect("common.items.armor.misc.back.backpack"); - let mut coins = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Coin)) - .copied() - .unwrap_or_default() - .round() - .min(rand::thread_rng().gen_range(1000.0..3000.0)) - as u32; - let armor = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Armor)) - .copied() - .unwrap_or_default() - / 10.0; - for s in backpack.slots_mut() { - if coins > 0 { - let mut coin_item = - Item::new_from_asset_expect("common.items.utility.coins"); - coin_item - .set_amount(coins) - .expect("coins should be stackable"); - *s = Some(coin_item); - coins = 0; - } else if armor > 0.0 { - if let Some(item_id) = - TradePricing::random_item(Good::Armor, armor, true) - { - *s = Some(Item::new_from_asset_expect(&item_id)); - } - } - } - let mut bag1 = Item::new_from_asset_expect( - "common.items.armor.misc.bag.reliable_backpack", - ); - let weapon = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Tools)) - .copied() - .unwrap_or_default() - / 10.0; - if weapon > 0.0 { - for i in bag1.slots_mut() { - if let Some(item_id) = - TradePricing::random_item(Good::Tools, weapon, true) - { - *i = Some(Item::new_from_asset_expect(&item_id)); - } - } - } - let mut item_with_amount = |item_id: &str, amount: &mut f32| { - if *amount > 0.0 { - let mut item = Item::new_from_asset_expect(item_id); - // NOTE: Conversion to and from f32 works fine because we make sure the - // number we're converting is ≤ 100. - let max = amount.min(16.min(item.max_amount()) as f32) as u32; - let n = rng.gen_range(1..max.max(2)); - *amount -= if item.set_amount(n).is_ok() { - n as f32 - } else { - 1.0 - }; - Some(item) - } else { - None - } - }; - let mut bag2 = Item::new_from_asset_expect( - "common.items.armor.misc.bag.reliable_backpack", - ); - let mut ingredients = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Ingredients)) - .copied() - .unwrap_or_default() - / 10.0; - for i in bag2.slots_mut() { - if let Some(item_id) = - TradePricing::random_item(Good::Ingredients, ingredients, true) - { - *i = item_with_amount(&item_id, &mut ingredients); - } - } - let mut bag3 = Item::new_from_asset_expect( - "common.items.armor.misc.bag.reliable_backpack", - ); - // TODO: currently econsim spends all its food on population, resulting in none - // for the players to buy; the `.max` is temporary to ensure that there's some - // food for sale at every site, to be used until we have some solution like NPC - // houses as a limit on econsim population growth - let mut food = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Food)) - .copied() - .unwrap_or_default() - .max(10000.0) - / 10.0; - for i in bag3.slots_mut() { - if let Some(item_id) = TradePricing::random_item(Good::Food, food, true) { - *i = item_with_amount(&item_id, &mut food); - } - } - let mut bag4 = Item::new_from_asset_expect( - "common.items.armor.misc.bag.reliable_backpack", - ); - let mut potions = economy - .and_then(|e| e.unconsumed_stock.get(&Good::Potions)) - .copied() - .unwrap_or_default() - / 10.0; - for i in bag4.slots_mut() { - if let Some(item_id) = - TradePricing::random_item(Good::Potions, potions, true) - { - *i = item_with_amount(&item_id, &mut potions); - } - } - builder - .with_asset_expect("common.loadout.village.merchant", rng) - .back(Some(backpack)) - .bag(ArmorSlot::Bag1, Some(bag1)) - .bag(ArmorSlot::Bag2, Some(bag2)) - .bag(ArmorSlot::Bag3, Some(bag3)) - .bag(ArmorSlot::Bag4, Some(bag4)) - }, - } - } else { - Self::with_default_equipment(&body, active_item) - }; - - Self(loadout_builder.build()) - } - #[must_use] pub fn active_mainhand(mut self, item: Option) -> Self { self.0.swap(EquipSlot::ActiveMainhand, item); @@ -838,40 +660,13 @@ mod tests { use rand::thread_rng; use strum::IntoEnumIterator; - // Testing all configs in loadout with weapons of different toolkinds + // Testing all loadout presets // // Things that will be catched - invalid assets paths #[test] - fn test_loadout_configs() { - let test_weapons = vec![ - // Melee - "common.items.weapons.sword.starter", // Sword - "common.items.weapons.axe.starter_axe", // Axe - "common.items.weapons.hammer.starter_hammer", // Hammer - // Ranged - "common.items.weapons.bow.starter", // Bow - "common.items.weapons.staff.starter_staff", // Staff - "common.items.weapons.sceptre.starter_sceptre", // Sceptre - // Other - "common.items.weapons.dagger.starter_dagger", // Dagger - "common.items.weapons.shield.shield_1", // Shield - "common.items.npc_weapons.biped_small.sahagin.wooden_spear", // Spear - // Exotic - "common.items.npc_weapons.unique.beast_claws", // Natural - "common.items.weapons.tool.rake", // Farming - "common.items.tool.pickaxe_stone", // Pick - "common.items.weapons.empty.empty", // Empty - ]; - - for config in LoadoutConfig::iter() { - for test_weapon in &test_weapons { - std::mem::drop(LoadoutBuilder::build_loadout( - Body::Humanoid(comp::humanoid::Body::random()), - Some(Item::new_from_asset_expect(test_weapon)), - Some(config), - None, - )); - } + fn test_loadout_presets() { + for preset in LoadoutPreset::iter() { + std::mem::drop(LoadoutBuilder::default().with_preset(preset)); } } @@ -895,17 +690,11 @@ mod tests { body_type: comp::$species::BodyType::Male, ..body }; - std::mem::drop(LoadoutBuilder::build_loadout( - Body::$body(female_body), - None, - None, - None, + std::mem::drop(LoadoutBuilder::from_default( + &Body::$body(female_body), )); - std::mem::drop(LoadoutBuilder::build_loadout( - Body::$body(male_body), - None, - None, - None, + std::mem::drop(LoadoutBuilder::from_default( + &Body::$body(male_body), )); } }; diff --git a/common/src/generation.rs b/common/src/generation.rs index e91763659a..78e5726bbb 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -1,7 +1,12 @@ use crate::{ - comp::{self, humanoid, inventory::loadout_builder::LoadoutConfig, Alignment, Body, Item}, + comp::{ + self, agent, humanoid, + inventory::loadout_builder::{LoadoutBuilder, LoadoutPreset}, + Alignment, Body, Item, + }, npc::{self, NPC_NAMES}, skillset_builder::SkillSetConfig, + trade, trade::SiteInformation, }; use vek::*; @@ -17,6 +22,7 @@ pub struct EntityInfo { pub is_giant: bool, pub has_agency: bool, pub alignment: Alignment, + pub agent_mark: Option, pub body: Body, pub name: Option, pub main_tool: Option, @@ -25,13 +31,16 @@ pub struct EntityInfo { // TODO: Properly give NPCs skills pub level: Option, pub loot_drop: Option, + // FIXME: using both preset and asset is silly, make it enum + // so it will be correct by construction pub loadout_config: Option, - pub loadout_preset: Option, + pub loadout_preset: Option, + pub make_loadout: Option) -> LoadoutBuilder>, pub skillset_config: Option, pub skillset_preset: Option, pub pet: Option>, // we can't use DHashMap, do we want to move that into common? - pub trading_information: Option, + pub trading_information: Option, //Option>, /* price and available amount */ } @@ -43,6 +52,7 @@ impl EntityInfo { is_giant: false, has_agency: true, alignment: Alignment::Wild, + agent_mark: None, body: Body::Humanoid(humanoid::Body::random()), name: None, main_tool: None, @@ -52,6 +62,7 @@ impl EntityInfo { loot_drop: None, loadout_config: None, loadout_preset: None, + make_loadout: None, skillset_config: None, skillset_preset: None, pet: None, @@ -96,6 +107,11 @@ impl EntityInfo { self } + pub fn with_agent_mark(mut self, agent_mark: agent::Mark) -> Self { + self.agent_mark = Some(agent_mark); + self + } + pub fn with_main_tool(mut self, main_tool: Item) -> Self { self.main_tool = Some(main_tool); self @@ -121,18 +137,21 @@ impl EntityInfo { self } - pub fn with_loadout_config(mut self, config: String) -> Self { - self.loadout_config = Some(config); - self - } - - pub fn with_loadout_preset(mut self, preset: LoadoutConfig) -> Self { + pub fn with_loadout_preset(mut self, preset: LoadoutPreset) -> Self { self.loadout_preset = Some(preset); self } - pub fn with_skillset_config(mut self, config: String) -> Self { - self.skillset_config = Some(config); + pub fn with_loadout_config(mut self, config: &str) -> Self { + self.loadout_config = Some(config.to_owned()); + self + } + + pub fn with_lazy_loadout( + mut self, + creator: fn(LoadoutBuilder, Option<&trade::SiteInformation>) -> LoadoutBuilder, + ) -> Self { + self.make_loadout = Some(creator); self } @@ -141,6 +160,13 @@ impl EntityInfo { self } + // FIXME: Doesn't work for now, because skills can't be loaded from assets for + // now + pub fn with_skillset_config(mut self, config: String) -> Self { + self.skillset_config = Some(config); + self + } + pub fn with_automatic_name(mut self) -> Self { let npc_names = NPC_NAMES.read(); self.name = match &self.body { diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 857ab191ed..5385dca55a 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ self, - inventory::loadout_builder::{LoadoutBuilder, LoadoutConfig}, + inventory::loadout_builder::{LoadoutBuilder, LoadoutPreset}, Behavior, BehaviorCapability, CharacterState, StateUpdate, }, event::{LocalEvent, ServerEvent}, @@ -76,13 +76,15 @@ impl CharacterBehavior for Data { { let body = self.static_data.summon_info.body; - let loadout = LoadoutBuilder::build_loadout( - body, - None, - self.static_data.summon_info.loadout_preset, - None, - ) - .build(); + let mut loadout_builder = + LoadoutBuilder::new().with_default_maintool(&body); + + if let Some(preset) = self.static_data.summon_info.loadout_preset { + loadout_builder = loadout_builder.with_preset(preset); + } + + let loadout = loadout_builder.build(); + let stats = comp::Stats::new("Summon".to_string()); let skill_set = SkillSetBuilder::build_skillset( &None, @@ -176,6 +178,6 @@ pub struct SummonInfo { scale: Option, health_scaling: u16, // TODO: use assets for specifying skills and loadouts? - loadout_preset: Option, + loadout_preset: Option, skillset_preset: Option, } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index a8f747e5b5..1ca186fbb3 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1005,7 +1005,7 @@ fn handle_spawn( ); let body = body(); - let loadout = LoadoutBuilder::build_loadout(body, None, None, None).build(); + let loadout = LoadoutBuilder::from_default(&body).build(); let inventory = Inventory::new_with_loadout(loadout); let mut entity_base = server diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 5405f10cea..eebcb404c6 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -78,7 +78,7 @@ impl Entity { pub fn get_loadout(&self) -> comp::inventory::loadout::Loadout { let mut rng = self.rng(PERM_LOADOUT); - LoadoutBuilder::from_asset_expect("common.loadout.world.traveler", &mut rng) + LoadoutBuilder::from_asset_expect("common.loadout.world.traveler", Some(&mut rng)) .bag( comp::inventory::slot::ArmorSlot::Bag1, Some(comp::inventory::loadout_builder::make_potion_bag(100)), diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index b352de9438..9925a5926a 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -3,12 +3,9 @@ use crate::{ presence::Presence, rtsim::RtSim, settings::Settings, SpawnPoint, Tick, }; use common::{ - comp::{ - self, bird_medium, inventory::loadout_builder::LoadoutConfig, Alignment, - BehaviorCapability, Pos, - }, + comp::{self, agent, bird_medium, Alignment, BehaviorCapability, Pos}, event::{EventBus, ServerEvent}, - generation::get_npc_name, + generation::{get_npc_name, EntityInfo}, npc::NPC_NAMES, terrain::TerrainGrid, LoadoutBuilder, SkillSetBuilder, @@ -178,7 +175,6 @@ impl<'a> System<'a> for Sys { let mut body = entity.body; let name = entity.name.unwrap_or_else(|| "Unnamed".to_string()); let alignment = entity.alignment; - let main_tool = entity.main_tool; let mut stats = comp::Stats::new(name); let mut scale = entity.scale; @@ -198,14 +194,53 @@ impl<'a> System<'a> for Sys { scale = 2.0 + rand::random::(); } - let loadout_config = entity.loadout_config; - let economy = entity.trading_information.as_ref(); - let skillset_config = entity.skillset_config; + let EntityInfo { + skillset_preset, + main_tool, + loadout_preset, + loadout_config, + make_loadout, + trading_information: economy, + .. + } = entity; let skill_set = - SkillSetBuilder::build_skillset(&main_tool, skillset_config).build(); - let loadout = - LoadoutBuilder::build_loadout(body, main_tool, loadout_config, economy).build(); + SkillSetBuilder::build_skillset(&main_tool, skillset_preset).build(); + + let mut loadout_builder = LoadoutBuilder::new(); + 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 there are configs, apply them + match (loadout_preset, &loadout_config) { + (Some(preset), Some(config)) => { + loadout_builder = loadout_builder.with_preset(preset); + loadout_builder = loadout_builder.with_asset_expect(&config, rng); + }, + (Some(preset), None) => { + loadout_builder = loadout_builder.with_preset(preset); + }, + (None, Some(config)) => { + loadout_builder = loadout_builder.with_asset_expect(&config, rng); + }, + // If not, use default equipement for this body + (None, None) => { + 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()); + } + + let loadout = loadout_builder.build(); let health = comp::Health::new(body, entity.level.unwrap_or(0)); let poise = comp::Poise::new(body); @@ -219,7 +254,7 @@ impl<'a> System<'a> for Sys { }, _ => false, }; - let trade_for_site = if matches!(loadout_config, Some(LoadoutConfig::Merchant)) { + let trade_for_site = if matches!(entity.agent_mark, Some(agent::Mark::Merchant)) { economy.map(|e| e.id) } else { None @@ -250,10 +285,7 @@ impl<'a> System<'a> for Sys { can_speak.then(|| BehaviorCapability::SPEAK), ) .with_trade_site(trade_for_site), - matches!( - loadout_config, - Some(comp::inventory::loadout_builder::LoadoutConfig::Guard) - ), + matches!(entity.agent_mark, Some(agent::Mark::Guard)), )) } else { None diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 266fba7ed4..c52ce446eb 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -11,10 +11,7 @@ use crate::{ use common::{ assets::{AssetExt, AssetHandle}, astar::Astar, - comp::{ - inventory::loadout_builder, - {self}, - }, + comp::{self}, generation::{ChunkSupplement, EntityInfo}, lottery::{LootSpec, Lottery}, store::{Id, Store}, @@ -936,8 +933,8 @@ fn enemy_0(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { ), )) .with_name("Gnarling") - .with_loadout_config(loadout_builder::LoadoutConfig::Gnarling) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Gnarling) + .with_loadout_config("common.loadout.dungeon.tier-0.gnarling") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Gnarling) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0..5) { @@ -951,21 +948,31 @@ fn enemy_0(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { fn enemy_1(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { let chosen = Lottery::::load_expect("common.loot_tables.dungeon.tier-1.enemy"); - entity + let adlet = entity .with_body(comp::Body::BipedSmall( comp::biped_small::Body::random_with(dynamic_rng, &comp::biped_small::Species::Adlet), )) .with_name("Adlet") - .with_loadout_config(loadout_builder::LoadoutConfig::Adlet) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Adlet) - .with_loot_drop(chosen.read().choose().to_item()) - .with_main_tool(comp::Item::new_from_asset_expect( - match dynamic_rng.gen_range(0..5) { - 0 => "common.items.npc_weapons.biped_small.adlet.adlet_bow", - 1 => "common.items.npc_weapons.biped_small.adlet.gnoll_staff", - _ => "common.items.npc_weapons.biped_small.adlet.wooden_spear", - }, - )) + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Adlet) + .with_loot_drop(chosen.read().choose().to_item()); + + match dynamic_rng.gen_range(0..5) { + 0 => adlet + .with_main_tool(comp::Item::new_from_asset_expect( + "common.items.npc_weapons.biped_small.adlet.adlet_bow", + )) + .with_loadout_config("common.loadout.dungeon.tier-1.adlet_bow"), + 1 => adlet + .with_main_tool(comp::Item::new_from_asset_expect( + "common.items.npc_weapons.biped_small.adlet.adlet_staff", + )) + .with_loadout_config("common.loadout.dungeon.tier-1.adlet_spear"), + _ => adlet + .with_main_tool(comp::Item::new_from_asset_expect( + "common.items.npc_weapons.biped_small.adlet.adlet_spear", + )) + .with_loadout_config("common.loadout.dungeon.tier-1.adlet_spear"), + } } fn enemy_2(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { @@ -976,8 +983,8 @@ fn enemy_2(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { comp::biped_small::Body::random_with(dynamic_rng, &comp::biped_small::Species::Sahagin), )) .with_name("Sahagin") - .with_loadout_config(loadout_builder::LoadoutConfig::Sahagin) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Sahagin) + .with_loadout_config("common.loadout.dungeon.tier-2.sahagin") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Sahagin) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0..5) { @@ -1005,8 +1012,8 @@ fn enemy_3(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { ), )) .with_name("Haniwa") - .with_loadout_config(loadout_builder::LoadoutConfig::Haniwa) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Haniwa) + .with_loadout_config("common.loadout.dungeon.tier-3.haniwa") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Haniwa) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0..5) { @@ -1028,8 +1035,8 @@ fn enemy_4(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { ), )) .with_name("Myrmidon") - .with_loadout_config(loadout_builder::LoadoutConfig::Myrmidon) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Myrmidon) + .with_loadout_config("common.loadout.dungeon.tier-4.myrmidon") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Myrmidon) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0..5) { @@ -1052,16 +1059,16 @@ fn enemy_5(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { 1 => entity .with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) .with_name("Cultist Warlock") - .with_loadout_config(loadout_builder::LoadoutConfig::Warlock) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Warlock) + .with_loadout_config("common.loadout.dungeon.tier-5.warlock") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Warlock) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( "common.items.weapons.staff.cultist_staff", )), _ => entity .with_name("Cultist Warlord") - .with_loadout_config(loadout_builder::LoadoutConfig::Warlord) - .with_skillset_config(common::skillset_builder::SkillSetConfig::Warlord) + .with_loadout_config("common.loadout.dungeon.tier-5.warlord") + .with_skillset_preset(common::skillset_builder::SkillSetConfig::Warlord) .with_loot_drop(chosen.read().choose().to_item()) .with_main_tool(comp::Item::new_from_asset_expect( match dynamic_rng.gen_range(0..6) { @@ -1176,7 +1183,7 @@ fn boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3) -> Vec) -> Vec "common.items.weapons.axe.malachite_axe-0", @@ -1321,7 +1328,7 @@ fn mini_boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3) -> Vec entity @@ -960,27 +967,28 @@ impl Settlement { "common.items.weapons.bow.eldwood-0", )) .with_name("Merchant") + .with_agent_mark(agent::Mark::Merchant) + .with_economy(&economy) + .with_lazy_loadout(merchant_loadout) .with_level(dynamic_rng.gen_range(10..15)) - .with_loadout_config(loadout_builder::LoadoutConfig::Merchant) - .with_skillset_config( + .with_skillset_preset( common::skillset_builder::SkillSetConfig::Merchant, - ) - .with_economy(&economy), + ), _ => entity .with_main_tool(Item::new_from_asset_expect( match dynamic_rng.gen_range(0..7) { - 0 => "common.items.weapons.tool.broom", - 1 => "common.items.weapons.tool.hoe", - 2 => "common.items.weapons.tool.pickaxe", - 3 => "common.items.weapons.tool.pitchfork", - 4 => "common.items.weapons.tool.rake", - 5 => "common.items.weapons.tool.shovel-0", - _ => "common.items.weapons.tool.shovel-1", - //_ => "common.items.weapons.bow.starter", TODO: Re-Add this when we have a better way of distributing npc_weapons here - }, + 0 => "common.items.weapons.tool.broom", + 1 => "common.items.weapons.tool.hoe", + 2 => "common.items.weapons.tool.pickaxe", + 3 => "common.items.weapons.tool.pitchfork", + 4 => "common.items.weapons.tool.rake", + 5 => "common.items.weapons.tool.shovel-0", + _ => "common.items.weapons.tool.shovel-1", + //_ => "common.items.weapons.bow.starter", TODO: Re-Add this when we have a better way of distributing npc_weapons here + }, )) - .with_loadout_config(loadout_builder::LoadoutConfig::Villager) - .with_skillset_config( + .with_lazy_loadout(villager_loadout) + .with_skillset_preset( common::skillset_builder::SkillSetConfig::Villager, ), } @@ -1043,6 +1051,137 @@ impl Settlement { } } +fn merchant_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&trade::SiteInformation>, +) -> LoadoutBuilder { + let rng = &mut rand::thread_rng(); + let mut backpack = Item::new_from_asset_expect("common.items.armor.misc.back.backpack"); + let mut coins = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Coin)) + .copied() + .unwrap_or_default() + .round() + .min(rand::thread_rng().gen_range(1000.0..3000.0)) as u32; + let armor = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Armor)) + .copied() + .unwrap_or_default() + / 10.0; + for s in backpack.slots_mut() { + if coins > 0 { + let mut coin_item = Item::new_from_asset_expect("common.items.utility.coins"); + coin_item + .set_amount(coins) + .expect("coins should be stackable"); + *s = Some(coin_item); + coins = 0; + } else if armor > 0.0 { + if let Some(item_id) = TradePricing::random_item(Good::Armor, armor, true) { + *s = Some(Item::new_from_asset_expect(&item_id)); + } + } + } + let mut bag1 = Item::new_from_asset_expect("common.items.armor.misc.bag.reliable_backpack"); + let weapon = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Tools)) + .copied() + .unwrap_or_default() + / 10.0; + if weapon > 0.0 { + for i in bag1.slots_mut() { + if let Some(item_id) = TradePricing::random_item(Good::Tools, weapon, true) { + *i = Some(Item::new_from_asset_expect(&item_id)); + } + } + } + let mut item_with_amount = |item_id: &str, amount: &mut f32| { + if *amount > 0.0 { + let mut item = Item::new_from_asset_expect(item_id); + // NOTE: Conversion to and from f32 works fine because we make sure the + // number we're converting is ≤ 100. + let max = amount.min(16.min(item.max_amount()) as f32) as u32; + let n = rng.gen_range(1..max.max(2)); + *amount -= if item.set_amount(n).is_ok() { + n as f32 + } else { + 1.0 + }; + Some(item) + } else { + None + } + }; + let mut bag2 = Item::new_from_asset_expect("common.items.armor.misc.bag.reliable_backpack"); + let mut ingredients = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Ingredients)) + .copied() + .unwrap_or_default() + / 10.0; + for i in bag2.slots_mut() { + if let Some(item_id) = TradePricing::random_item(Good::Ingredients, ingredients, true) { + *i = item_with_amount(&item_id, &mut ingredients); + } + } + let mut bag3 = Item::new_from_asset_expect("common.items.armor.misc.bag.reliable_backpack"); + // TODO: currently econsim spends all its food on population, resulting in none + // for the players to buy; the `.max` is temporary to ensure that there's some + // food for sale at every site, to be used until we have some solution like NPC + // houses as a limit on econsim population growth + let mut food = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Food)) + .copied() + .unwrap_or_default() + .max(10000.0) + / 10.0; + for i in bag3.slots_mut() { + if let Some(item_id) = TradePricing::random_item(Good::Food, food, true) { + *i = item_with_amount(&item_id, &mut food); + } + } + let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.reliable_backpack"); + let mut potions = economy + .and_then(|e| e.unconsumed_stock.get(&Good::Potions)) + .copied() + .unwrap_or_default() + / 10.0; + for i in bag4.slots_mut() { + if let Some(item_id) = TradePricing::random_item(Good::Potions, potions, true) { + *i = item_with_amount(&item_id, &mut potions); + } + } + + loadout_builder + .with_asset_expect("common.loadout.village.merchant", rng) + .back(Some(backpack)) + .bag(ArmorSlot::Bag1, Some(bag1)) + .bag(ArmorSlot::Bag2, Some(bag2)) + .bag(ArmorSlot::Bag3, Some(bag3)) + .bag(ArmorSlot::Bag4, Some(bag4)) +} + +fn guard_loadout( + loadout_builder: LoadoutBuilder, + _economy: Option<&trade::SiteInformation>, +) -> LoadoutBuilder { + let rng = &mut rand::thread_rng(); + + loadout_builder + .with_asset_expect("common.loadout.village.guard", rng) + .bag(ArmorSlot::Bag1, Some(make_potion_bag(25))) +} + +fn villager_loadout( + loadout_builder: LoadoutBuilder, + _economy: Option<&trade::SiteInformation>, +) -> LoadoutBuilder { + let rng = &mut rand::thread_rng(); + + loadout_builder + .with_asset_expect("common.loadout.village.villager", rng) + .bag(ArmorSlot::Bag1, Some(make_potion_bag(10))) +} + #[derive(Copy, Clone, PartialEq)] pub enum Crop { Corn,