Improved herbalist, hunter, farmer, added cultist factions

This commit is contained in:
Joshua Barretto 2023-01-05 20:25:32 +00:00
parent 2aa6ced357
commit 077da13a5f
14 changed files with 275 additions and 47 deletions

View File

@ -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: [],
)

View File

@ -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: [],
)

View File

@ -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: [],
)

View File

@ -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")),
]),
)

View File

@ -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")),
]),
)

View File

@ -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")),
]),
)

View File

@ -121,6 +121,8 @@ pub enum Profession {
Pirate, Pirate,
#[serde(rename = "9")] #[serde(rename = "9")]
Cultist, Cultist,
#[serde(rename = "10")]
Herbalist,
} }
impl Profession { impl Profession {
@ -136,6 +138,7 @@ impl Profession {
Self::Alchemist => "Alchemist".to_string(), Self::Alchemist => "Alchemist".to_string(),
Self::Pirate => "Pirate".to_string(), Self::Pirate => "Pirate".to_string(),
Self::Cultist => "Cultist".to_string(), Self::Cultist => "Cultist".to_string(),
Self::Herbalist => "Herbalist".to_string(),
} }
} }
} }

View File

@ -10,6 +10,7 @@ use vek::*;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Faction { pub struct Faction {
pub leader: Option<Actor>, pub leader: Option<Actor>,
pub good_or_evil: bool, // TODO: Very stupid, get rid of this
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]

View File

@ -5,6 +5,9 @@ use world::{IndexRef, World};
impl Faction { impl Faction {
pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self {
Self { leader: None } Self {
leader: None,
good_or_evil: rng.gen(),
}
} }
} }

View File

@ -40,7 +40,7 @@ impl Data {
time_of_day: TimeOfDay(settings.start_time), time_of_day: TimeOfDay(settings.start_time),
}; };
let initial_factions = (0..10) let initial_factions = (0..16)
.map(|_| { .map(|_| {
let faction = Faction::generate(world, index, &mut rng); let faction = Faction::generate(world, index, &mut rng);
let wpos = world let wpos = world
@ -56,7 +56,13 @@ impl Data {
// Register sites with rtsim // Register sites with rtsim
for (world_site_id, _) in index.sites.iter() { 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); this.sites.create(site);
} }
info!( info!(
@ -67,17 +73,23 @@ impl Data {
// Spawn some test entities at the sites // Spawn some test entities at the sites
for (site_id, site) in this.sites.iter() for (site_id, site) in this.sites.iter()
// TODO: Stupid // TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) // .filter(|(_, site)| site.world_site.map_or(false, |ws|
.skip(1) // matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1)
.take(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 rand_wpos = |rng: &mut SmallRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d wpos2d
.map(|e| e as f32 + 0.5) .map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
}; };
for _ in 0..16 { if good_or_evil {
for _ in 0..32 {
this.npcs.create( this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
@ -87,12 +99,25 @@ impl Data {
1 => Profession::Blacksmith, 1 => Profession::Blacksmith,
2 => Profession::Chef, 2 => Profession::Chef,
3 => Profession::Alchemist, 3 => Profession::Alchemist,
5..=10 => Profession::Farmer, 5..=8 => Profession::Farmer,
9..=10 => Profession::Herbalist,
11..=15 => Profession::Guard, 11..=15 => Profession::Guard,
_ => Profession::Adventurer(rng.gen_range(0..=3)), _ => 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( this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_home(site_id) .with_home(site_id)

View File

@ -1,4 +1,4 @@
use crate::data::{FactionId, Site}; use crate::data::{FactionId, Factions, Site};
use common::store::Id; use common::store::Id;
use vek::*; use vek::*;
use world::{ use world::{
@ -12,24 +12,42 @@ impl Site {
world: &World, world: &World,
index: IndexRef, index: IndexRef,
nearby_factions: &[(Vec2<i32>, FactionId)], nearby_factions: &[(Vec2<i32>, FactionId)],
factions: &Factions,
) -> Self { ) -> Self {
let world_site = index.sites.get(world_site_id); let world_site = index.sites.get(world_site_id);
let wpos = world_site.get_origin(); 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 { Self {
wpos, wpos,
world_site: Some(world_site_id), world_site: Some(world_site_id),
faction: if matches!( faction: good_or_evil.and_then(|good_or_evil| {
&world_site.kind,
SiteKind::Refactor(_) | SiteKind::CliffTown(_) | SiteKind::DesertCity(_)
) {
nearby_factions nearby_factions
.iter() .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)) .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos))
.map(|(_, faction)| *faction) .map(|(_, faction)| *faction)
} else { }),
None
},
} }
} }
} }

View File

@ -317,10 +317,9 @@ impl Rule for NpcAi {
fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") } fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") }
/// Try to walk toward a 3D position without caring for obstacles. /// Try to walk toward a 3D position without caring for obstacles.
fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action { fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
const STEP_DIST: f32 = 24.0; const STEP_DIST: f32 = 24.0;
const WAYPOINT_DIST: f32 = 12.0; const WAYPOINT_DIST: f32 = 12.0;
const GOAL_DIST: f32 = 2.0;
let mut waypoint = None; let mut waypoint = None;
@ -342,19 +341,19 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action {
ctx.controller.goto = Some((*waypoint, speed_factor)); ctx.controller.goto = Some((*waypoint, speed_factor));
}) })
.repeat() .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)) .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {}) .map(|_| {})
} }
/// Try to walk toward a 2D position on the terrain without caring for /// Try to walk toward a 2D position on the terrain without caring for
/// obstacles. /// obstacles.
fn goto_2d(wpos2d: Vec2<f32>, speed_factor: f32) -> impl Action { fn goto_2d(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
const MIN_DIST: f32 = 2.0; const MIN_DIST: f32 = 2.0;
now(move |ctx| { now(move |ctx| {
let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); 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_()); .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_());
// Walk toward the node // 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)) .debug(move || format!("traversing track node ({}/{})", i + 1, track_len))
}))) })))
}) })
@ -407,7 +406,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
.boxed() .boxed()
} else if let Some(site) = sites.get(tgt_site) { } else if let Some(site) = sites.get(tgt_site) {
// If all else fails, just walk toward the target site in a straight line // 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 { } else {
// If we can't find a way to get to the site at all, there's nothing more to be done // If we can't find a way to get to the site at all, there's nothing more to be done
finish().boxed() finish().boxed()
@ -431,10 +430,16 @@ fn adventure() -> impl Action {
.sites .sites
.iter() .iter()
.filter(|(site_id, site)| { .filter(|(site_id, site)| {
// TODO: faction.is_some() is used as a proxy for whether the site likely has // Only path toward towns
// paths, don't do this matches!(
site.faction.is_some() site.world_site.map(|ws| &ctx.index.sites.get(ws).kind),
&& ctx.npc.current_site.map_or(true, |cs| *site_id != cs) 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) && thread_rng().gen_bool(0.25)
}) })
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .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_()) 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") .debug(|| "walk to house")
.then(idle().repeat().debug(|| "wait in house")) .then(idle().repeat().debug(|| "wait in house"))
.stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) .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... // Walk to the plaza...
goto_2d(plaza_wpos, 0.5) goto_2d(plaza_wpos, 0.5, 8.0)
.debug(|| "walk to plaza") .debug(|| "walk to plaza")
// ...then wait for some time before moving on // ...then wait for some time before moving on
.then({ .then({

View File

@ -48,8 +48,13 @@ impl Rule for Setup {
be generated afresh.", be generated afresh.",
world_site_id world_site_id
); );
data.sites data.sites.create(Site::generate(
.create(Site::generate(world_site_id, ctx.world, ctx.index, &[])); world_site_id,
ctx.world,
ctx.index,
&[],
&data.factions,
));
} }
} }

