mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'sam/cultist-raiders' into 'master'
Cultist raiders See merge request veloren/veloren!2807
This commit is contained in:
commit
3fa89c7a22
@ -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
|
||||
|
||||
|
@ -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"))),
|
||||
|
@ -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"))),
|
||||
]),
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user