From 077da13a5f186df2743a33445773ed35e1f25e45 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 20:25:32 +0000 Subject: [PATCH] Improved herbalist, hunter, farmer, added cultist factions --- assets/common/entity/village/farmer.ron | 23 +++++++ assets/common/entity/village/herbalist.ron | 21 +++++++ assets/common/entity/village/hunter.ron | 23 +++++++ assets/common/loadout/village/farmer.ron | 30 +++++++++ assets/common/loadout/village/herbalist.ron | 26 ++++++++ assets/common/loadout/village/hunter.ron | 28 +++++++++ common/src/rtsim.rs | 3 + rtsim/src/data/faction.rs | 1 + rtsim/src/gen/faction.rs | 5 +- rtsim/src/gen/mod.rs | 67 ++++++++++++++------- rtsim/src/gen/site.rs | 34 ++++++++--- rtsim/src/rule/npc_ai.rs | 31 ++++++---- rtsim/src/rule/setup.rs | 9 ++- server/src/rtsim2/tick.rs | 21 ++++++- 14 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 assets/common/entity/village/farmer.ron create mode 100644 assets/common/entity/village/herbalist.ron create mode 100644 assets/common/entity/village/hunter.ron create mode 100644 assets/common/loadout/village/farmer.ron create mode 100644 assets/common/loadout/village/herbalist.ron create mode 100644 assets/common/loadout/village/hunter.ron diff --git a/assets/common/entity/village/farmer.ron b/assets/common/entity/village/farmer.ron new file mode 100644 index 0000000000..77c8f1469c --- /dev/null +++ b/assets/common/entity/village/farmer.ron @@ -0,0 +1,23 @@ +#![enable(implicit_some)] +( + name: Name("Farmer"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.farmer"), + active_hands: InHands((Choice([ + (1, Item("common.items.weapons.tool.hoe")), + (1, Item("common.items.weapons.tool.rake")), + (1, Item("common.items.weapons.tool.shovel-0")), + (1, Item("common.items.weapons.tool.shovel-1")), + ]), None)), + )), + items: [ + (10, "common.items.food.cheese"), + (10, "common.items.food.plainsalad"), + ], + ), + meta: [], +) diff --git a/assets/common/entity/village/herbalist.ron b/assets/common/entity/village/herbalist.ron new file mode 100644 index 0000000000..db8bea4a03 --- /dev/null +++ b/assets/common/entity/village/herbalist.ron @@ -0,0 +1,21 @@ +#![enable(implicit_some)] +( + name: Name("Herbalist"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.herbalist"), + active_hands: InHands((Choice([ + (1, Item("common.items.weapons.tool.hoe")), + (1, Item("common.items.weapons.tool.rake")), + ]), None)), + )), + items: [ + (10, "common.items.food.cheese"), + (10, "common.items.food.plainsalad"), + ], + ), + meta: [], +) diff --git a/assets/common/entity/village/hunter.ron b/assets/common/entity/village/hunter.ron new file mode 100644 index 0000000000..d0304584c2 --- /dev/null +++ b/assets/common/entity/village/hunter.ron @@ -0,0 +1,23 @@ +#![enable(implicit_some)] +( + name: Name("Hunter"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.hunter"), + active_hands: InHands((Choice([ + (8, ModularWeapon(tool: Bow, material: Wood, hands: None)), + (4, ModularWeapon(tool: Bow, material: Bamboo, hands: None)), + (2, ModularWeapon(tool: Bow, material: Hardwood, hands: None)), + (2, ModularWeapon(tool: Bow, material: Ironwood, hands: None)), + (1, ModularWeapon(tool: Bow, material: Eldwood, hands: None)), + ]), None)), + )), + items: [ + (10, "common.items.consumable.potion_big"), + ], + ), + meta: [], +) diff --git a/assets/common/loadout/village/farmer.ron b/assets/common/loadout/village/farmer.ron new file mode 100644 index 0000000000..14f646383a --- /dev/null +++ b/assets/common/loadout/village/farmer.ron @@ -0,0 +1,30 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (3, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.bamboo_twig")), + (2, None), + ]), + chest: Choice([ + (1, Item("common.items.armor.misc.chest.worker_green_0")), + (1, Item("common.items.armor.misc.chest.worker_green_1")), + (1, Item("common.items.armor.misc.chest.worker_red_0")), + (1, Item("common.items.armor.misc.chest.worker_red_1")), + (1, Item("common.items.armor.misc.chest.worker_purple_0")), + (1, Item("common.items.armor.misc.chest.worker_purple_1")), + (1, Item("common.items.armor.misc.chest.worker_yellow_0")), + (1, Item("common.items.armor.misc.chest.worker_yellow_1")), + (1, Item("common.items.armor.misc.chest.worker_orange_0")), + (1, Item("common.items.armor.misc.chest.worker_orange_1")), + ]), + legs: Choice([ + (1, Item("common.items.armor.misc.pants.worker_blue")), + (1, Item("common.items.armor.misc.pants.worker_brown")), + ]), + feet: Choice([ + (1, Item("common.items.armor.misc.foot.sandals")), + (1, Item("common.items.armor.cloth_blue.foot")), + ]), +) diff --git a/assets/common/loadout/village/herbalist.ron b/assets/common/loadout/village/herbalist.ron new file mode 100644 index 0000000000..e41a4f4fdb --- /dev/null +++ b/assets/common/loadout/village/herbalist.ron @@ -0,0 +1,26 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (3, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.hood")), + (2, None), + ]), + chest: Choice([ + (1, Item("common.items.armor.twigs.chest")), + (1, Item("common.items.armor.twigsflowers.chest")), + (1, Item("common.items.armor.twigsleaves.chest")), + ]), + legs: Choice([ + (1, Item("common.items.armor.twigs.pants")), + (1, Item("common.items.armor.twigsflowers.pants")), + (1, Item("common.items.armor.twigsleaves.pants")), + ]), + feet: Choice([ + (1, Item("common.items.armor.twigs.foot")), + (1, Item("common.items.armor.twigsflowers.foot")), + (1, Item("common.items.armor.twigsleaves.foot")), + (1, Item("common.items.armor.misc.foot.sandals")), + ]), +) diff --git a/assets/common/loadout/village/hunter.ron b/assets/common/loadout/village/hunter.ron new file mode 100644 index 0000000000..8a4933751d --- /dev/null +++ b/assets/common/loadout/village/hunter.ron @@ -0,0 +1,28 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (6, None), + (2, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.hood")), + (3, Item("common.items.armor.misc.head.hood_dark")), + ]), + chest: Choice([ + (1, Item("common.items.armor.hide.leather.chest")), + (1, Item("common.items.armor.hide.rawhide.chest")), + (1, Item("common.items.armor.hide.primal.chest")), + ]), + legs: Choice([ + (1, Item("common.items.armor.hide.leather.pants")), + (1, Item("common.items.armor.hide.rawhide.pants")), + (1, Item("common.items.armor.hide.primal.pants")), + ]), + feet: Choice([ + (1, None), + (2, Item("common.items.armor.misc.foot.sandals")), + (4, Item("common.items.armor.hide.leather.foot")), + (4, Item("common.items.armor.hide.rawhide.foot")), + (4, Item("common.items.armor.hide.primal.foot")), + ]), +) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index b672f073aa..8e34c97726 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -121,6 +121,8 @@ pub enum Profession { Pirate, #[serde(rename = "9")] Cultist, + #[serde(rename = "10")] + Herbalist, } impl Profession { @@ -136,6 +138,7 @@ impl Profession { Self::Alchemist => "Alchemist".to_string(), Self::Pirate => "Pirate".to_string(), Self::Cultist => "Cultist".to_string(), + Self::Herbalist => "Herbalist".to_string(), } } } diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs index d27c294e00..921d0cad58 100644 --- a/rtsim/src/data/faction.rs +++ b/rtsim/src/data/faction.rs @@ -10,6 +10,7 @@ use vek::*; #[derive(Clone, Serialize, Deserialize)] pub struct Faction { pub leader: Option, + pub good_or_evil: bool, // TODO: Very stupid, get rid of this } #[derive(Clone, Serialize, Deserialize)] diff --git a/rtsim/src/gen/faction.rs b/rtsim/src/gen/faction.rs index ed09d71594..b7a4c073dc 100644 --- a/rtsim/src/gen/faction.rs +++ b/rtsim/src/gen/faction.rs @@ -5,6 +5,9 @@ use world::{IndexRef, World}; impl Faction { pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { - Self { leader: None } + Self { + leader: None, + good_or_evil: rng.gen(), + } } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index eaf7e09ee0..e54a396fee 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -40,7 +40,7 @@ impl Data { time_of_day: TimeOfDay(settings.start_time), }; - let initial_factions = (0..10) + let initial_factions = (0..16) .map(|_| { let faction = Faction::generate(world, index, &mut rng); let wpos = world @@ -56,7 +56,13 @@ impl Data { // Register sites with rtsim for (world_site_id, _) in index.sites.iter() { - let site = Site::generate(world_site_id, world, index, &initial_factions); + let site = Site::generate( + world_site_id, + world, + index, + &initial_factions, + &this.factions, + ); this.sites.create(site); } info!( @@ -66,32 +72,51 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() - // TODO: Stupid - .filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) - .skip(1) - .take(1) + // TODO: Stupid + // .filter(|(_, site)| site.world_site.map_or(false, |ws| + // matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) + // .take(1) { + let good_or_evil = site + .faction + .and_then(|f| this.factions.get(f)) + .map_or(true, |f| f.good_or_evil); + let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); wpos2d .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..16 { - this.npcs.create( - Npc::new(rng.gen(), rand_wpos(&mut rng)) - .with_faction(site.faction) - .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - 11..=15 => Profession::Guard, - _ => Profession::Adventurer(rng.gen_range(0..=3)), - }), - ); + if good_or_evil { + for _ in 0..32 { + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=8 => Profession::Farmer, + 9..=10 => Profession::Herbalist, + 11..=15 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), + }), + ); + } + } else { + for _ in 0..5 { + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + _ => Profession::Cultist, + }), + ); + } } this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index d871360ca3..8325146ac4 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,4 +1,4 @@ -use crate::data::{FactionId, Site}; +use crate::data::{FactionId, Factions, Site}; use common::store::Id; use vek::*; use world::{ @@ -12,24 +12,42 @@ impl Site { world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)], + factions: &Factions, ) -> Self { let world_site = index.sites.get(world_site_id); let wpos = world_site.get_origin(); + // TODO: This is stupid, do better + let good_or_evil = match &world_site.kind { + // Good + SiteKind::Refactor(_) + | SiteKind::CliffTown(_) + | SiteKind::DesertCity(_) + | SiteKind::SavannahPit(_) => Some(true), + // Evil + SiteKind::Dungeon(_) | SiteKind::ChapelSite(_) | SiteKind::Gnarling(_) => Some(false), + // Neutral + SiteKind::Settlement(_) + | SiteKind::Castle(_) + | SiteKind::Tree(_) + | SiteKind::GiantTree(_) + | SiteKind::Bridge(_) => None, + }; + Self { wpos, world_site: Some(world_site_id), - faction: if matches!( - &world_site.kind, - SiteKind::Refactor(_) | SiteKind::CliffTown(_) | SiteKind::DesertCity(_) - ) { + faction: good_or_evil.and_then(|good_or_evil| { nearby_factions .iter() + .filter(|(_, faction)| { + factions + .get(*faction) + .map_or(false, |f| f.good_or_evil == good_or_evil) + }) .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) .map(|(_, faction)| *faction) - } else { - None - }, + }), } } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index f9c609391f..9fd1e5d2c3 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -317,10 +317,9 @@ impl Rule for NpcAi { fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") } /// Try to walk toward a 3D position without caring for obstacles. -fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { +fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { const STEP_DIST: f32 = 24.0; const WAYPOINT_DIST: f32 = 12.0; - const GOAL_DIST: f32 = 2.0; let mut waypoint = None; @@ -342,19 +341,19 @@ fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { ctx.controller.goto = Some((*waypoint, speed_factor)); }) .repeat() - .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2)) + .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z)) .map(|_| {}) } /// Try to walk toward a 2D position on the terrain without caring for /// obstacles. -fn goto_2d(wpos2d: Vec2, speed_factor: f32) -> impl Action { +fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action { const MIN_DIST: f32 = 2.0; now(move |ctx| { let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); - goto(wpos, speed_factor) + goto(wpos, speed_factor, goal_dist) }) } @@ -399,7 +398,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()); // Walk toward the node - goto_2d(node_wpos.as_(), 1.0) + goto_2d(node_wpos.as_(), 1.0, 8.0) .debug(move || format!("traversing track node ({}/{})", i + 1, track_len)) }))) }) @@ -407,7 +406,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { .boxed() } else if let Some(site) = sites.get(tgt_site) { // If all else fails, just walk toward the target site in a straight line - goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0).boxed() + goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0, 8.0).boxed() } else { // If we can't find a way to get to the site at all, there's nothing more to be done finish().boxed() @@ -431,10 +430,16 @@ fn adventure() -> impl Action { .sites .iter() .filter(|(site_id, site)| { - // TODO: faction.is_some() is used as a proxy for whether the site likely has - // paths, don't do this - site.faction.is_some() - && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) + // Only path toward towns + matches!( + site.world_site.map(|ws| &ctx.index.sites.get(ws).kind), + Some( + SiteKind::Refactor(_) + | SiteKind::CliffTown(_) + | SiteKind::SavannahPit(_) + | SiteKind::DesertCity(_) + ), + ) && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25) }) .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) @@ -487,7 +492,7 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - goto_2d(house_wpos, 0.5) + goto_2d(house_wpos, 0.5, 1.0) .debug(|| "walk to house") .then(idle().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) @@ -514,7 +519,7 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { // Walk to the plaza... - goto_2d(plaza_wpos, 0.5) + goto_2d(plaza_wpos, 0.5, 8.0) .debug(|| "walk to plaza") // ...then wait for some time before moving on .then({ diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index eebec822e5..c6636ef6c1 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -48,8 +48,13 @@ impl Rule for Setup { be generated afresh.", world_site_id ); - data.sites - .create(Site::generate(world_site_id, ctx.world, ctx.index, &[])); + data.sites.create(Site::generate( + world_site_id, + ctx.world, + ctx.index, + &[], + &data.factions, + )); } } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 421a6b24b3..586c6a9f48 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -23,7 +23,9 @@ use world::site::settlement::trader_loadout; fn humanoid_config(profession: &Profession) -> &'static str { match profession { - Profession::Farmer | Profession::Hunter => "common.entity.village.villager", + Profession::Farmer => "common.entity.village.farmer", + Profession::Hunter => "common.entity.village.hunter", + Profession::Herbalist => "common.entity.village.herbalist", Profession::Merchant => "common.entity.village.merchant", Profession::Guard => "common.entity.village.guard", Profession::Adventurer(rank) => match rank { @@ -59,6 +61,15 @@ fn farmer_loadout( trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) } +fn herbalist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Ingredients) + }) +} + fn chef_loadout( loadout_builder: LoadoutBuilder, economy: Option<&SiteInformation>, @@ -90,6 +101,7 @@ fn profession_extra_loadout( match profession { Some(Profession::Merchant) => merchant_loadout, Some(Profession::Farmer) => farmer_loadout, + Some(Profession::Herbalist) => herbalist_loadout, Some(Profession::Chef) => chef_loadout, Some(Profession::Blacksmith) => blacksmith_loadout, Some(Profession::Alchemist) => alchemist_loadout, @@ -102,6 +114,7 @@ fn profession_agent_mark(profession: Option<&Profession>) -> Option EntityInfo let mut rng = npc.rng(3); EntityInfo::at(pos.0) .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(comp::Alignment::Npc) + .with_alignment(if matches!(profession, Profession::Cultist) { + comp::Alignment::Enemy + } else { + comp::Alignment::Npc + }) .with_maybe_economy(economy.as_ref()) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref()))