Merge branch 'sam/cultist-raiders' into 'master'

Cultist raiders

See merge request veloren/veloren!2807
This commit is contained in:
Samuel Keiffer 2021-09-04 17:21:38 +00:00
commit 3fa89c7a22
8 changed files with 303 additions and 61 deletions

View File

@ -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

View File

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

View File

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

View File

@ -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 {

View File

@ -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);

View File

@ -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::<f32>() {
// 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::<f32>() {
// 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<Site>,
home_id: Id<Site>,
raid_complete: bool,
time_to_move: Option<f64>,
},
// 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<Site>, target_id: Id<Site>) -> 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) {

View File

@ -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);

View File

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