allow pet definitions in entity configurations

This commit is contained in:
crabman 2024-02-28 14:30:07 +00:00
parent 285791817d
commit 40fed95760
No known key found for this signature in database
10 changed files with 168 additions and 102 deletions

View File

@ -7,5 +7,7 @@
inventory: (
loadout: FromBody,
),
// Added for testing
pets: ["common.entity.wild.peaceful.crab"],
meta: [],
)
)

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>,
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>) -> Self {
self.pets = pets;
self
}
}
pub struct ClientConnectedEvent {

View File

@ -136,6 +136,10 @@ 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)
#[serde(default)]
pub pets: Vec<String>,
/// Meta Info for optional fields
/// Possible fields:
/// SkillSetAsset(String) with asset_specifier for skillset
@ -204,8 +208,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 +239,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 +282,7 @@ impl EntityInfo {
inventory,
loot,
meta,
pets,
} = config;
match body {
@ -316,6 +320,18 @@ impl EntityInfo {
// NOTE: set loadout after body, as it's used with default equipement
self = self.with_inventory(inventory, config_asset, loadout_rng, time);
self.pets = pets
.into_iter()
.map(|pet_config| {
EntityInfo::at(self.pos).with_entity_config(
EntityConfig::load_expect_cloned(&pet_config),
config_asset,
loadout_rng,
time,
)
})
.collect();
// Prefer the new configuration, if possible
let AgentConfig {
has_agency,
@ -675,6 +691,18 @@ mod tests {
}
}
#[cfg(test)]
fn validate_pets(pets: Vec<String>, config_asset: &str) {
for pet in pets.into_iter().map(|pet_asset| {
EntityConfig::load_cloned(&pet_asset)
.expect(&format!("Pet asset path invalid, in {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 +717,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 +727,7 @@ mod tests {
// misc
validate_loot(loot, &config_asset);
validate_meta(meta, &config_asset);
validate_pets(pets, &config_asset);
}
}
}

View File

@ -744,6 +744,8 @@ fn handle_make_npc(
agent,
alignment,
scale,
// TODO
pets: _,
loot,
} => {
// Spread about spawned npcs

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::{
@ -227,6 +227,17 @@ pub fn handle_create_npc(server: &mut Server, mut ev: CreateNpcEvent) -> EcsEnti
.expect("We just created these entities");
}
for pet in ev.npc.pets {
let pet_entity = handle_create_npc(server, CreateNpcEvent {
pos: ev.pos,
ori: Ori::default(),
npc: pet,
rider: None,
});
tame_pet(server.state.ecs(), pet_entity, new_entity);
}
new_entity
}

View File

@ -7,6 +7,8 @@ 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},
@ -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,
@ -2154,6 +2156,7 @@ pub fn transform_entity(
loot,
alignment: _,
pos: _,
pets,
} => {
fn set_or_remove_component<C: specs::Component>(
server: &mut Server,
@ -2247,6 +2250,26 @@ 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
.ecs()
.read_storage::<comp::Pos>()
.get(entity)
.copied();
if let Some(pos) = position {
for (pet, _pos) in pets.into_iter().filter_map(|pet| pet.to_npc_builder().ok()) {
let pet_entity = handle_create_npc(server, CreateNpcEvent {
pos,
ori: comp::Ori::default(),
npc: pet,
rider: None,
});
tame_pet(server.state.ecs(), pet_entity, entity);
}
}
},
NpcData::Waypoint(_) => {
return Err(TransformEntityError::UnexpectedNpcWaypoint);

View File

@ -37,6 +37,14 @@ fn tame_pet_internal(ecs: &specs::World, pet_entity: Entity, owner: Entity, pet:
}
}
if let Some(Alignment::Owned(owner_alignment_uid)) = ecs.read_storage::<Alignment>().get(owner)
{
if *owner_alignment_uid != owner_uid {
warn!("Pets cannot be owners of pets");
return;
}
}
let _ = ecs
.write_storage()
.insert(pet_entity, common::comp::Alignment::Owned(owner_uid));

View File

@ -4,7 +4,7 @@ use super::*;
use crate::sys::terrain::NpcData;
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,19 @@ 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) = NpcData::from_entity_info(entity_info)
.to_npc_builder()
.expect("NpcData must be valid");
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 +378,17 @@ 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)),
// 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 = NpcData::from_entity_info(entity_info)
.to_npc_builder()
.expect("NpcData must be valid")
.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

@ -209,31 +209,16 @@ impl<'a> System<'a> for Sys {
NpcData::Waypoint(pos) => {
emitters.emit(CreateWaypointEvent(pos));
},
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => {
data @ NpcData::Data { .. } => {
// TODO: Investigate anchor chains with pets
let (npc_builder, pos) = data
.to_npc_builder()
.expect("This NpcData is known to be valid");
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,
});
},
@ -420,6 +405,7 @@ impl<'a> System<'a> for Sys {
// TODO: better name?
// TODO: if this is send around network, optimize the large_enum_variant
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum NpcData {
Data {
pos: Pos,
@ -433,6 +419,7 @@ pub enum NpcData {
alignment: comp::Alignment,
scale: comp::Scale,
loot: LootSpec<String>,
pets: Vec<NpcData>,
},
Waypoint(Vec3<f32>),
Teleporter(Vec3<f32>, PortalData),
@ -462,7 +449,7 @@ impl NpcData {
make_loadout,
trading_information: economy,
// unused
pet: _, // TODO: I had no idea we have this.
pets,
} = entity;
if let Some(special) = special_entity {
@ -574,6 +561,44 @@ impl NpcData {
alignment,
scale: comp::Scale(scale),
loot,
pets: pets.into_iter().map(NpcData::from_entity_info).collect(),
}
}
#[allow(clippy::result_large_err)]
pub fn to_npc_builder(self) -> Result<(NpcBuilder, comp::Pos), Self> {
match self {
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
pets,
} => Ok((
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_data| pet_data.to_npc_builder().map(|(npc, _pos)| npc))
.collect::<Result<Vec<_>, _>>()?,
),
pos,
)),
NpcData::Waypoint(_) => Err(self),
NpcData::Teleporter(_, _) => Err(self),
}
}
}