From 0f92f38967348a0da13cb279a52cf068606a6632 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 1 May 2023 18:29:32 +0100 Subject: [PATCH] Started adding wandering rtsim monsters --- .../common/entity/wild/aggressive/cyclops.ron | 11 +++ .../entity/wild/aggressive/werewolf.ron | 11 +++ assets/voxygen/i18n/en/body.ftl | 4 + assets/voxygen/i18n/en/npc.ftl | 14 ++- common/src/comp/body.rs | 8 ++ common/src/comp/body/biped_large.rs | 11 ++- common/src/comp/compass.rs | 10 ++ common/src/rtsim.rs | 9 ++ common/src/terrain/mod.rs | 22 +++++ rtsim/src/ai/mod.rs | 35 +++++-- rtsim/src/data/npc.rs | 32 +++++-- rtsim/src/gen/mod.rs | 79 +++++++++++----- rtsim/src/rule/npc_ai.rs | 91 +++++++++++-------- rtsim/src/rule/simulate_npcs.rs | 17 ++-- server/src/cmd.rs | 12 ++- server/src/rtsim/tick.rs | 18 ++-- 16 files changed, 286 insertions(+), 98 deletions(-) create mode 100644 assets/common/entity/wild/aggressive/cyclops.ron create mode 100644 assets/common/entity/wild/aggressive/werewolf.ron create mode 100644 assets/voxygen/i18n/en/body.ftl diff --git a/assets/common/entity/wild/aggressive/cyclops.ron b/assets/common/entity/wild/aggressive/cyclops.ron new file mode 100644 index 0000000000..1aba05a14d --- /dev/null +++ b/assets/common/entity/wild/aggressive/cyclops.ron @@ -0,0 +1,11 @@ +#![enable(implicit_some)] +( + name: Automatic, + body: RandomWith("cyclops"), + alignment: Alignment(Enemy), + loot: LootTable("common.loot_tables.creature.biped_large.default"), + inventory: ( + loadout: FromBody, + ), + meta: [], +) diff --git a/assets/common/entity/wild/aggressive/werewolf.ron b/assets/common/entity/wild/aggressive/werewolf.ron new file mode 100644 index 0000000000..5dfd15d609 --- /dev/null +++ b/assets/common/entity/wild/aggressive/werewolf.ron @@ -0,0 +1,11 @@ +#![enable(implicit_some)] +( + name: Automatic, + body: RandomWith("werewolf"), + alignment: Alignment(Enemy), + loot: LootTable("common.loot_tables.creature.biped_large.default"), + inventory: ( + loadout: FromBody, + ), + meta: [], +) diff --git a/assets/voxygen/i18n/en/body.ftl b/assets/voxygen/i18n/en/body.ftl new file mode 100644 index 0000000000..8f712a55f2 --- /dev/null +++ b/assets/voxygen/i18n/en/body.ftl @@ -0,0 +1,4 @@ +body-generic = creature +body-biped_large-cyclops = cyclops +body-biped_large-wendigo = wendigo +body-biped_large-werewolf = werewolf diff --git a/assets/voxygen/i18n/en/npc.ftl b/assets/voxygen/i18n/en/npc.ftl index 071f93a284..5805282a45 100644 --- a/assets/voxygen/i18n/en/npc.ftl +++ b/assets/voxygen/i18n/en/npc.ftl @@ -253,7 +253,11 @@ npc-speech-merchant_sell_directed = npc-speech-tell_site = .a0 = Have you visited { $site }? It's just { $dir } of here! .a1 = You should visit { $site } some time. - .a2 = If you travel { $dir }, you can get to { $site }. + .a2 = If you travel { $dist } to the { $dir }, you can get to { $site }. + .a3 = To the { $dir } you'll find { $site }, it's { $dist }. +npc-speech-tell_monster = + .a0 = They say there's a { $body } to the { $dir }, { $dist }... + .a1 = You think you're tough? To the { $dir } there's a { $body }. npc-speech-witness_murder = .a0 = Murderer! .a1 = How could you do this? @@ -273,4 +277,10 @@ npc-speech-dir_south_east = south-east npc-speech-dir_south = south npc-speech-dir_south_west = south-west npc-speech-dir_west = west -npc-speech-dir_north_west = north-west +npc-speech-dir_north_west = very far away + +npc-speech-dist_very_far = very far away +npc-speech-dist_far = far away +npc-speech-dist_ahead = some way away +npc-speech-dist_near = nearby +npc-speech-dist_near_to = very close diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 470b747242..7308abd268 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -18,6 +18,7 @@ pub mod theropod; use crate::{ assets::{self, Asset}, + comp::Content, consts::{HUMAN_DENSITY, WATER_DENSITY}, make_case_elim, npc::NpcKind, @@ -1076,6 +1077,13 @@ impl Body { } .into() } + + pub fn localize(&self) -> Content { + match self { + Self::BipedLarge(biped_large) => biped_large.localize(), + _ => Content::localized("body-generic"), + } + } } impl Component for Body { diff --git a/common/src/comp/body/biped_large.rs b/common/src/comp/body/biped_large.rs index 51db673f99..5cda9e3685 100644 --- a/common/src/comp/body/biped_large.rs +++ b/common/src/comp/body/biped_large.rs @@ -1,4 +1,4 @@ -use crate::{make_case_elim, make_proj_elim}; +use crate::{comp::Content, make_case_elim, make_proj_elim}; use rand::{seq::SliceRandom, thread_rng}; use serde::{Deserialize, Serialize}; @@ -23,6 +23,15 @@ impl Body { let body_type = *ALL_BODY_TYPES.choose(rng).unwrap(); Self { species, body_type } } + + pub fn localize(&self) -> Content { + Content::localized(match &self.species { + Species::Cyclops => "body-biped_large-cyclops", + Species::Wendigo => "body-biped_large-wendigo", + Species::Werewolf => "body-biped_large-werewolf", + _ => "body-generic", + }) + } } impl From for super::Body { diff --git a/common/src/comp/compass.rs b/common/src/comp/compass.rs index f5e2ad1a1b..7809347dbc 100644 --- a/common/src/comp/compass.rs +++ b/common/src/comp/compass.rs @@ -102,4 +102,14 @@ impl Distance { Distance::NextTo => "just around", } } + + pub fn localize_npc(&self) -> Content { + Content::localized(match self { + Self::VeryFar => "npc-speech-dist_very_far", + Self::Far => "npc-speech-dist_far", + Self::Ahead => "npc-speech-dist_ahead", + Self::Near => "npc-speech-dist_near", + Self::NextTo => "npc-speech-dist_near_to", + }) + } } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 9dc0dfa728..16964fedd1 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -286,6 +286,15 @@ pub enum ChunkResource { Ore, // Iron, copper, etc. } +// Note: the `serde(name = "...")` is to minimise the length of field +// identifiers for the sake of rtsim persistence +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Role { + Civilised(Option), + Wild, + Monster, +} + // Note: the `serde(name = "...")` is to minimise the length of field // identifiers for the sake of rtsim persistence #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index a2fb964cf8..93ec4833f2 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -80,6 +80,7 @@ impl TerrainChunkSize { pub trait CoordinateConversions { fn wpos_to_cpos(&self) -> Self; fn cpos_to_wpos(&self) -> Self; + fn cpos_to_wpos_center(&self) -> Self; } impl CoordinateConversions for Vec2 { @@ -90,6 +91,13 @@ impl CoordinateConversions for Vec2 { #[inline] fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32) } + + #[inline] + fn cpos_to_wpos_center(&self) -> Self { + self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + e * sz as i32 + sz as i32 / 2 + }) + } } impl CoordinateConversions for Vec2 { @@ -98,6 +106,13 @@ impl CoordinateConversions for Vec2 { #[inline] fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f32) } + + #[inline] + fn cpos_to_wpos_center(&self) -> Self { + self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + e * sz as f32 + sz as f32 / 2.0 + }) + } } impl CoordinateConversions for Vec2 { @@ -106,6 +121,13 @@ impl CoordinateConversions for Vec2 { #[inline] fn cpos_to_wpos(&self) -> Self { self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as f64) } + + #[inline] + fn cpos_to_wpos_center(&self) -> Self { + self.map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + e * sz as f64 + sz as f64 / 2.0 + }) + } } // TerrainChunkMeta diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 2fe6222151..dfaf40f07c 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -7,6 +7,7 @@ use crate::{ }; use common::resources::{Time, TimeOfDay}; use hashbrown::HashSet; +use itertools::Either; use rand_chacha::ChaChaRng; use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow}; use world::{IndexRef, World}; @@ -227,6 +228,22 @@ pub trait Action: Any + Send + Sync { { Debug(self, mk_info, PhantomData) } + + #[must_use] + fn l(self) -> Either + where + Self: Sized, + { + Either::Left(self) + } + + #[must_use] + fn r(self) -> Either + where + Self: Sized, + { + Either::Right(self) + } } impl Action for Box> { @@ -246,11 +263,11 @@ impl Action for Box> { fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { (**self).tick(ctx) } } -impl, B: Action> Action for itertools::Either { +impl, B: Action> Action for Either { fn is_same(&self, other: &Self) -> bool { match (self, other) { - (itertools::Either::Left(x), itertools::Either::Left(y)) => x.is_same(y), - (itertools::Either::Right(x), itertools::Either::Right(y)) => x.is_same(y), + (Either::Left(x), Either::Left(y)) => x.is_same(y), + (Either::Right(x), Either::Right(y)) => x.is_same(y), _ => false, } } @@ -259,22 +276,22 @@ impl, B: Action> Action for itertools::Either) { match self { - itertools::Either::Left(x) => x.backtrace(bt), - itertools::Either::Right(x) => x.backtrace(bt), + Either::Left(x) => x.backtrace(bt), + Either::Right(x) => x.backtrace(bt), } } fn reset(&mut self) { match self { - itertools::Either::Left(x) => x.reset(), - itertools::Either::Right(x) => x.reset(), + Either::Left(x) => x.reset(), + Either::Right(x) => x.reset(), } } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { match self { - itertools::Either::Left(x) => x.tick(ctx), - itertools::Either::Right(x) => x.tick(ctx), + Either::Left(x) => x.tick(ctx), + Either::Right(x) => x.tick(ctx), } } } diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 61f0cc7167..26cb86c379 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -9,7 +9,8 @@ use common::{ comp, grid::Grid, rtsim::{ - Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, + Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, Role, SiteId, + VehicleId, }, store::Id, terrain::CoordinateConversions, @@ -96,7 +97,7 @@ pub struct Npc { pub wpos: Vec3, pub body: comp::Body, - pub profession: Option, + pub role: Role, pub home: Option, pub faction: Option, pub riding: Option, @@ -138,7 +139,7 @@ impl Clone for Npc { Self { seed: self.seed, wpos: self.wpos, - profession: self.profession.clone(), + role: self.role.clone(), home: self.home, faction: self.faction, riding: self.riding.clone(), @@ -162,14 +163,14 @@ impl Npc { pub const PERM_ENTITY_CONFIG: u32 = 1; const PERM_NAME: u32 = 0; - pub fn new(seed: u32, wpos: Vec3, body: comp::Body) -> Self { + pub fn new(seed: u32, wpos: Vec3, body: comp::Body, role: Role) -> Self { Self { seed, wpos, body, personality: Default::default(), sentiments: Default::default(), - profession: None, + role, home: None, faction: None, riding: None, @@ -190,11 +191,15 @@ impl Npc { self } - // TODO: have a dedicated `NpcBuilder` type for this. - pub fn with_profession(mut self, profession: impl Into>) -> Self { - self.profession = profession.into(); - self - } + // // TODO: have a dedicated `NpcBuilder` type for this. + // pub fn with_profession(mut self, profession: impl Into>) + // -> Self { if let Role::Humanoid(p) = &mut self.role { + // *p = profession.into(); + // } else { + // panic!("Tried to assign profession {:?} to NPC, but has role {:?}, + // which cannot have a profession", profession.into(), self.role); } + // self + // } // TODO: have a dedicated `NpcBuilder` type for this. pub fn with_home(mut self, home: impl Into>) -> Self { @@ -232,6 +237,13 @@ impl Npc { // once we've decided that we want to pub fn get_name(&self) -> String { name::generate(&mut self.rng(Self::PERM_NAME)) } + pub fn profession(&self) -> Option { + match &self.role { + Role::Civilised(profession) => profession.clone(), + Role::Monster | Role::Wild => None, + } + } + pub fn cleanup(&mut self, reports: &Reports) { // Clear old or superfluous sentiments // TODO: It might be worth giving more important NPCs a higher sentiment diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 1c6c8c13ce..f8972e2333 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -12,8 +12,8 @@ use common::{ comp::{self, Body}, grid::Grid, resources::TimeOfDay, - rtsim::{Personality, WorldSettings}, - terrain::TerrainChunkSize, + rtsim::{Personality, Role, WorldSettings}, + terrain::{CoordinateConversions, TerrainChunkSize}, vol::RectVolSize, }; use rand::prelude::*; @@ -124,20 +124,20 @@ impl Data { rng.gen(), rand_wpos(&mut rng, matches_buildings), random_humanoid(&mut rng), + Role::Civilised(Some(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..=16 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), + })), ) .with_faction(site.faction) .with_home(site_id) - .with_personality(Personality::random(&mut rng)) - .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..=16 => Profession::Guard, - _ => Profession::Adventurer(rng.gen_range(0..=3)), - }), + .with_personality(Personality::random(&mut rng)), ); } } else { @@ -147,11 +147,11 @@ impl Data { rng.gen(), rand_wpos(&mut rng, matches_buildings), random_humanoid(&mut rng), + Role::Civilised(Some(Profession::Cultist)), ) .with_personality(Personality::random_evil(&mut rng)) .with_faction(site.faction) - .with_home(site_id) - .with_profession(Profession::Cultist), + .with_home(site_id), ); } } @@ -163,10 +163,10 @@ impl Data { rng.gen(), rand_wpos(&mut rng, matches_plazas), random_humanoid(&mut rng), + Role::Civilised(Some(Profession::Merchant)), ) .with_home(site_id) - .with_personality(Personality::random_good(&mut rng)) - .with_profession(Profession::Merchant), + .with_personality(Personality::random_good(&mut rng)), ); } } @@ -178,11 +178,15 @@ impl Data { .create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship)); this.npcs.create_npc( - Npc::new(rng.gen(), wpos, random_humanoid(&mut rng)) - .with_home(site_id) - .with_profession(Profession::Captain) - .with_personality(Personality::random_good(&mut rng)) - .steering(vehicle_id), + Npc::new( + rng.gen(), + wpos, + random_humanoid(&mut rng), + Role::Civilised(Some(Profession::Captain)), + ) + .with_home(site_id) + .with_personality(Personality::random_good(&mut rng)) + .steering(vehicle_id), ); } } @@ -211,10 +215,41 @@ impl Data { rng.gen(), rand_wpos(&mut rng), Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)), + Role::Wild, ) .with_home(site_id), ); } + + // Spawn monsters into the world + for _ in 0..100 { + // Try a few times to find a location that's not underwater + if let Some(pos) = (0..10) + .map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32))) + .find(|pos| world.sim().get(*pos).map_or(false, |c| !c.is_underwater())) + { + let wpos2d = pos.cpos_to_wpos_center(); + let wpos = wpos2d + .map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)); + + let species = match rng.gen_range(0..3) { + 0 => comp::body::biped_large::Species::Cyclops, + 1 => comp::body::biped_large::Species::Wendigo, + _ => comp::body::biped_large::Species::Werewolf, + }; + + this.npcs.create_npc(Npc::new( + rng.gen(), + wpos, + Body::BipedLarge(comp::body::biped_large::Body::random_with( + &mut rng, &species, + )), + Role::Monster, + )); + } + } + info!("Generated {} rtsim NPCs.", this.npcs.len()); this diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 588bf83dfc..f78e67ad30 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -11,9 +11,12 @@ use crate::{ }; use common::{ astar::{Astar, PathResult}, - comp::{compass::Direction, Content}, + comp::{ + compass::{Direction, Distance}, + Content, + }, path::Path, - rtsim::{Actor, ChunkResource, Profession, SiteId}, + rtsim::{Actor, ChunkResource, Profession, Role, SiteId}, spiral::Spiral2d, store::Id, terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize}, @@ -543,15 +546,16 @@ fn socialize() -> impl Action { now(|ctx| { // Skip most socialising actions if we're not loaded if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) { + // Sometimes dance if ctx.rng.gen_bool(0.15) { - return Either::Left( - just(|ctx| ctx.controller.do_dance()) - .repeat() - .stop_if(timeout(6.0)) - .debug(|| "dancing") - .map(|_| ()) - .boxed(), - ); + return just(|ctx| ctx.controller.do_dance()) + .repeat() + .stop_if(timeout(6.0)) + .debug(|| "dancing") + .map(|_| ()) + .l() + .l(); + // Talk to nearby NPCs } else if let Some(other) = ctx .state .data() @@ -559,31 +563,44 @@ fn socialize() -> impl Action { .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .choose(&mut ctx.rng) { - return Either::Left( - just(move |ctx| ctx.controller.say(other, if ctx.rng.gen_bool(0.3) - && let Some(current_site) = ctx.npc.current_site - && let Some(current_site) = ctx.state.data().sites.get(current_site) - && let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng) - && let Some(mention_site) = ctx.state.data().sites.get(*mention_site) - && let Some(mention_site_name) = mention_site.world_site - .map(|ws| ctx.index.sites.get(ws).name().to_string()) - { - Content::localized_with_args("npc-speech-tell_site", [ - ("site", Content::Plain(mention_site_name)), - ("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()), - ]) - } else { - ctx.npc.personality.get_generic_comment(&mut ctx.rng) - })) - // After greeting the actor, wait for a while + // Mention nearby sites + let comment = if ctx.rng.gen_bool(0.3) + && let Some(current_site) = ctx.npc.current_site + && let Some(current_site) = ctx.state.data().sites.get(current_site) + && let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng) + && let Some(mention_site) = ctx.state.data().sites.get(*mention_site) + && let Some(mention_site_name) = mention_site.world_site + .map(|ws| ctx.index.sites.get(ws).name().to_string()) + { + Content::localized_with_args("npc-speech-tell_site", [ + ("site", Content::Plain(mention_site_name)), + ("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()), + ("dist", Distance::from_length(mention_site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), + ]) + // Mention nearby monsters + } else if ctx.rng.gen_bool(0.3) + && let Some(monster) = ctx.state.data().npcs + .values() + .filter(|other| matches!(&other.role, Role::Monster)) + .min_by_key(|other| other.wpos.xy().distance(ctx.npc.wpos.xy()) as i32) + { + Content::localized_with_args("npc-speech-tell_monster", [ + ("body", monster.body.localize()), + ("dir", Direction::from_dir(monster.wpos.xy() - ctx.npc.wpos.xy()).localize_npc()), + ("dist", Distance::from_length(monster.wpos.xy().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), + ]) + } else { + ctx.npc.personality.get_generic_comment(&mut ctx.rng) + }; + return just(move |ctx| ctx.controller.say(other, comment.clone())) + // After talking, wait for a while .then(idle().repeat().stop_if(timeout(4.0))) .map(|_| ()) - .boxed(), - ); + .r().l(); } } - Either::Right(idle()) + idle().r() }) } @@ -611,7 +628,7 @@ fn adventure() -> impl Action { .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .map(|(site_id, _)| site_id) { - let wait_time = if matches!(ctx.npc.profession, Some(Profession::Merchant)) { + let wait_time = if matches!(ctx.npc.profession(), Some(Profession::Merchant)) { 60.0 * 15.0 } else { 60.0 * 3.0 @@ -729,7 +746,7 @@ fn villager(visiting_site: SiteId) -> impl Action { } if DayPeriod::from(ctx.time_of_day.0).is_dark() - && !matches!(ctx.npc.profession, Some(Profession::Guard)) + && !matches!(ctx.npc.profession(), Some(Profession::Guard)) { return important( now(move |ctx| { @@ -769,7 +786,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "find somewhere to sleep"), ); // Villagers with roles should perform those roles - } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) + } else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) { if let Some(forest_wpos) = find_forest(ctx) { return casual( @@ -782,7 +799,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { + } else if matches!(ctx.npc.profession(), Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { if let Some(forest_wpos) = find_forest(ctx) { return casual( just(|ctx| { @@ -798,7 +815,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) { + } else if matches!(ctx.npc.profession(), Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) { if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) { return casual( travel_to_point(plaza_wpos, 0.4) @@ -816,7 +833,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) + } else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) { return casual( just(|ctx| { @@ -1049,7 +1066,7 @@ fn humanoid() -> impl Action { } } else { let action = if matches!( - ctx.npc.profession, + ctx.npc.profession(), Some(Profession::Adventurer(_) | Profession::Merchant) ) { adventure().boxed() diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 08b60004c7..5a362a2166 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -5,7 +5,7 @@ use crate::{ }; use common::{ comp::{self, Body}, - rtsim::{Actor, NpcAction, NpcActivity, Personality}, + rtsim::{Actor, NpcAction, NpcActivity, Personality, Role}, terrain::CoordinateConversions, }; use rand::prelude::*; @@ -82,11 +82,15 @@ fn on_death(ctx: EventCtx) { Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) }; let npc_id = data.spawn_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) - .with_personality(Personality::random(&mut rng)) - .with_home(site_id) - .with_faction(npc.faction) - .with_profession(npc.profession.clone()), + Npc::new( + rng.gen(), + rand_wpos(&mut rng), + random_humanoid(&mut rng), + npc.role.clone(), + ) + .with_personality(Personality::random(&mut rng)) + .with_home(site_id) + .with_faction(npc.faction), ); Some((npc_id, site_id)) } else { @@ -126,6 +130,7 @@ fn on_death(ctx: EventCtx) { Body::BirdLarge(comp::body::bird_large::Body::random_with( &mut rng, species, )), + Role::Wild, ) .with_home(site_id), ); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index ecf79226bf..bccc3337fe 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -45,7 +45,7 @@ use common::{ outcome::Outcome, parse_cmd_args, resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay}, - rtsim::Actor, + rtsim::{Actor, Role}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, uid::{Uid, UidAllocator}, vol::ReadVol, @@ -1250,7 +1250,7 @@ fn handle_rtsim_info( let _ = writeln!(&mut info, "-- General Information --"); let _ = writeln!(&mut info, "Seed: {}", npc.seed); - let _ = writeln!(&mut info, "Profession: {:?}", npc.profession); + let _ = writeln!(&mut info, "Role: {:?}", npc.role); let _ = writeln!(&mut info, "Home: {:?}", npc.home); let _ = writeln!(&mut info, "Faction: {:?}", npc.faction); let _ = writeln!(&mut info, "Personality: {:?}", npc.personality); @@ -1302,10 +1302,14 @@ fn handle_rtsim_npc( .enumerate() .filter(|(idx, npc)| { let tags = [ - npc.profession - .as_ref() + npc.profession() .map(|p| format!("{:?}", p)) .unwrap_or_default(), + match &npc.role { + Role::Civilised(_) => "civilised".to_string(), + Role::Wild => "wild".to_string(), + Role::Monster => "monster".to_string(), + }, format!("{:?}", npc.mode), format!("{}", idx), ]; diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 7f2c72cd6d..d2e5bfa4c0 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -138,13 +138,13 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo let pos = comp::Pos(npc.wpos); let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG); - if let Some(ref profession) = npc.profession { + if let Some(profession) = npc.profession() { let economy = npc.home.and_then(|home| { let site = sites.get(home)?.world_site?; index.sites.get(site).trade_information(site.id()) }); - let config_asset = humanoid_config(profession); + let config_asset = humanoid_config(&profession); let entity_config = EntityConfig::from_asset_expect_owned(config_asset) .with_body(BodyBuilder::Exact(npc.body)); @@ -156,9 +156,9 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo comp::Alignment::Npc }) .with_economy(economy.as_ref()) - .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) + .with_lazy_loadout(profession_extra_loadout(Some(&profession))) .with_alias(npc.get_name()) - .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) + .with_agent_mark(profession_agent_mark(Some(&profession))) } else { let config_asset = match npc.body { Body::BirdLarge(body) => match body.species { @@ -169,14 +169,18 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo // which limits what species are used _ => unimplemented!(), }, + Body::BipedLarge(body) => match body.species { + comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops", + comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo", + comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf", + _ => unimplemented!(), + }, _ => unimplemented!(), }; let entity_config = EntityConfig::from_asset_expect_owned(config_asset) .with_body(BodyBuilder::Exact(npc.body)); - EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(comp::Alignment::Wild) + EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng) } }