Merge branch 'crabman/npc-pets' into 'master'

Allow pets in entity configurations

See merge request veloren/veloren!4344
This commit is contained in:
crabman 2024-03-06 20:35:41 +00:00
commit 130aa2aa99
16 changed files with 358 additions and 246 deletions

View File

@ -14,7 +14,8 @@
]), None)),
)),
),
pets: [("common.entity.dungeon.cultist.hound", ( start: 4, end: 5 ))],
meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"),
],
)
)

View File

@ -8,4 +8,4 @@
loadout: FromBody,
),
meta: [],
)
)

View File

@ -14,7 +14,7 @@ use std::{collections::VecDeque, fmt};
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
use super::{dialogue::Subject, Pos};
use super::{dialogue::Subject, Group, Pos};
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
@ -112,6 +112,18 @@ impl Alignment {
_ => false,
}
}
/// Default group for this alignment
// NOTE: This is a HACK
pub fn group(&self) -> Option<Group> {
match self {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(super::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(super::group::NPC),
Alignment::Owned(_) => None,
}
}
}
impl Component for Alignment {

View File

@ -3,6 +3,7 @@ use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use slab::Slab;
use specs::{storage::GenericReadStorage, Component, DerefFlaggedStorage, Join, LendJoin};
use std::iter;
use tracing::{error, warn};
// Primitive group system
@ -309,19 +310,6 @@ impl GroupManager {
return;
}
self.remove_from_group(member, groups, alignments, uids, entities, notifier, false);
// Set NPC back to their group
if let Some(alignment) = alignments.get(member) {
match alignment {
Alignment::Npc => {
let _ = groups.insert(member, NPC);
},
Alignment::Enemy => {
let _ = groups.insert(member, ENEMY);
},
_ => {},
}
}
}
pub fn entity_deleted(
@ -367,7 +355,7 @@ impl GroupManager {
.join()
.filter(|(e, _, g, _)| **g == group && !(to_be_deleted && *e == member))
.fold(
HashMap::<Uid, (Option<specs::Entity>, Vec<specs::Entity>)>::new(),
HashMap::<Uid, (Option<specs::Entity>, Option<Alignment>, Vec<specs::Entity>)>::new(),
|mut acc, (e, uid, _, alignment)| {
if let Some(owner) = alignment.and_then(|a| match a {
Alignment::Owned(owner) if uid != owner => Some(owner),
@ -375,10 +363,14 @@ impl GroupManager {
}) {
// A pet
// Assumes owner will be in the group
acc.entry(*owner).or_default().1.push(e);
acc.entry(*owner).or_default().2.push(e);
} else {
// Not a pet
acc.entry(*uid).or_default().0 = Some(e);
//
// Entry may already exist from inserting pets
let entry = acc.entry(*uid).or_default();
entry.0 = Some(e);
entry.1 = alignment.copied();
}
acc
@ -386,9 +378,15 @@ impl GroupManager {
)
.into_iter()
.map(|(_, v)| v)
.for_each(|(owner, pets)| {
.for_each(|(owner, alignment, pets)| {
if let Some(owner) = owner {
if !pets.is_empty() {
if let Some(special_group) = alignment.and_then(|a| a.group()) {
// Set NPC and pets back to their special alignment based group
for entity in iter::once(owner).chain(pets) {
groups.insert(entity, special_group)
.expect("entity from join above so it must exist");
}
} else if !pets.is_empty() {
let mut members =
pets.iter().map(|e| (*e, Role::Pet)).collect::<Vec<_>>();
members.push((owner, Role::Member));
@ -415,8 +413,8 @@ impl GroupManager {
});
}
});
// Not leader
} else {
// Not leader
let leaving_member_uid = if let Some(uid) = uids.get(member) {
*uid
} else {
@ -426,8 +424,24 @@ impl GroupManager {
let leaving_pets = pets(member, leaving_member_uid, alignments, entities);
// If pets and not about to be deleted form new group
if !leaving_pets.is_empty() && !to_be_deleted {
// Handle updates for the leaving group member and their pets.
if let Some(special_group) = alignments.get(member).and_then(|a| a.group())
&& !to_be_deleted
{
// Set NPC and pets back to their the special alignment based group
for entity in iter::once(member).chain(leaving_pets.iter().copied()) {
groups
.insert(entity, special_group)
.expect("entity from join above so it must exist");
}
// Form new group if these conditions are met:
// * Alignment not for a special npc group (handled above)
// * The entity has pets.
// * The entity isn't about to be deleted.
} else if !leaving_pets.is_empty()
&& !to_be_deleted
&& alignments.get(member).and_then(Alignment::group).is_none()
{
let new_group = self.create_group(member, 1);
notifier(member, ChangeNotification::NewGroup {
@ -435,7 +449,7 @@ impl GroupManager {
members: leaving_pets
.iter()
.map(|p| (*p, Role::Pet))
.chain(std::iter::once((member, Role::Member)))
.chain(iter::once((member, Role::Member)))
.collect(),
});
@ -451,6 +465,8 @@ impl GroupManager {
});
}
// Handle updates for the rest of the group, potentially disbanding it if there
// is now only one member left.
if let Some(info) = self.group_info_mut(group) {
// If not pet, decrement number of members
if !matches!(alignments.get(member), Some(Alignment::Owned(owner)) if uids.get(member).map_or(true, |uid| uid != owner))

View File

@ -32,7 +32,7 @@ impl Component for Object {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct PortalData {
pub target: Vec3<f32>,
pub requires_no_aggro: bool,

View File

@ -59,6 +59,7 @@ pub struct NpcBuilder {
pub scale: comp::Scale,
pub anchor: Option<comp::Anchor>,
pub loot: LootSpec<String>,
pub pets: Vec<(NpcBuilder, Vec3<f32>)>,
pub rtsim_entity: Option<RtSimEntity>,
pub projectile: Option<comp::Projectile>,
}
@ -79,6 +80,7 @@ impl NpcBuilder {
loot: LootSpec::Nothing,
rtsim_entity: None,
projectile: None,
pets: Vec::new(),
}
}
@ -131,6 +133,11 @@ impl NpcBuilder {
self.loot = loot;
self
}
pub fn with_pets(mut self, pets: Vec<(NpcBuilder, Vec3<f32>)>) -> Self {
self.pets = pets;
self
}
}
pub struct ClientConnectedEvent {

View File

@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use crate::{
assets::{self, AssetExt, Error},
calendar::Calendar,
@ -15,6 +17,7 @@ use crate::{
};
use enum_map::EnumMap;
use serde::Deserialize;
use tracing::error;
use vek::*;
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
@ -136,6 +139,11 @@ pub struct EntityConfig {
/// Check docs for `InventorySpec` struct in this file.
pub inventory: InventorySpec,
/// Pets to spawn with this entity (specified as a list of asset paths).
/// The range represents how many pets will be spawned.
#[serde(default)]
pub pets: Vec<(String, RangeInclusive<usize>)>,
/// Meta Info for optional fields
/// Possible fields:
/// SkillSetAsset(String) with asset_specifier for skillset
@ -204,8 +212,7 @@ pub struct EntityInfo {
// Skills
pub skillset_asset: Option<String>,
// Not implemented
pub pet: Option<Box<EntityInfo>>,
pub pets: Vec<EntityInfo>,
// Economy
// we can't use DHashMap, do we want to move that into common?
@ -236,7 +243,7 @@ impl EntityInfo {
loadout: LoadoutBuilder::empty(),
make_loadout: None,
skillset_asset: None,
pet: None,
pets: Vec::new(),
trading_information: None,
special_entity: None,
}
@ -279,6 +286,7 @@ impl EntityInfo {
inventory,
loot,
meta,
pets,
} = config;
match body {
@ -316,6 +324,26 @@ impl EntityInfo {
// NOTE: set loadout after body, as it's used with default equipement
self = self.with_inventory(inventory, config_asset, loadout_rng, time);
let mut pet_infos: Vec<EntityInfo> = Vec::new();
for (pet_asset, amount) in pets {
let config = EntityConfig::load_expect(&pet_asset).read();
let (start, mut end) = amount.into_inner();
if start > end {
error!("Invalid range for pet count start: {start}, end: {end}");
end = start;
}
pet_infos.extend((0..loadout_rng.gen_range(start..=end)).map(|_| {
EntityInfo::at(self.pos).with_entity_config(
config.clone(),
config_asset,
loadout_rng,
time,
)
}));
}
self.pets = pet_infos;
// Prefer the new configuration, if possible
let AgentConfig {
has_agency,
@ -675,6 +703,29 @@ mod tests {
}
}
#[cfg(test)]
fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
(
EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
}),
amount,
)
}) {
assert!(
amount.end() >= amount.start(),
"Invalid pet spawn range ({}..={}), in {}",
amount.start(),
amount.end(),
config_asset
);
if !pet.pets.is_empty() {
panic!("Pets must not be owners of pets: {config_asset}");
}
}
}
#[test]
fn test_all_entity_assets() {
// Get list of entity configs, load everything, validate content.
@ -689,6 +740,7 @@ mod tests {
loot,
meta,
alignment: _, // can't fail if serialized, it's a boring enum
pets,
} = EntityConfig::from_asset_expect_owned(&config_asset);
validate_body(&body, &config_asset);
@ -698,6 +750,7 @@ mod tests {
// misc
validate_loot(loot, &config_asset);
validate_meta(meta, &config_asset);
validate_pets(pets, &config_asset);
}
}
}

View File

@ -9,7 +9,7 @@ use crate::{
server_description::ServerDescription, Ban, BanAction, BanInfo, EditableSetting,
SettingError, WhitelistInfo, WhitelistRecord,
},
sys::terrain::NpcData,
sys::terrain::SpawnEntityData,
weather::WeatherJob,
wiring,
wiring::OutputFormula,
@ -40,8 +40,8 @@ use common::{
depot,
effect::Effect,
event::{
ClientDisconnectEvent, CreateWaypointEvent, EventBus, ExplosionEvent, GroupManipEvent,
InitiateInviteEvent, TamePetEvent,
ClientDisconnectEvent, CreateNpcEvent, CreateWaypointEvent, EventBus, ExplosionEvent,
GroupManipEvent, InitiateInviteEvent, TamePetEvent,
},
generation::{EntityConfig, EntityInfo},
link::Is,
@ -726,68 +726,26 @@ fn handle_make_npc(
None,
);
match NpcData::from_entity_info(entity_info) {
NpcData::Waypoint(_) => {
match SpawnEntityData::from_entity_info(entity_info) {
SpawnEntityData::Waypoint(_) => {
return Err(Content::localized("command-unimplemented-waypoint-spawn"));
},
NpcData::Teleporter(_, _) => {
SpawnEntityData::Teleporter(_, _) => {
return Err(Content::localized("command-unimplemented-teleporter-spawn"));
},
NpcData::Data {
inventory,
pos,
stats,
skill_set,
poise,
health,
body,
agent,
alignment,
scale,
loot,
} => {
// Spread about spawned npcs
let vel = Vec3::new(
thread_rng().gen_range(-2.0..3.0),
thread_rng().gen_range(-2.0..3.0),
10.0,
);
SpawnEntityData::Npc(data) => {
let (npc_builder, _pos) = data.to_npc_builder();
let mut entity_builder = server
server
.state
.create_npc(
pos,
comp::Ori::default(),
stats,
skill_set,
health,
poise,
inventory,
body,
)
.with(alignment)
.with(scale)
.with(comp::Vel(vel));
if let Some(agent) = agent {
entity_builder = entity_builder.with(agent);
}
if let Some(drop_items) = loot.to_items() {
entity_builder = entity_builder.with(comp::ItemDrops(drop_items));
}
// Some would say it's a hack, some would say it's incomplete
// simulation. But this is what we do to avoid PvP between npc.
let npc_group = match alignment {
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
Alignment::Wild | Alignment::Passive | Alignment::Owned(_) => None,
};
if let Some(group) = npc_group {
entity_builder = entity_builder.with(group);
}
entity_builder.build();
.ecs()
.read_resource::<EventBus<CreateNpcEvent>>()
.emit_now(CreateNpcEvent {
pos: comp::Pos(pos),
ori: comp::Ori::default(),
npc: npc_builder,
rider: None,
});
},
};
}
@ -1775,13 +1733,7 @@ fn handle_spawn(
owner_entity: target,
pet_entity: new_entity,
});
} else if let Some(group) = match alignment {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
comp::Alignment::Owned(_) => unreachable!(),
} {
} else if let Some(group) = alignment.group() {
insert_or_replace_component(server, new_entity, group, "new entity")?;
}

View File

@ -1,6 +1,6 @@
use crate::{
client::Client, events::player::handle_exit_ingame, persistence::PersistedComponents,
presence::RepositionOnChunkLoad, sys, CharacterUpdater, Server, StateExt,
pet::tame_pet, presence::RepositionOnChunkLoad, sys, CharacterUpdater, Server, StateExt,
};
use common::{
comp::{
@ -198,13 +198,7 @@ pub fn handle_create_npc(server: &mut Server, mut ev: CreateNpcEvent) -> EcsEnti
},
);
}
} else if let Some(group) = match ev.npc.alignment {
Alignment::Wild => None,
Alignment::Passive => None,
Alignment::Enemy => Some(comp::group::ENEMY),
Alignment::Npc | Alignment::Tame => Some(comp::group::NPC),
comp::Alignment::Owned(_) => unreachable!(),
} {
} else if let Some(group) = ev.npc.alignment.group() {
let _ = server.state.ecs().write_storage().insert(new_entity, group);
}
@ -227,6 +221,17 @@ pub fn handle_create_npc(server: &mut Server, mut ev: CreateNpcEvent) -> EcsEnti
.expect("We just created these entities");
}
for (pet, offset) in ev.npc.pets {
let pet_entity = handle_create_npc(server, CreateNpcEvent {
pos: comp::Pos(ev.pos.0 + offset),
ori: Ori::from_unnormalized_vec(offset).unwrap_or_default(),
npc: pet,
rider: None,
});
tame_pet(server.state.ecs(), pet_entity, new_entity);
}
new_entity
}

View File

@ -7,9 +7,11 @@ use crate::{
BuffKind, BuffSource, PhysicsState,
},
error,
events::entity_creation::handle_create_npc,
pet::tame_pet,
rtsim::RtSim,
state_ext::StateExt,
sys::terrain::{NpcData, SAFE_ZONE_RADIUS},
sys::terrain::{NpcData, SpawnEntityData, SAFE_ZONE_RADIUS},
Server, Settings, SpawnPoint,
};
use common::{
@ -28,12 +30,12 @@ use common::{
consts::TELEPORTER_RADIUS,
event::{
AuraEvent, BonkEvent, BuffEvent, ChangeAbilityEvent, ChangeBodyEvent, ChangeStanceEvent,
ChatEvent, ComboChangeEvent, CreateItemDropEvent, CreateObjectEvent, DeleteEvent,
DestroyEvent, EmitExt, Emitter, EnergyChangeEvent, EntityAttackedHookEvent, EventBus,
ExplosionEvent, HealthChangeEvent, KnockbackEvent, LandOnGroundEvent, MakeAdminEvent,
ParryHookEvent, PoiseChangeEvent, RemoveLightEmitterEvent, RespawnEvent, SoundEvent,
StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent, TransformEvent,
UpdateMapMarkerEvent,
ChatEvent, ComboChangeEvent, CreateItemDropEvent, CreateNpcEvent, CreateObjectEvent,
DeleteEvent, DestroyEvent, EmitExt, Emitter, EnergyChangeEvent, EntityAttackedHookEvent,
EventBus, ExplosionEvent, HealthChangeEvent, KnockbackEvent, LandOnGroundEvent,
MakeAdminEvent, ParryHookEvent, PoiseChangeEvent, RemoveLightEmitterEvent, RespawnEvent,
SoundEvent, StartTeleportingEvent, TeleportToEvent, TeleportToPositionEvent,
TransformEvent, UpdateMapMarkerEvent,
},
event_emitters,
generation::EntityInfo,
@ -2141,8 +2143,8 @@ pub fn transform_entity(
.read_storage::<comp::Player>()
.contains(entity);
match NpcData::from_entity_info(entity_info) {
NpcData::Data {
match SpawnEntityData::from_entity_info(entity_info) {
SpawnEntityData::Npc(NpcData {
inventory,
stats,
skill_set,
@ -2154,7 +2156,8 @@ pub fn transform_entity(
loot,
alignment: _,
pos: _,
} => {
pets,
}) => {
fn set_or_remove_component<C: specs::Component>(
server: &mut Server,
entity: EcsEntity,
@ -2247,11 +2250,29 @@ pub fn transform_entity(
set_or_remove_component(server, entity, agent)?;
set_or_remove_component(server, entity, loot.to_items().map(comp::ItemDrops))?;
}
// Spawn pets
let position = server.state.read_component_copied::<comp::Pos>(entity);
if let Some(pos) = position {
for (pet, offset) in pets
.into_iter()
.map(|(pet, offset)| (pet.to_npc_builder().0, offset))
{
let pet_entity = handle_create_npc(server, CreateNpcEvent {
pos: comp::Pos(pos.0 + offset),
ori: comp::Ori::from_unnormalized_vec(offset).unwrap_or_default(),
npc: pet,
rider: None,
});
tame_pet(server.state.ecs(), pet_entity, entity);
}
}
},
NpcData::Waypoint(_) => {
SpawnEntityData::Waypoint(_) => {
return Err(TransformEntityError::UnexpectedNpcWaypoint);
},
NpcData::Teleporter(_, _) => {
SpawnEntityData::Teleporter(_, _) => {
return Err(TransformEntityError::UnexpectedNpcTeleporter);
},
}

View File

@ -105,6 +105,7 @@ pub struct InventoryManipData<'a> {
orientations: ReadStorage<'a, comp::Ori>,
controllers: ReadStorage<'a, comp::Controller>,
agents: ReadStorage<'a, comp::Agent>,
pets: ReadStorage<'a, comp::Pet>,
velocities: ReadStorage<'a, comp::Vel>,
}
@ -520,9 +521,9 @@ impl ServerEvent for InventoryManipEvent {
const MAX_PETS: usize = 3;
let reinsert = if let Some(pos) = data.positions.get(entity)
{
if (&data.alignments, &data.agents)
if (&data.alignments, &data.agents, data.pets.mask())
.join()
.filter(|(alignment, _)| {
.filter(|(alignment, _, _)| {
alignment == &&comp::Alignment::Owned(*uid)
})
.count()

View File

@ -838,7 +838,16 @@ impl Server {
}
}
// Prevent anchor entity chains which are not currently supported
// Prevent anchor entity chains which are not currently supported due to:
// * potential cycles?
// * unloading a chain could occur across an unbounded number of ticks with the
// current implementation.
// * in particular, we want to be able to unload all entities in a
// limited number of ticks when a database error occurs and kicks all
// players (not quiet sure on exact time frame, since it already
// takes a tick after unloading all chunks for entities to despawn?),
// see this thread and the discussion linked from there:
// https://gitlab.com/veloren/veloren/-/merge_requests/2668#note_634913847
let anchors = self.state.ecs().read_storage::<Anchor>();
let anchored_anchor_entities: Vec<Entity> = (
&self.state.ecs().entities(),
@ -849,7 +858,13 @@ impl Server {
Anchor::Entity(anchor_entity) => Some(*anchor_entity),
_ => None,
})
.filter(|anchor_entity| anchors.get(*anchor_entity).is_some())
// We allow Anchor::Entity(_) -> Anchor::Chunk(_) connections, since they can't chain further.
//
// NOTE: The entity with `Anchor::Entity` will unload one tick after the entity with `Anchor::Chunk`.
.filter(|anchor_entity| match anchors.get(*anchor_entity) {
Some(Anchor::Entity(_)) => true,
Some(Anchor::Chunk(_)) | None => false
})
.collect();
drop(anchors);

View File

@ -7,8 +7,8 @@ use common::{
uid::Uid,
};
use common_net::msg::ServerGeneral;
use specs::{Entity, WorldExt};
use tracing::warn;
use specs::{Entity, Join, WorldExt};
use tracing::{error, warn};
/// Restores a pet retrieved from the database on login, assigning it to its
/// owner
@ -23,23 +23,43 @@ pub fn tame_pet(ecs: &specs::World, pet_entity: Entity, owner: Entity) {
fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet: Option<Pet>) {
let uids = ecs.read_storage::<Uid>();
let owner_uid = match uids.get(owner) {
Some(uid) => *uid,
None => return,
let (owner_uid, pet_uid) = match (uids.get(owner), uids.get(pet_entity)) {
(Some(uid_owner), Some(uid_pet)) => (*uid_owner, *uid_pet),
_ => return,
};
let mut alignments = ecs.write_storage::<Alignment>();
let Some(owner_alignment) = alignments.get(owner).copied() else {
error!("Owner of a pet must have an Alignment");
return;
};
if let Some(Alignment::Owned(existing_owner_uid)) =
ecs.read_storage::<Alignment>().get(pet_entity)
{
if let Some(Alignment::Owned(existing_owner_uid)) = alignments.get(pet_entity) {
if *existing_owner_uid != owner_uid {
warn!("Disallowing taming of pet already owned by another entity");
return;
}
}
let _ = ecs
.write_storage()
.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));
if let Alignment::Owned(owner_alignment_uid) = owner_alignment {
if owner_alignment_uid != owner_uid {
error!("Pets cannot be owners of pets");
return;
}
}
if (
&ecs.entities(),
&alignments,
ecs.read_storage::<Pet>().mask(),
)
.join()
.any(|(_, alignment, _)| matches!(alignment, Alignment::Owned(uid) if *uid == pet_uid))
{
error!("Cannot tame entity which owns pets");
return;
}
let _ = alignments.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));
// Anchor the pet to the player to prevent it de-spawning
// when its chunk is unloaded if its owner is still logged
@ -70,6 +90,8 @@ fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet:
let clients = ecs.read_storage::<Client>();
let mut group_manager = ecs.write_resource::<GroupManager>();
let map_markers = ecs.read_storage::<comp::MapMarker>();
drop(alignments);
group_manager.new_pet(
pet_entity,
owner,

View File

@ -1,10 +1,10 @@
#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out
use super::*;
use crate::sys::terrain::NpcData;
use crate::sys::terrain::SpawnEntityData;
use common::{
calendar::Calendar,
comp::{self, Agent, Body, Presence, PresenceKind},
comp::{self, Body, Presence, PresenceKind},
event::{CreateNpcEvent, CreateShipEvent, DeleteEvent, EventBus, NpcBuilder},
generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time, TimeOfDay},
@ -339,41 +339,20 @@ impl<'a> System<'a> for Sys {
Some(&calendar_data),
);
create_npc_emitter.emit(match NpcData::from_entity_info(entity_info) {
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => CreateNpcEvent {
pos,
ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
npc: NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale)
.with_loot(loot)
.with_rtsim(RtSimEntity(id)),
rider: steering,
},
// EntityConfig can't represent Waypoints at all
// as of now, and if someone will try to spawn
// rtsim waypoint it is definitely error.
NpcData::Waypoint(_) => unimplemented!(),
NpcData::Teleporter(_, _) => unimplemented!(),
let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
.into_npc_data_inner()
.expect("Entity loaded from assets cannot be special")
.to_npc_builder();
if let Some(agent) = &mut npc_builder.agent {
agent.rtsim_outbox = Some(Default::default());
}
create_npc_emitter.emit(CreateNpcEvent {
pos,
ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
npc: npc_builder.with_rtsim(RtSimEntity(id)),
rider: steering,
});
},
};
@ -400,37 +379,21 @@ impl<'a> System<'a> for Sys {
Some(&calendar_data),
);
Some(match NpcData::from_entity_info(entity_info) {
NpcData::Data {
pos: _,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale)
.with_loot(loot)
.with_rtsim(RtSimEntity(npc_id)),
let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
.into_npc_data_inner()
// EntityConfig can't represent Waypoints at all
// as of now, and if someone will try to spawn
// rtsim waypoint it is definitely error.
NpcData::Waypoint(_) => unimplemented!(),
NpcData::Teleporter(_, _) => unimplemented!(),
})
.expect("Entity loaded from assets cannot be special")
.to_npc_builder()
.0
.with_rtsim(RtSimEntity(npc_id));
if let Some(agent) = &mut npc_builder.agent {
agent.rtsim_outbox = Some(Default::default());
}
Some(npc_builder)
} else {
error!("Npc is loaded but vehicle is unloaded");
None

View File

@ -2,6 +2,7 @@
use crate::test_world::{IndexOwned, World};
#[cfg(feature = "persistent_world")]
use crate::TerrainPersistence;
use tracing::error;
#[cfg(feature = "worldgen")]
use world::{IndexOwned, World};
@ -39,7 +40,7 @@ use specs::{
storage::GenericReadStorage, Entities, Entity, Join, LendJoin, ParJoin, Read, ReadExpect,
ReadStorage, Write, WriteExpect, WriteStorage,
};
use std::sync::Arc;
use std::{f32::consts::TAU, sync::Arc};
use vek::*;
#[cfg(feature = "persistent_world")]
@ -204,40 +205,22 @@ impl<'a> System<'a> for Sys {
"Chunk spawned entity that wasn't nearby",
);
let data = NpcData::from_entity_info(entity);
let data = SpawnEntityData::from_entity_info(entity);
match data {
NpcData::Waypoint(pos) => {
SpawnEntityData::Waypoint(pos) => {
emitters.emit(CreateWaypointEvent(pos));
},
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => {
SpawnEntityData::Npc(data) => {
let (npc_builder, pos) = data.to_npc_builder();
emitters.emit(CreateNpcEvent {
pos,
ori: comp::Ori::from(Dir::random_2d(&mut rng)),
npc: NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent)
.with_scale(scale)
.with_anchor(comp::Anchor::Chunk(key))
.with_loot(loot),
npc: npc_builder.with_anchor(comp::Anchor::Chunk(key)),
rider: None,
});
},
NpcData::Teleporter(pos, teleporter) => {
SpawnEntityData::Teleporter(pos, teleporter) => {
emitters.emit(CreateTeleporterEvent(pos, teleporter));
},
}
@ -415,30 +398,36 @@ impl<'a> System<'a> for Sys {
}
}
// TODO: better name
#[derive(Debug)]
pub struct NpcData {
pub pos: Pos,
pub stats: comp::Stats,
pub skill_set: comp::SkillSet,
pub health: Option<comp::Health>,
pub poise: comp::Poise,
pub inventory: comp::inventory::Inventory,
pub agent: Option<comp::Agent>,
pub body: comp::Body,
pub alignment: comp::Alignment,
pub scale: comp::Scale,
pub loot: LootSpec<String>,
pub pets: Vec<(NpcData, Vec3<f32>)>,
}
/// Convinient structure to use when you need to create new npc
/// from EntityInfo
// TODO: better name?
// TODO: if this is send around network, optimize the large_enum_variant
#[allow(clippy::large_enum_variant)]
pub enum NpcData {
Data {
pos: Pos,
stats: comp::Stats,
skill_set: comp::SkillSet,
health: Option<comp::Health>,
poise: comp::Poise,
inventory: comp::inventory::Inventory,
agent: Option<comp::Agent>,
body: comp::Body,
alignment: comp::Alignment,
scale: comp::Scale,
loot: LootSpec<String>,
},
#[derive(Debug)]
pub enum SpawnEntityData {
Npc(NpcData),
Waypoint(Vec3<f32>),
Teleporter(Vec3<f32>, PortalData),
}
impl NpcData {
impl SpawnEntityData {
pub fn from_entity_info(entity: EntityInfo) -> Self {
let EntityInfo {
// flags
@ -461,8 +450,7 @@ impl NpcData {
inventory: items,
make_loadout,
trading_information: economy,
// unused
pet: _, // TODO: I had no idea we have this.
pets,
} = entity;
if let Some(special) = special_entity {
@ -562,7 +550,7 @@ impl NpcData {
agent
};
NpcData::Data {
SpawnEntityData::Npc(NpcData {
pos: Pos(pos),
stats,
skill_set,
@ -574,10 +562,73 @@ impl NpcData {
alignment,
scale: comp::Scale(scale),
loot,
pets: {
let pet_count = pets.len() as f32;
pets.into_iter()
.enumerate()
.flat_map(|(i, pet)| {
Some((
SpawnEntityData::from_entity_info(pet)
.into_npc_data_inner()
.inspect_err(|data| {
error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
})
.ok()?,
Vec2::one()
.rotated_z(TAU * (i as f32 / pet_count))
.with_z(0.0)
* ((pet_count * 3.0) / TAU),
))
})
.collect()
},
})
}
pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
match self {
SpawnEntityData::Npc(inner) => Ok(inner),
other => Err(other),
}
}
}
impl NpcData {
pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
let NpcData {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
pets,
} = self;
(
NpcBuilder::new(stats, body, alignment)
.with_skill_set(skill_set)
.with_health(health)
.with_poise(poise)
.with_inventory(inventory)
.with_agent(agent)
.with_scale(scale)
.with_loot(loot)
.with_pets(
pets.into_iter()
.map(|(pet, offset)| (pet.to_npc_builder().0, offset))
.collect::<Vec<_>>(),
),
pos,
)
}
}
pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
// Hardcoded max VD to prevent stupid view distances from creating overflows.
// This must be a value ≤

View File

@ -854,13 +854,6 @@ fn mini_boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<Entit
None,
),
);
entities.resize_with(entities.len() + 4, || {
EntityInfo::at(tile_wcenter.map(|e| e as f32)).with_asset_expect(
"common.entity.dungeon.cultist.hound",
dynamic_rng,
None,
)
});
},
1 => {
entities.resize_with(2, || {