mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Made NPCs give directions to nearby towns, fixed player death propagation
This commit is contained in:
parent
d26a711469
commit
daacadaedb
@ -250,6 +250,10 @@ npc-speech-merchant_sell_directed =
|
|||||||
.a0 = You there! Are you in need of a new thingamabob?
|
.a0 = You there! Are you in need of a new thingamabob?
|
||||||
.a1 = Are you hungry? I'm sure I've got some cheese you can buy.
|
.a1 = Are you hungry? I'm sure I've got some cheese you can buy.
|
||||||
.a2 = You look like you could do with some new armour!
|
.a2 = You look like you could do with some new armour!
|
||||||
|
npc-speech-tell_site =
|
||||||
|
.a0 = Have you visited { $site }? It's just { $dir } of here!
|
||||||
|
.a1 = You should visit { $site } some time.
|
||||||
|
.a2 = If you travel { $dir }, you can get to { $site }.
|
||||||
npc-speech-witness_murder =
|
npc-speech-witness_murder =
|
||||||
.a0 = Murderer!
|
.a0 = Murderer!
|
||||||
.a1 = How could you do this?
|
.a1 = How could you do this?
|
||||||
@ -258,3 +262,11 @@ npc-speech-witness_death =
|
|||||||
.a0 = No!
|
.a0 = No!
|
||||||
.a1 = This is terrible!
|
.a1 = This is terrible!
|
||||||
.a2 = Oh my goodness!
|
.a2 = Oh my goodness!
|
||||||
|
npc-speech-dir_north = north
|
||||||
|
npc-speech-dir_north_east = north-east
|
||||||
|
npc-speech-dir_east = east
|
||||||
|
npc-speech-dir_south_east = south-east
|
||||||
|
npc-speech-dir_south = south
|
||||||
|
npc-speech-dir_south_west = south-west
|
||||||
|
npc-speech-dir_west = west
|
||||||
|
npc-speech-dir_north_west = north-west
|
||||||
|
@ -231,6 +231,10 @@ pub enum LocalizationArg {
|
|||||||
Nat(u64),
|
Nat(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Content> for LocalizationArg {
|
||||||
|
fn from(content: Content) -> Self { Self::Content(content) }
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
|
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
|
||||||
// discourage it)
|
// discourage it)
|
||||||
impl From<String> for LocalizationArg {
|
impl From<String> for LocalizationArg {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use super::Content;
|
||||||
use vek::Vec2;
|
use vek::Vec2;
|
||||||
// TODO: Move this to common/src/, it's not a component
|
// TODO: Move this to common/src/, it's not a component
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ impl Direction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: localization
|
// TODO: Remove this in favour of `Direction::localize_npc`?
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Direction::North => "North",
|
Direction::North => "North",
|
||||||
@ -52,6 +53,19 @@ impl Direction {
|
|||||||
Direction::Northwest => "Northwest",
|
Direction::Northwest => "Northwest",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn localize_npc(&self) -> Content {
|
||||||
|
Content::localized(match self {
|
||||||
|
Direction::North => "npc-speech-dir_north",
|
||||||
|
Direction::Northeast => "npc-speech-dir_north_east",
|
||||||
|
Direction::East => "npc-speech-dir_east",
|
||||||
|
Direction::Southeast => "npc-speech-dir_south_east",
|
||||||
|
Direction::South => "npc-speech-dir_south",
|
||||||
|
Direction::Southwest => "npc-speech-dir_south_west",
|
||||||
|
Direction::West => "npc-speech-dir_west",
|
||||||
|
Direction::Northwest => "npc-speech-dir_north_west",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arbitrarily named Distances
|
/// Arbitrarily named Distances
|
||||||
|
@ -40,6 +40,13 @@ pub struct Site {
|
|||||||
// Note: there's currently no guarantee that site populations are non-intersecting
|
// Note: there's currently no guarantee that site populations are non-intersecting
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub population: HashSet<NpcId>,
|
pub population: HashSet<NpcId>,
|
||||||
|
|
||||||
|
/// A list of the nearby sites where each elements is both further and
|
||||||
|
/// larger (currently based on number of plots) than the next.
|
||||||
|
/// Effectively, this is a list of nearby sites that might be deemed
|
||||||
|
/// 'important' to the current one
|
||||||
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
|
pub nearby_sites_by_size: Vec<SiteId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Site {
|
impl Site {
|
||||||
|
@ -53,6 +53,7 @@ impl Site {
|
|||||||
}),
|
}),
|
||||||
population: Default::default(),
|
population: Default::default(),
|
||||||
known_reports: Default::default(),
|
known_reports: Default::default(),
|
||||||
|
nearby_sites_by_size: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
astar::{Astar, PathResult},
|
astar::{Astar, PathResult},
|
||||||
comp::Content,
|
comp::{compass::Direction, Content},
|
||||||
path::Path,
|
path::Path,
|
||||||
rtsim::{ChunkResource, Profession, SiteId},
|
rtsim::{Actor, ChunkResource, Profession, SiteId},
|
||||||
spiral::Spiral2d,
|
spiral::Spiral2d,
|
||||||
store::Id,
|
store::Id,
|
||||||
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
|
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
|
||||||
@ -489,7 +489,21 @@ fn socialize() -> impl Action {
|
|||||||
.choose(&mut ctx.rng)
|
.choose(&mut ctx.rng)
|
||||||
{
|
{
|
||||||
return Either::Left(
|
return Either::Left(
|
||||||
just(move |ctx| ctx.controller.say(other, ctx.npc.personality.get_generic_comment(&mut ctx.rng)))
|
just(move |ctx| ctx.controller.say(other, if ctx.rng.gen_bool(0.3)
|
||||||
|
&& let Some(current_site) = ctx.npc.current_site
|
||||||
|
&& let Some(current_site) = ctx.state.data().sites.get(current_site)
|
||||||
|
&& let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng)
|
||||||
|
&& let Some(mention_site) = ctx.state.data().sites.get(*mention_site)
|
||||||
|
&& let Some(mention_site_name) = mention_site.world_site
|
||||||
|
.map(|ws| ctx.index.sites.get(ws).name().to_string())
|
||||||
|
{
|
||||||
|
Content::localized_with_args("npc-speech-tell_site", [
|
||||||
|
("site", Content::Plain(mention_site_name.clone())),
|
||||||
|
("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
ctx.npc.personality.get_generic_comment(&mut ctx.rng)
|
||||||
|
}))
|
||||||
// After greeting the actor, wait for a while
|
// After greeting the actor, wait for a while
|
||||||
.then(idle().repeat().stop_if(timeout(4.0)))
|
.then(idle().repeat().stop_if(timeout(4.0)))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@ -713,7 +727,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
|
|||||||
.map(|_| ()),
|
.map(|_| ()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.5) {
|
} else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) {
|
||||||
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
|
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
|
||||||
return casual(
|
return casual(
|
||||||
travel_to_point(plaza_wpos, 0.4)
|
travel_to_point(plaza_wpos, 0.4)
|
||||||
@ -918,8 +932,13 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
|
|||||||
// TODO: Sentiment should be positive if we didn't like actor that died
|
// TODO: Sentiment should be positive if we didn't like actor that died
|
||||||
// TODO: Don't report self
|
// TODO: Don't report self
|
||||||
let phrase = if let Some(killer) = killer {
|
let phrase = if let Some(killer) = killer {
|
||||||
// TODO: Don't hard-code sentiment change
|
// TODO: For now, we don't make sentiment changes if the killer was an
|
||||||
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
|
// NPC because NPCs can't hurt one-another.
|
||||||
|
// This should be changed in the future.
|
||||||
|
if !matches!(killer, Actor::Npc(_)) {
|
||||||
|
// TODO: Don't hard-code sentiment change
|
||||||
|
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
|
||||||
|
}
|
||||||
"npc-speech-witness_murder"
|
"npc-speech-witness_murder"
|
||||||
} else {
|
} else {
|
||||||
"npc-speech-witness_death"
|
"npc-speech-witness_death"
|
||||||
|
@ -45,107 +45,110 @@ fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
|
|||||||
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
||||||
let data = &mut *ctx.state.data_mut();
|
let data = &mut *ctx.state.data_mut();
|
||||||
|
|
||||||
if let Actor::Npc(npc_id) = ctx.event.actor
|
if let Actor::Npc(npc_id) = ctx.event.actor {
|
||||||
&& let Some(npc) = data.npcs.get(npc_id)
|
if let Some(npc) = data.npcs.get(npc_id) {
|
||||||
{
|
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
||||||
let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
|
|
||||||
|
|
||||||
// Respawn dead NPCs
|
// Respawn dead NPCs
|
||||||
let details = match npc.body {
|
let details = match npc.body {
|
||||||
Body::Humanoid(_) => {
|
Body::Humanoid(_) => {
|
||||||
if let Some((site_id, site)) = data
|
if let Some((site_id, site)) = data
|
||||||
.sites
|
.sites
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, site)| {
|
.filter(|(id, site)| {
|
||||||
Some(*id) != npc.home
|
Some(*id) != npc.home
|
||||||
&& (npc.faction.is_none() || site.faction == npc.faction)
|
&& (npc.faction.is_none() || site.faction == npc.faction)
|
||||||
&& site.world_site.map_or(false, |s| {
|
&& site.world_site.map_or(false, |s| {
|
||||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)
|
matches!(
|
||||||
| SiteKind::CliffTown(_)
|
ctx.index.sites.get(s).kind,
|
||||||
| SiteKind::SavannahPit(_)
|
SiteKind::Refactor(_)
|
||||||
| SiteKind::DesertCity(_))
|
| SiteKind::CliffTown(_)
|
||||||
})
|
| SiteKind::SavannahPit(_)
|
||||||
})
|
| SiteKind::DesertCity(_)
|
||||||
.min_by_key(|(_, site)| site.population.len())
|
)
|
||||||
{
|
})
|
||||||
let rand_wpos = |rng: &mut ChaChaRng| {
|
})
|
||||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
.min_by_key(|(_, site)| site.population.len())
|
||||||
wpos2d
|
{
|
||||||
.map(|e| e as f32 + 0.5)
|
let rand_wpos = |rng: &mut ChaChaRng| {
|
||||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||||
};
|
wpos2d
|
||||||
let random_humanoid = |rng: &mut ChaChaRng| {
|
.map(|e| e as f32 + 0.5)
|
||||||
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
|
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||||
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
|
};
|
||||||
};
|
let random_humanoid = |rng: &mut ChaChaRng| {
|
||||||
let npc_id = data.spawn_npc(
|
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
|
||||||
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
|
Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
|
||||||
.with_personality(Personality::random(&mut rng))
|
};
|
||||||
.with_home(site_id)
|
let npc_id = data.spawn_npc(
|
||||||
.with_faction(npc.faction)
|
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
|
||||||
.with_profession(npc.profession.clone()),
|
.with_personality(Personality::random(&mut rng))
|
||||||
);
|
.with_home(site_id)
|
||||||
Some((npc_id, site_id))
|
.with_faction(npc.faction)
|
||||||
} else {
|
.with_profession(npc.profession.clone()),
|
||||||
warn!("No site found for respawning humanoid");
|
);
|
||||||
|
Some((npc_id, site_id))
|
||||||
|
} else {
|
||||||
|
warn!("No site found for respawning humanoid");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Body::BirdLarge(_) => {
|
||||||
|
if let Some((site_id, site)) = data
|
||||||
|
.sites
|
||||||
|
.iter()
|
||||||
|
.filter(|(id, site)| {
|
||||||
|
Some(*id) != npc.home
|
||||||
|
&& site.world_site.map_or(false, |s| {
|
||||||
|
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.min_by_key(|(_, site)| site.population.len())
|
||||||
|
{
|
||||||
|
let rand_wpos = |rng: &mut ChaChaRng| {
|
||||||
|
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||||
|
wpos2d
|
||||||
|
.map(|e| e as f32 + 0.5)
|
||||||
|
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||||
|
};
|
||||||
|
let species = [
|
||||||
|
comp::body::bird_large::Species::Phoenix,
|
||||||
|
comp::body::bird_large::Species::Cockatrice,
|
||||||
|
comp::body::bird_large::Species::Roc,
|
||||||
|
]
|
||||||
|
.choose(&mut rng)
|
||||||
|
.unwrap();
|
||||||
|
let npc_id = data.npcs.create_npc(
|
||||||
|
Npc::new(
|
||||||
|
rng.gen(),
|
||||||
|
rand_wpos(&mut rng),
|
||||||
|
Body::BirdLarge(comp::body::bird_large::Body::random_with(
|
||||||
|
&mut rng, species,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_home(site_id),
|
||||||
|
);
|
||||||
|
Some((npc_id, site_id))
|
||||||
|
} else {
|
||||||
|
warn!("No site found for respawning bird");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body => {
|
||||||
|
error!("Tried to respawn rtsim NPC with invalid body: {:?}", body);
|
||||||
None
|
None
|
||||||
}
|
},
|
||||||
},
|
};
|
||||||
Body::BirdLarge(_) => {
|
|
||||||
if let Some((site_id, site)) = data
|
|
||||||
.sites
|
|
||||||
.iter()
|
|
||||||
.filter(|(id, site)| {
|
|
||||||
Some(*id) != npc.home
|
|
||||||
&& site.world_site.map_or(false, |s| {
|
|
||||||
matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.min_by_key(|(_, site)| site.population.len())
|
|
||||||
{
|
|
||||||
let rand_wpos = |rng: &mut ChaChaRng| {
|
|
||||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
|
||||||
wpos2d
|
|
||||||
.map(|e| e as f32 + 0.5)
|
|
||||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
|
||||||
};
|
|
||||||
let species = [
|
|
||||||
comp::body::bird_large::Species::Phoenix,
|
|
||||||
comp::body::bird_large::Species::Cockatrice,
|
|
||||||
comp::body::bird_large::Species::Roc,
|
|
||||||
]
|
|
||||||
.choose(&mut rng)
|
|
||||||
.unwrap();
|
|
||||||
let npc_id = data.npcs.create_npc(
|
|
||||||
Npc::new(
|
|
||||||
rng.gen(),
|
|
||||||
rand_wpos(&mut rng),
|
|
||||||
Body::BirdLarge(comp::body::bird_large::Body::random_with(
|
|
||||||
&mut rng, species,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_home(site_id),
|
|
||||||
);
|
|
||||||
Some((npc_id, site_id))
|
|
||||||
} else {
|
|
||||||
warn!("No site found for respawning bird");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
body => {
|
|
||||||
error!("Tried to respawn rtsim NPC with invalid body: {:?}", body);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the NPC to their home site
|
// Add the NPC to their home site
|
||||||
if let Some((npc_id, home_site)) = details {
|
if let Some((npc_id, home_site)) = details {
|
||||||
if let Some(home) = data.sites.get_mut(home_site) {
|
if let Some(home) = data.sites.get_mut(home_site) {
|
||||||
home.population.insert(npc_id);
|
home.population.insert(npc_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error!("Trying to respawn non-existent NPC");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
error!("Trying to respawn non-existent NPC");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,43 @@ fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
|
|||||||
home.population.insert(npc_id);
|
home.population.insert(npc_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the list of nearest sites by size for each site
|
||||||
|
let sites_iter = data.sites.iter().filter_map(|(site_id, site)| {
|
||||||
|
let site2 = site
|
||||||
|
.world_site
|
||||||
|
.and_then(|ws| ctx.index.sites.get(ws).site2())?;
|
||||||
|
Some((site_id, site, site2))
|
||||||
|
});
|
||||||
|
let nearest_by_size = sites_iter.clone()
|
||||||
|
.map(|(site_id, site, site2)| {
|
||||||
|
let mut other_sites = sites_iter.clone()
|
||||||
|
// Only include sites in the list if they're not the current one and they're more populus
|
||||||
|
.filter(|(other_id, _, other_site2)| *other_id != site_id && other_site2.plots().len() > site2.plots().len())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
other_sites.sort_by_key(|(_, other, _)| other.wpos.distance_squared(site.wpos) as i64);
|
||||||
|
let mut max_size = 0;
|
||||||
|
// Remove sites that aren't in increasing order of size (Stalin sort?!)
|
||||||
|
other_sites.retain(|(_, _, other_site2)| {
|
||||||
|
if other_site2.plots().len() > max_size {
|
||||||
|
max_size = other_site2.plots().len();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let nearest_by_size = other_sites
|
||||||
|
.into_iter()
|
||||||
|
.map(|(site_id, _, _)| site_id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(site_id, nearest_by_size)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (site_id, nearest_by_size) in nearest_by_size {
|
||||||
|
if let Some(site) = data.sites.get_mut(site_id) {
|
||||||
|
site.nearby_sites_by_size = nearest_by_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
|
||||||
|
@ -513,12 +513,11 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
|||||||
inventory.damage_items(&ability_map, &msm);
|
inventory.damage_items(&ability_map, &msm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_delete {
|
if let Some(actor) = state.entity_as_actor(entity) {
|
||||||
if let Some(actor) = state.entity_as_actor(entity) {
|
state
|
||||||
state
|
.ecs()
|
||||||
.ecs()
|
.write_resource::<rtsim::RtSim>()
|
||||||
.write_resource::<rtsim::RtSim>()
|
.hook_rtsim_actor_death(
|
||||||
.hook_rtsim_actor_death(
|
|
||||||
&state.ecs().read_resource::<Arc<world::World>>(),
|
&state.ecs().read_resource::<Arc<world::World>>(),
|
||||||
state
|
state
|
||||||
.ecs()
|
.ecs()
|
||||||
@ -540,8 +539,9 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
|||||||
)
|
)
|
||||||
.and_then(|killer| state.entity_as_actor(killer)),
|
.and_then(|killer| state.entity_as_actor(killer)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if should_delete {
|
||||||
if let Err(e) = state.delete_entity_recorded(entity) {
|
if let Err(e) = state.delete_entity_recorded(entity) {
|
||||||
error!(?e, ?entity, "Failed to delete destroyed entity");
|
error!(?e, ?entity, "Failed to delete destroyed entity");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user