View File

@ -23,7 +23,9 @@ use world::site::settlement::trader_loadout;
fn humanoid_config(profession: &Profession) -> &'static str { fn humanoid_config(profession: &Profession) -> &'static str {
match profession { 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::Merchant => "common.entity.village.merchant",
Profession::Guard => "common.entity.village.guard", Profession::Guard => "common.entity.village.guard",
Profession::Adventurer(rank) => match rank { Profession::Adventurer(rank) => match rank {
@ -59,6 +61,15 @@ fn farmer_loadout(
trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) 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( fn chef_loadout(
loadout_builder: LoadoutBuilder, loadout_builder: LoadoutBuilder,
economy: Option<&SiteInformation>, economy: Option<&SiteInformation>,
@ -90,6 +101,7 @@ fn profession_extra_loadout(
match profession { match profession {
Some(Profession::Merchant) => merchant_loadout, Some(Profession::Merchant) => merchant_loadout,
Some(Profession::Farmer) => farmer_loadout, Some(Profession::Farmer) => farmer_loadout,
Some(Profession::Herbalist) => herbalist_loadout,
Some(Profession::Chef) => chef_loadout, Some(Profession::Chef) => chef_loadout,
Some(Profession::Blacksmith) => blacksmith_loadout, Some(Profession::Blacksmith) => blacksmith_loadout,
Some(Profession::Alchemist) => alchemist_loadout, Some(Profession::Alchemist) => alchemist_loadout,
@ -102,6 +114,7 @@ fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent:
Some( Some(
Profession::Merchant Profession::Merchant
| Profession::Farmer | Profession::Farmer
| Profession::Herbalist
| Profession::Chef | Profession::Chef
| Profession::Blacksmith | Profession::Blacksmith
| Profession::Alchemist, | Profession::Alchemist,
@ -128,7 +141,11 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
let mut rng = npc.rng(3); let mut rng = npc.rng(3);
EntityInfo::at(pos.0) EntityInfo::at(pos.0)
.with_entity_config(entity_config, Some(config_asset), &mut rng) .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_maybe_economy(economy.as_ref())
.with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref()))
.with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref())) .with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref()))