diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa1c14199..5df2a2b272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shooting sprites, such as apples and hives, can knock them out of trees - Sprite pickup animations - Add VELOREN_ASSETS_OVERRIDE variable for specifying folder to partially override assets. +- Cultist raiders ### Changed diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 283fd9f054..53ccc29b63 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -8,6 +8,7 @@ EntityConfig ( hands: TwoHanded(Choice([ (2.0, Some(Item("common.items.weapons.axe_1h.orichalcum-0"))), (4.0, Some(Item("common.items.weapons.sword.cultist"))), + (2.0, Some(Item("common.items.weapons.staff.cultist_staff"))), (2.0, Some(Item("common.items.weapons.hammer.cultist_purp_2h-0"))), (2.0, Some(Item("common.items.weapons.hammer_1h.orichalcum-0"))), (2.0, Some(Item("common.items.weapons.bow.velorite"))), diff --git a/assets/common/loadout/dungeon/tier-5/cultist.ron b/assets/common/loadout/dungeon/tier-5/cultist.ron index 426a32c963..ecfbae5e64 100644 --- a/assets/common/loadout/dungeon/tier-5/cultist.ron +++ b/assets/common/loadout/dungeon/tier-5/cultist.ron @@ -14,4 +14,14 @@ ]), Glider: Item("common.items.glider.glider_purp"), + + ActiveMainhand: Choice([ + (2.0, Some(Item("common.items.weapons.axe_1h.orichalcum-0"))), + (4.0, Some(Item("common.items.weapons.sword.cultist"))), + (2.0, Some(Item("common.items.weapons.staff.cultist_staff"))), + (2.0, Some(Item("common.items.weapons.hammer.cultist_purp_2h-0"))), + (2.0, Some(Item("common.items.weapons.hammer_1h.orichalcum-0"))), + (2.0, Some(Item("common.items.weapons.bow.velorite"))), + (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), + ]), }) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 72cd5bc641..0f21db8320 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -504,6 +504,11 @@ impl Agent { self.position_pid_controller = Some(pid); self } + + pub fn with_aggro_no_warn(mut self) -> Self { + self.psyche.aggro_dist = None; + self + } } impl Component for Agent { diff --git a/server/src/lib.rs b/server/src/lib.rs index 97ecf22a93..6ef9676c59 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -468,7 +468,7 @@ impl Server { // Initiate real-time world simulation #[cfg(feature = "worldgen")] - rtsim::init(&mut state, &world); + rtsim::init(&mut state, &world, index.as_index_ref()); #[cfg(not(feature = "worldgen"))] rtsim::init(&mut state); diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index c79fb4c04f..6cd177872e 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -21,10 +21,15 @@ pub struct Entity { pub seed: u32, pub last_time_ticked: f64, pub controller: RtSimController, - + pub kind: RtSimEntityKind, pub brain: Brain, } +pub enum RtSimEntityKind { + Random, + Cultist, +} + const PERM_SPECIES: u32 = 0; const PERM_BODY: u32 = 1; const PERM_LOADOUT: u32 = 2; @@ -35,22 +40,34 @@ impl Entity { pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } pub fn get_body(&self) -> comp::Body { - match self.rng(PERM_GENUS).gen::() { - // we want 5% airships, 45% birds, 50% humans - x if x < 0.05 => comp::ship::Body::random_with(&mut self.rng(PERM_BODY)).into(), - x if x < 0.45 => { - let species = *(&comp::bird_medium::ALL_SPECIES) - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_medium::Body::random_with(&mut self.rng(PERM_BODY), &species).into() + match self.kind { + RtSimEntityKind::Random => { + match self.rng(PERM_GENUS).gen::() { + // we want 5% airships, 45% birds, 50% humans + x if x < 0.05 => comp::ship::Body::random_with(&mut self.rng(PERM_BODY)).into(), + x if x < 0.45 => { + let species = *(&comp::bird_medium::ALL_SPECIES) + .choose(&mut self.rng(PERM_SPECIES)) + .unwrap(); + comp::bird_medium::Body::random_with(&mut self.rng(PERM_BODY), &species) + .into() + }, + x if x < 0.50 => { + let species = *(&comp::bird_large::ALL_SPECIES) + .choose(&mut self.rng(PERM_SPECIES)) + .unwrap(); + comp::bird_large::Body::random_with(&mut self.rng(PERM_BODY), &species) + .into() + }, + _ => { + let species = *(&comp::humanoid::ALL_SPECIES) + .choose(&mut self.rng(PERM_SPECIES)) + .unwrap(); + comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() + }, + } }, - x if x < 0.50 => { - let species = *(&comp::bird_large::ALL_SPECIES) - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_large::Body::random_with(&mut self.rng(PERM_BODY), &species).into() - }, - _ => { + RtSimEntityKind::Cultist => { let species = *(&comp::humanoid::ALL_SPECIES) .choose(&mut self.rng(PERM_SPECIES)) .unwrap(); @@ -60,30 +77,47 @@ impl Entity { } pub fn get_name(&self) -> String { - use common::{generation::get_npc_name, npc::NPC_NAMES}; - let npc_names = NPC_NAMES.read(); - match self.get_body() { - comp::Body::BirdMedium(b) => { - get_npc_name(&npc_names.bird_medium, b.species).to_string() + match self.kind { + RtSimEntityKind::Random => { + use common::{generation::get_npc_name, npc::NPC_NAMES}; + let npc_names = NPC_NAMES.read(); + match self.get_body() { + comp::Body::BirdMedium(b) => { + get_npc_name(&npc_names.bird_medium, b.species).to_string() + }, + comp::Body::BirdLarge(b) => { + get_npc_name(&npc_names.bird_large, b.species).to_string() + }, + comp::Body::Dragon(b) => get_npc_name(&npc_names.dragon, b.species).to_string(), + comp::Body::Humanoid(b) => { + get_npc_name(&npc_names.humanoid, b.species).to_string() + }, + comp::Body::Ship(_) => "Veloren Air".to_string(), + //TODO: finish match as necessary + _ => unimplemented!(), + } }, - comp::Body::BirdLarge(b) => get_npc_name(&npc_names.bird_large, b.species).to_string(), - comp::Body::Dragon(b) => get_npc_name(&npc_names.dragon, b.species).to_string(), - comp::Body::Humanoid(b) => get_npc_name(&npc_names.humanoid, b.species).to_string(), - comp::Body::Ship(_) => "Veloren Air".to_string(), - //TODO: finish match as necessary - _ => unimplemented!(), + RtSimEntityKind::Cultist => "Cultist Raider".to_string(), } } pub fn get_loadout(&self) -> comp::inventory::loadout::Loadout { let mut rng = self.rng(PERM_LOADOUT); - - 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)), + match self.kind { + RtSimEntityKind::Random => { + 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)), + ) + .build() + }, + RtSimEntityKind::Cultist => LoadoutBuilder::from_asset_expect( + "common.loadout.dungeon.tier-5.cultist", + Some(&mut rng), ) - .build() + .build(), + } } pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { @@ -441,6 +475,104 @@ impl Entity { Travel::Lost } }, + Travel::DirectRaid { + target_id, + home_id, + raid_complete, + time_to_move, + } => { + // Destination site is home if raid is complete, else it is target site + let dest_site = if raid_complete { + &world.civs().sites[home_id] + } else { + &world.civs().sites[target_id] + }; + let destination_name = dest_site + .site_tmp + .map_or("".to_string(), |id| index.sites[id].name().to_string()); + + let wpos = dest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; + + // Once at site, stay for a bit, then move to other site + if dist < 128_u32.pow(2) { + // If time_to_move is not set yet, use current time, ceiling to nearest multiple + // of 100, and then add another 100. + let time_to_move = if time_to_move.is_none() { + // Time increment is how long raiders stay at a site about. Is longer for + // home site and shorter for target site. + let time_increment = if raid_complete { 300.0 } else { 60.0 }; + Some((time.0 / time_increment).ceil() * time_increment + time_increment) + } else { + time_to_move + }; + + // If the time has come to move, flip raid bool + if time_to_move.map_or(false, |t| time.0 > t) { + Travel::DirectRaid { + target_id, + home_id, + raid_complete: !raid_complete, + time_to_move: None, + } + } else { + let theta = (time.0 / 30.0).floor() as f32 * self.seed as f32; + // Otherwise wander around site (or "plunder" if target site) + let travel_to = + wpos.map(|e| e as f32) + Vec2::new(theta.cos(), theta.sin()) * 100.0; + let travel_to_alt = world + .sim() + .get_alt_approx(travel_to.map(|e| e as i32)) + .unwrap_or(0.0) as i32; + let travel_to = terrain + .find_space(Vec3::new( + travel_to.x as i32, + travel_to.y as i32, + travel_to_alt, + )) + .map(|e| e as f32) + + Vec3::new(0.5, 0.5, 0.0); + + self.controller.travel_to = Some((travel_to, destination_name)); + self.controller.speed_factor = 0.75; + Travel::DirectRaid { + target_id, + home_id, + raid_complete, + time_to_move, + } + } + } else { + let travel_to = self.pos.xy() + + Vec3::from( + (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) + .try_normalized() + .unwrap_or_else(Vec2::zero), + ) * 64.0; + let travel_to_alt = world + .sim() + .get_alt_approx(travel_to.map(|e| e as i32)) + .unwrap_or(0.0) as i32; + let travel_to = terrain + .find_space(Vec3::new( + travel_to.x as i32, + travel_to.y as i32, + travel_to_alt, + )) + .map(|e| e as f32) + + Vec3::new(0.5, 0.5, 0.0); + + self.controller.travel_to = Some((travel_to, destination_name)); + self.controller.speed_factor = 0.90; + Travel::DirectRaid { + target_id, + home_id, + raid_complete, + time_to_move, + } + } + }, + Travel::Idle => Travel::Idle, }; // Forget old memories @@ -482,6 +614,15 @@ enum Travel { progress: usize, reversed: bool, }, + // Move directly towards a target site, then head back to a home territory + DirectRaid { + target_id: Id, + home_id: Id, + raid_complete: bool, + time_to_move: Option, + }, + // For testing purposes + Idle, } impl Default for Travel { @@ -498,6 +639,31 @@ pub struct Brain { } impl Brain { + pub fn idle() -> Self { + Self { + begin: None, + tgt: None, + route: Travel::Idle, + last_visited: None, + memories: Vec::new(), + } + } + + pub fn raid(home_id: Id, target_id: Id) -> Self { + Self { + begin: None, + tgt: None, + route: Travel::DirectRaid { + target_id, + home_id, + raid_complete: false, + time_to_move: None, + }, + last_visited: None, + memories: Vec::new(), + } + } + pub fn add_memory(&mut self, memory: Memory) { self.memories.push(memory); } pub fn forget_enemy(&mut self, to_forget: &str) { diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index f0ff8a19cf..0735f72b98 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -20,7 +20,7 @@ use slab::Slab; use specs::{DispatcherBuilder, WorldExt}; use vek::*; -pub use self::entity::Entity; +pub use self::entity::{Brain, Entity, RtSimEntityKind}; pub struct RtSim { tick: u64, @@ -104,7 +104,11 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { ]); } -pub fn init(state: &mut State, #[cfg(feature = "worldgen")] world: &world::World) { +pub fn init( + state: &mut State, + #[cfg(feature = "worldgen")] world: &world::World, + #[cfg(feature = "worldgen")] index: world::IndexRef, +) { #[cfg(feature = "worldgen")] let mut rtsim = RtSim::new(world.sim().get_size()); #[cfg(not(feature = "worldgen"))] @@ -113,22 +117,69 @@ pub fn init(state: &mut State, #[cfg(feature = "worldgen")] world: &world::World // TODO: Determine number of rtsim entities based on things like initial site // populations rather than world size #[cfg(feature = "worldgen")] - for _ in 0..world.sim().get_size().product() / 400 { - let pos = rtsim - .chunks - .size() - .map2(TerrainChunk::RECT_SIZE, |sz, chunk_sz| { - thread_rng().gen_range(0..sz * chunk_sz) as i32 - }); + { + for _ in 0..world.sim().get_size().product() / 400 { + let pos = rtsim + .chunks + .size() + .map2(TerrainChunk::RECT_SIZE, |sz, chunk_sz| { + thread_rng().gen_range(0..sz * chunk_sz) as i32 + }); - rtsim.entities.insert(Entity { - is_loaded: false, - pos: Vec3::from(pos.map(|e| e as f32)), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - brain: Default::default(), - }); + rtsim.entities.insert(Entity { + is_loaded: false, + pos: Vec3::from(pos.map(|e| e as f32)), + seed: thread_rng().gen(), + controller: RtSimController::default(), + last_time_ticked: 0.0, + kind: RtSimEntityKind::Random, + brain: Default::default(), + }); + } + for (site_id, site) in world + .civs() + .sites + .iter() + .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) + { + use world::site::SiteKind; + #[allow(clippy::single_match)] + match &site.kind { + #[allow(clippy::single_match)] + SiteKind::Dungeon(dungeon) => match dungeon.dungeon_difficulty() { + Some(5) => { + let pos = site.get_origin(); + if let Some(nearest_village) = world + .civs() + .sites + .iter() + .filter(|s| s.1.is_settlement()) + .min_by_key(|(_, site)| { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + wpos.map(|e| e as f32) + .distance_squared(pos.map(|x| x as f32)) + as u32 + }) + .map(|(id, _)| id) + { + for _ in 0..25 { + rtsim.entities.insert(Entity { + is_loaded: false, + pos: Vec3::from(pos.map(|e| e as f32)), + seed: thread_rng().gen(), + controller: RtSimController::default(), + last_time_ticked: 0.0, + kind: RtSimEntityKind::Cultist, + brain: Brain::raid(site_id, nearest_village), + }); + } + } + }, + _ => {}, + }, + _ => {}, + } + } } state.ecs_mut().insert(rtsim); diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index b813461e7c..df17a84b1a 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -107,12 +107,24 @@ impl<'a> System<'a> for Sys { rtsim.reify_entity(id); let entity = &rtsim.entities[id]; let body = entity.get_body(); + let alignment = match body { + comp::Body::Humanoid(_) => match entity.kind { + RtSimEntityKind::Random => comp::Alignment::Npc, + RtSimEntityKind::Cultist => comp::Alignment::Enemy, + }, + comp::Body::BirdLarge(bird_large) => match bird_large.species { + comp::bird_large::Species::Roc => comp::Alignment::Enemy, + comp::bird_large::Species::Cockatrice => comp::Alignment::Enemy, + _ => comp::Alignment::Wild, + }, + _ => comp::Alignment::Wild, + }; let spawn_pos = terrain .find_space(entity.pos.map(|e| e.floor() as i32)) .map(|e| e as f32) + Vec3::new(0.5, 0.5, body.flying_height()); let pos = comp::Pos(spawn_pos); - let agent = Some(comp::Agent::from_body(&body).with_behavior( + let mut agent = Some(comp::Agent::from_body(&body).with_behavior( if matches!(body, comp::Body::Humanoid(_)) { Behavior::from(BehaviorCapability::SPEAK) } else { @@ -120,6 +132,10 @@ impl<'a> System<'a> for Sys { }, )); + if matches!(alignment, comp::Alignment::Enemy) { + agent = agent.map(|a| a.with_aggro_no_warn()); + } + let rtsim_entity = Some(RtSimEntity(id)); // TODO: this should be a bit more intelligent @@ -145,15 +161,7 @@ impl<'a> System<'a> for Sys { poise: comp::Poise::new(body), body, agent, - alignment: match body { - comp::Body::Humanoid(_) => comp::Alignment::Npc, - comp::Body::BirdLarge(bird_large) => match bird_large.species { - comp::bird_large::Species::Roc => comp::Alignment::Enemy, - comp::bird_large::Species::Cockatrice => comp::Alignment::Enemy, - _ => comp::Alignment::Wild, - }, - _ => comp::Alignment::Wild, - }, + alignment, scale: match body { comp::Body::Ship(_) => comp::Scale(comp::ship::AIRSHIP_SCALE), _ => comp::Scale(1.0),