mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
added portals
This commit is contained in:
parent
730f96536a
commit
0f2db68498
BIN
assets/voxygen/voxel/object/portal.vox
(Stored with Git LFS)
Normal file
BIN
assets/voxygen/voxel/object/portal.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -979,4 +979,14 @@
|
|||||||
central: ("armor.empty"),
|
central: ("armor.empty"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
Portal: (
|
||||||
|
bone0: (
|
||||||
|
offset: (-11.0, -1.5, 0.0),
|
||||||
|
central: ("object.portal"),
|
||||||
|
),
|
||||||
|
bone1: (
|
||||||
|
offset: (0.0, 0.0, 0.0),
|
||||||
|
central: ("armor.empty"),
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
@ -102,6 +102,8 @@ synced_components!(reexport_comps);
|
|||||||
// === NetSync implementations ===
|
// === NetSync implementations ===
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
|
use common::comp::Teleporter;
|
||||||
|
|
||||||
use crate::sync::{NetSync, SyncFrom};
|
use crate::sync::{NetSync, SyncFrom};
|
||||||
|
|
||||||
impl NetSync for Body {
|
impl NetSync for Body {
|
||||||
@ -234,6 +236,10 @@ impl NetSync for Stance {
|
|||||||
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NetSync for Teleporter {
|
||||||
|
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||||
|
}
|
||||||
|
|
||||||
// These are synced only from the client's own entity.
|
// These are synced only from the client's own entity.
|
||||||
|
|
||||||
impl NetSync for Admin {
|
impl NetSync for Admin {
|
||||||
|
@ -310,6 +310,7 @@ pub enum ServerChatCommand {
|
|||||||
Object,
|
Object,
|
||||||
PermitBuild,
|
PermitBuild,
|
||||||
Players,
|
Players,
|
||||||
|
Portal,
|
||||||
Region,
|
Region,
|
||||||
ReloadChunks,
|
ReloadChunks,
|
||||||
RemoveLights,
|
RemoveLights,
|
||||||
@ -618,6 +619,16 @@ impl ServerChatCommand {
|
|||||||
Some(Admin),
|
Some(Admin),
|
||||||
),
|
),
|
||||||
ServerChatCommand::Players => cmd(vec![], "Lists players currently online", None),
|
ServerChatCommand::Players => cmd(vec![], "Lists players currently online", None),
|
||||||
|
ServerChatCommand::Portal => cmd(
|
||||||
|
vec![
|
||||||
|
Float("x", 0., Required),
|
||||||
|
Float("y", 0., Required),
|
||||||
|
Float("z", 0., Required),
|
||||||
|
Boolean("requires_no_aggro", "true".to_string(), Optional),
|
||||||
|
],
|
||||||
|
"Spawns a portal",
|
||||||
|
Some(Moderator),
|
||||||
|
),
|
||||||
ServerChatCommand::ReloadChunks => cmd(
|
ServerChatCommand::ReloadChunks => cmd(
|
||||||
vec![],
|
vec![],
|
||||||
"Reloads all chunks loaded on the server",
|
"Reloads all chunks loaded on the server",
|
||||||
@ -890,6 +901,7 @@ impl ServerChatCommand {
|
|||||||
ServerChatCommand::Object => "object",
|
ServerChatCommand::Object => "object",
|
||||||
ServerChatCommand::PermitBuild => "permit_build",
|
ServerChatCommand::PermitBuild => "permit_build",
|
||||||
ServerChatCommand::Players => "players",
|
ServerChatCommand::Players => "players",
|
||||||
|
ServerChatCommand::Portal => "portal",
|
||||||
ServerChatCommand::Region => "region",
|
ServerChatCommand::Region => "region",
|
||||||
ServerChatCommand::ReloadChunks => "reload_chunks",
|
ServerChatCommand::ReloadChunks => "reload_chunks",
|
||||||
ServerChatCommand::RemoveLights => "remove_lights",
|
ServerChatCommand::RemoveLights => "remove_lights",
|
||||||
|
@ -110,6 +110,7 @@ make_case_elim!(
|
|||||||
Mine = 95,
|
Mine = 95,
|
||||||
LightningBolt = 96,
|
LightningBolt = 96,
|
||||||
SpearIcicle = 97,
|
SpearIcicle = 97,
|
||||||
|
Portal = 98,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ impl Body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ALL_OBJECTS: [Body; 98] = [
|
pub const ALL_OBJECTS: [Body; 99] = [
|
||||||
Body::Arrow,
|
Body::Arrow,
|
||||||
Body::Bomb,
|
Body::Bomb,
|
||||||
Body::Scarecrow,
|
Body::Scarecrow,
|
||||||
@ -219,6 +220,7 @@ pub const ALL_OBJECTS: [Body; 98] = [
|
|||||||
Body::Mine,
|
Body::Mine,
|
||||||
Body::LightningBolt,
|
Body::LightningBolt,
|
||||||
Body::SpearIcicle,
|
Body::SpearIcicle,
|
||||||
|
Body::Portal,
|
||||||
];
|
];
|
||||||
|
|
||||||
impl From<Body> for super::Body {
|
impl From<Body> for super::Body {
|
||||||
@ -326,6 +328,7 @@ impl Body {
|
|||||||
Body::Mine => "mine",
|
Body::Mine => "mine",
|
||||||
Body::LightningBolt => "lightning_bolt",
|
Body::LightningBolt => "lightning_bolt",
|
||||||
Body::SpearIcicle => "spear_icicle",
|
Body::SpearIcicle => "spear_icicle",
|
||||||
|
Body::Portal => "portal",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,6 +453,7 @@ impl Body {
|
|||||||
Body::AdletTrap => 10.0,
|
Body::AdletTrap => 10.0,
|
||||||
Body::Mine => 100.0,
|
Body::Mine => 100.0,
|
||||||
Body::LightningBolt | Body::SpearIcicle => 20000.0,
|
Body::LightningBolt | Body::SpearIcicle => 20000.0,
|
||||||
|
Body::Portal => 10., // I dont know really
|
||||||
};
|
};
|
||||||
|
|
||||||
Mass(m)
|
Mass(m)
|
||||||
|
@ -3,6 +3,7 @@ use crate::{resources::Time, uid::Uid};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specs::{Component, DerefFlaggedStorage};
|
use specs::{Component, DerefFlaggedStorage};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use vek::Vec3;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Object {
|
pub enum Object {
|
||||||
@ -22,3 +23,13 @@ pub enum Object {
|
|||||||
impl Component for Object {
|
impl Component for Object {
|
||||||
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
|
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Teleporter {
|
||||||
|
pub target: Vec3<f32>,
|
||||||
|
pub requires_no_aggro: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Teleporter {
|
||||||
|
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
|
||||||
|
}
|
||||||
|
@ -86,7 +86,7 @@ pub use self::{
|
|||||||
location::{MapMarker, MapMarkerChange, MapMarkerUpdate, Waypoint, WaypointArea},
|
location::{MapMarker, MapMarkerChange, MapMarkerUpdate, Waypoint, WaypointArea},
|
||||||
loot_owner::LootOwner,
|
loot_owner::LootOwner,
|
||||||
melee::{Melee, MeleeConstructor, MeleeConstructorKind},
|
melee::{Melee, MeleeConstructor, MeleeConstructorKind},
|
||||||
misc::Object,
|
misc::{Object, Teleporter},
|
||||||
ori::Ori,
|
ori::Ori,
|
||||||
pet::Pet,
|
pet::Pet,
|
||||||
phys::{
|
phys::{
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
agent::Sound,
|
agent::Sound,
|
||||||
dialogue::Subject,
|
dialogue::Subject,
|
||||||
invite::{InviteKind, InviteResponse},
|
invite::{InviteKind, InviteResponse},
|
||||||
DisconnectReason, Ori, Pos,
|
DisconnectReason, Ori, Pos, Teleporter,
|
||||||
},
|
},
|
||||||
lottery::LootSpec,
|
lottery::LootSpec,
|
||||||
mounting::VolumePos,
|
mounting::VolumePos,
|
||||||
@ -239,6 +239,7 @@ pub enum ServerEvent {
|
|||||||
driver: Option<NpcBuilder>,
|
driver: Option<NpcBuilder>,
|
||||||
},
|
},
|
||||||
CreateWaypoint(Vec3<f32>),
|
CreateWaypoint(Vec3<f32>),
|
||||||
|
CreateTeleporter(Vec3<f32>, Teleporter),
|
||||||
ClientDisconnect(EcsEntity, DisconnectReason),
|
ClientDisconnect(EcsEntity, DisconnectReason),
|
||||||
ClientDisconnectWithoutPersistence(EcsEntity),
|
ClientDisconnectWithoutPersistence(EcsEntity),
|
||||||
Command(EcsEntity, String, Vec<String>),
|
Command(EcsEntity, String, Vec<String>),
|
||||||
|
@ -3,7 +3,7 @@ use crate::{
|
|||||||
comp::{
|
comp::{
|
||||||
self, agent, humanoid,
|
self, agent, humanoid,
|
||||||
inventory::loadout_builder::{LoadoutBuilder, LoadoutSpec},
|
inventory::loadout_builder::{LoadoutBuilder, LoadoutSpec},
|
||||||
Alignment, Body, Item,
|
Alignment, Body, Item, Teleporter,
|
||||||
},
|
},
|
||||||
lottery::LootSpec,
|
lottery::LootSpec,
|
||||||
npc::{self, NPC_NAMES},
|
npc::{self, NPC_NAMES},
|
||||||
@ -163,10 +163,15 @@ pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
|
|||||||
Ok(configs.ids().map(|id| id.to_string()).collect())
|
Ok(configs.ids().map(|id| id.to_string()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SpecialEntity {
|
||||||
|
Waypoint,
|
||||||
|
Teleporter(Teleporter),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EntityInfo {
|
pub struct EntityInfo {
|
||||||
pub pos: Vec3<f32>,
|
pub pos: Vec3<f32>,
|
||||||
pub is_waypoint: bool, // Edge case, overrides everything else
|
|
||||||
pub alignment: Alignment,
|
pub alignment: Alignment,
|
||||||
/// Parameterises agent behaviour
|
/// Parameterises agent behaviour
|
||||||
pub has_agency: bool,
|
pub has_agency: bool,
|
||||||
@ -194,13 +199,15 @@ pub struct EntityInfo {
|
|||||||
// we can't use DHashMap, do we want to move that into common?
|
// we can't use DHashMap, do we want to move that into common?
|
||||||
pub trading_information: Option<SiteInformation>,
|
pub trading_information: Option<SiteInformation>,
|
||||||
//Option<hashbrown::HashMap<crate::trade::Good, (f32, f32)>>, /* price and available amount */
|
//Option<hashbrown::HashMap<crate::trade::Good, (f32, f32)>>, /* price and available amount */
|
||||||
|
|
||||||
|
// Edge cases, override everything else
|
||||||
|
pub special_entity: Option<SpecialEntity>, // Campfire
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntityInfo {
|
impl EntityInfo {
|
||||||
pub fn at(pos: Vec3<f32>) -> Self {
|
pub fn at(pos: Vec3<f32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pos,
|
pos,
|
||||||
is_waypoint: false,
|
|
||||||
alignment: Alignment::Wild,
|
alignment: Alignment::Wild,
|
||||||
|
|
||||||
has_agency: true,
|
has_agency: true,
|
||||||
@ -219,6 +226,7 @@ impl EntityInfo {
|
|||||||
skillset_asset: None,
|
skillset_asset: None,
|
||||||
pet: None,
|
pet: None,
|
||||||
trading_information: None,
|
trading_information: None,
|
||||||
|
special_entity: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,8 +385,8 @@ impl EntityInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_waypoint(mut self) -> Self {
|
pub fn into_special(mut self, special: SpecialEntity) -> Self {
|
||||||
self.is_waypoint = true;
|
self.special_entity = Some(special);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +219,7 @@ impl State {
|
|||||||
ecs.register::<comp::LootOwner>();
|
ecs.register::<comp::LootOwner>();
|
||||||
ecs.register::<comp::Admin>();
|
ecs.register::<comp::Admin>();
|
||||||
ecs.register::<comp::Stance>();
|
ecs.register::<comp::Stance>();
|
||||||
|
ecs.register::<comp::Teleporter>();
|
||||||
|
|
||||||
// Register components send from clients -> server
|
// Register components send from clients -> server
|
||||||
ecs.register::<comp::Controller>();
|
ecs.register::<comp::Controller>();
|
||||||
|
@ -167,6 +167,7 @@ fn do_command(
|
|||||||
ServerChatCommand::Object => handle_object,
|
ServerChatCommand::Object => handle_object,
|
||||||
ServerChatCommand::PermitBuild => handle_permit_build,
|
ServerChatCommand::PermitBuild => handle_permit_build,
|
||||||
ServerChatCommand::Players => handle_players,
|
ServerChatCommand::Players => handle_players,
|
||||||
|
ServerChatCommand::Portal => handle_spawn_portal,
|
||||||
ServerChatCommand::Region => handle_region,
|
ServerChatCommand::Region => handle_region,
|
||||||
ServerChatCommand::ReloadChunks => handle_reload_chunks,
|
ServerChatCommand::ReloadChunks => handle_reload_chunks,
|
||||||
ServerChatCommand::RemoveLights => handle_remove_lights,
|
ServerChatCommand::RemoveLights => handle_remove_lights,
|
||||||
@ -694,6 +695,9 @@ fn handle_make_npc(
|
|||||||
NpcData::Waypoint(_) => {
|
NpcData::Waypoint(_) => {
|
||||||
return Err("Waypoint spawning is not implemented".to_owned());
|
return Err("Waypoint spawning is not implemented".to_owned());
|
||||||
},
|
},
|
||||||
|
NpcData::Teleporter(_, _) => {
|
||||||
|
return Err("Teleporter spawning is not implemented".to_owned());
|
||||||
|
},
|
||||||
NpcData::Data {
|
NpcData::Data {
|
||||||
inventory,
|
inventory,
|
||||||
pos,
|
pos,
|
||||||
@ -2038,6 +2042,37 @@ fn handle_players(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_spawn_portal(
|
||||||
|
server: &mut Server,
|
||||||
|
client: EcsEntity,
|
||||||
|
target: EcsEntity,
|
||||||
|
args: Vec<String>,
|
||||||
|
_action: &ServerChatCommand,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
let pos = position(server, target, "target")?;
|
||||||
|
|
||||||
|
if let (Some(x), Some(y), Some(z), requires_no_aggro) =
|
||||||
|
parse_cmd_args!(args, f32, f32, f32, bool)
|
||||||
|
{
|
||||||
|
let requires_no_aggro = requires_no_aggro.unwrap_or(false);
|
||||||
|
server
|
||||||
|
.state
|
||||||
|
.create_teleporter(pos, comp::Teleporter {
|
||||||
|
target: Vec3::new(x, y, z),
|
||||||
|
requires_no_aggro,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
server.notify_client(
|
||||||
|
client,
|
||||||
|
ServerGeneral::server_msg(ChatType::CommandInfo, "Spawned portal"),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Invalid arguments".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_build(
|
fn handle_build(
|
||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
client: EcsEntity,
|
client: EcsEntity,
|
||||||
|
@ -11,7 +11,7 @@ use common::{
|
|||||||
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
|
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
|
||||||
ship::figuredata::VOXEL_COLLIDER_MANIFEST,
|
ship::figuredata::VOXEL_COLLIDER_MANIFEST,
|
||||||
shockwave, Alignment, BehaviorCapability, Body, ItemDrops, LightEmitter, Object, Ori, Pos,
|
shockwave, Alignment, BehaviorCapability, Body, ItemDrops, LightEmitter, Object, Ori, Pos,
|
||||||
Projectile, TradingBehavior, Vel, WaypointArea,
|
Projectile, Teleporter, TradingBehavior, Vel, WaypointArea,
|
||||||
},
|
},
|
||||||
event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
|
event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
|
||||||
mounting::{Mounting, Volume, VolumeMounting, VolumePos},
|
mounting::{Mounting, Volume, VolumeMounting, VolumePos},
|
||||||
@ -418,3 +418,10 @@ pub fn handle_create_waypoint(server: &mut Server, pos: Vec3<f32>) {
|
|||||||
]))
|
]))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_create_teleporter(server: &mut Server, pos: Vec3<f32>, teleporter: Teleporter) {
|
||||||
|
server
|
||||||
|
.state
|
||||||
|
.create_teleporter(comp::Pos(pos), teleporter)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
events::interaction::{handle_mount_volume, handle_tame_pet},
|
events::{
|
||||||
|
entity_creation::handle_create_teleporter,
|
||||||
|
interaction::{handle_mount_volume, handle_tame_pet},
|
||||||
|
},
|
||||||
persistence::PersistedComponents,
|
persistence::PersistedComponents,
|
||||||
state_ext::StateExt,
|
state_ext::StateExt,
|
||||||
Server,
|
Server,
|
||||||
@ -209,6 +212,9 @@ impl Server {
|
|||||||
driver,
|
driver,
|
||||||
} => handle_create_ship(self, pos, ori, ship, rtsim_entity, driver, Vec::new()),
|
} => handle_create_ship(self, pos, ori, ship, rtsim_entity, driver, Vec::new()),
|
||||||
ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
|
ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
|
||||||
|
ServerEvent::CreateTeleporter(pos, teleporter) => {
|
||||||
|
handle_create_teleporter(self, pos, teleporter)
|
||||||
|
},
|
||||||
ServerEvent::ClientDisconnect(entity, reason) => {
|
ServerEvent::ClientDisconnect(entity, reason) => {
|
||||||
frontend_events.push(handle_client_disconnect(self, entity, reason, false))
|
frontend_events.push(handle_client_disconnect(self, entity, reason, false))
|
||||||
},
|
},
|
||||||
|
@ -338,6 +338,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
// as of now, and if someone will try to spawn
|
// as of now, and if someone will try to spawn
|
||||||
// rtsim waypoint it is definitely error.
|
// rtsim waypoint it is definitely error.
|
||||||
NpcData::Waypoint(_) => unimplemented!(),
|
NpcData::Waypoint(_) => unimplemented!(),
|
||||||
|
NpcData::Teleporter(_, _) => unimplemented!(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
error!("Npc is loaded but vehicle is unloaded");
|
error!("Npc is loaded but vehicle is unloaded");
|
||||||
@ -401,6 +402,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
// as of now, and if someone will try to spawn
|
// as of now, and if someone will try to spawn
|
||||||
// rtsim waypoint it is definitely error.
|
// rtsim waypoint it is definitely error.
|
||||||
NpcData::Waypoint(_) => unimplemented!(),
|
NpcData::Waypoint(_) => unimplemented!(),
|
||||||
|
NpcData::Teleporter(_, _) => unimplemented!(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ use common::{
|
|||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
item::{ItemKind, MaterialStatManifest},
|
item::{ItemKind, MaterialStatManifest},
|
||||||
|
object,
|
||||||
skills::{GeneralSkill, Skill},
|
skills::{GeneralSkill, Skill},
|
||||||
ChatType, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence, PresenceKind,
|
ChatType, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence, PresenceKind,
|
||||||
},
|
},
|
||||||
@ -114,6 +115,14 @@ pub trait StateExt {
|
|||||||
world: &std::sync::Arc<world::World>,
|
world: &std::sync::Arc<world::World>,
|
||||||
index: &world::IndexOwned,
|
index: &world::IndexOwned,
|
||||||
) -> EcsEntityBuilder;
|
) -> EcsEntityBuilder;
|
||||||
|
/// Creates a teleporter entity, which allows players to teleport to the
|
||||||
|
/// `target` position. You might want to require the teleporting entity
|
||||||
|
/// to not have agro for teleporting.
|
||||||
|
fn create_teleporter(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
teleporter: comp::Teleporter,
|
||||||
|
) -> EcsEntityBuilder;
|
||||||
/// Insert common/default components for a new character joining the server
|
/// Insert common/default components for a new character joining the server
|
||||||
fn initialize_character_data(
|
fn initialize_character_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -607,6 +616,16 @@ impl StateExt for State {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_teleporter(
|
||||||
|
&mut self,
|
||||||
|
pos: comp::Pos,
|
||||||
|
teleporter: comp::Teleporter,
|
||||||
|
) -> EcsEntityBuilder {
|
||||||
|
self.create_object(pos, object::Body::Portal)
|
||||||
|
.with(comp::Immovable)
|
||||||
|
.with(teleporter)
|
||||||
|
}
|
||||||
|
|
||||||
fn initialize_character_data(
|
fn initialize_character_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: EcsEntity,
|
entity: EcsEntity,
|
||||||
|
@ -11,6 +11,7 @@ pub mod persistence;
|
|||||||
pub mod pets;
|
pub mod pets;
|
||||||
pub mod sentinel;
|
pub mod sentinel;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
|
pub mod teleporter;
|
||||||
pub mod terrain;
|
pub mod terrain;
|
||||||
pub mod terrain_sync;
|
pub mod terrain_sync;
|
||||||
pub mod waypoint;
|
pub mod waypoint;
|
||||||
@ -32,6 +33,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
|||||||
dispatch::<agent::Sys>(dispatch_builder, &[]);
|
dispatch::<agent::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<terrain::Sys>(dispatch_builder, &[&msg::terrain::Sys::sys_name()]);
|
dispatch::<terrain::Sys>(dispatch_builder, &[&msg::terrain::Sys::sys_name()]);
|
||||||
dispatch::<waypoint::Sys>(dispatch_builder, &[]);
|
dispatch::<waypoint::Sys>(dispatch_builder, &[]);
|
||||||
|
dispatch::<teleporter::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<invite_timeout::Sys>(dispatch_builder, &[]);
|
dispatch::<invite_timeout::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<persistence::Sys>(dispatch_builder, &[]);
|
dispatch::<persistence::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<object::Sys>(dispatch_builder, &[]);
|
dispatch::<object::Sys>(dispatch_builder, &[]);
|
||||||
|
87
server/src/sys/teleporter.rs
Normal file
87
server/src/sys/teleporter.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use common::{
|
||||||
|
comp::{Agent, ForceUpdate, Player, Pos, Teleporter},
|
||||||
|
CachedSpatialGrid,
|
||||||
|
};
|
||||||
|
use common_ecs::{Origin, Phase, System};
|
||||||
|
use specs::{Entities, Join, Read, ReadStorage, WriteStorage};
|
||||||
|
|
||||||
|
const TELEPORT_RADIUS: f32 = 1.;
|
||||||
|
const MAX_AGGRO_DIST: f32 = 200.; // If an entity further than this is aggroed at a player, the portal will still work
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Sys;
|
||||||
|
|
||||||
|
impl<'a> System<'a> for Sys {
|
||||||
|
type SystemData = (
|
||||||
|
Entities<'a>,
|
||||||
|
WriteStorage<'a, Pos>,
|
||||||
|
ReadStorage<'a, Player>,
|
||||||
|
ReadStorage<'a, Teleporter>,
|
||||||
|
ReadStorage<'a, Agent>,
|
||||||
|
WriteStorage<'a, ForceUpdate>,
|
||||||
|
Read<'a, CachedSpatialGrid>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const NAME: &'static str = "teleporter";
|
||||||
|
const ORIGIN: Origin = Origin::Server;
|
||||||
|
const PHASE: Phase = Phase::Create;
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
_job: &mut common_ecs::Job<Self>,
|
||||||
|
(
|
||||||
|
entities,
|
||||||
|
mut positions,
|
||||||
|
players,
|
||||||
|
teleporters,
|
||||||
|
agent,
|
||||||
|
mut forced_update,
|
||||||
|
spatial_grid,
|
||||||
|
): Self::SystemData,
|
||||||
|
) {
|
||||||
|
let mut attempt_teleport = vec![];
|
||||||
|
let mut player_data = (&entities, &positions, &players).join();
|
||||||
|
|
||||||
|
for (_entity, teleporter_pos, teleporter) in (&entities, &positions, &teleporters).join() {
|
||||||
|
let nearby_entities = spatial_grid
|
||||||
|
.0
|
||||||
|
.in_circle_aabr(teleporter_pos.0.xy(), TELEPORT_RADIUS);
|
||||||
|
|
||||||
|
for (entity, position, _) in nearby_entities.filter_map(|entity| {
|
||||||
|
player_data
|
||||||
|
.get(entity, &entities)
|
||||||
|
.filter(|(_, player_pos, _)| {
|
||||||
|
player_pos.0.distance_squared(teleporter_pos.0) <= (TELEPORT_RADIUS).powi(2)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
// TODO: Check for aggro
|
||||||
|
attempt_teleport.push((entity, position.0, teleporter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, origin_pos, teleporter) in attempt_teleport {
|
||||||
|
if teleporter.requires_no_aggro {
|
||||||
|
// FIXME: How does this go with performance?
|
||||||
|
let is_aggroed = spatial_grid
|
||||||
|
.0
|
||||||
|
.in_circle_aabr(origin_pos.xy(), MAX_AGGRO_DIST)
|
||||||
|
.any(|agent_entity| {
|
||||||
|
agent.get(agent_entity).map_or(false, |agent| {
|
||||||
|
agent.target.map_or(false, |agent_target| {
|
||||||
|
agent_target.target == entity && agent_target.aggro_on
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_aggroed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positions
|
||||||
|
.get_mut(entity)
|
||||||
|
.map(|position| position.0 = teleporter.target);
|
||||||
|
forced_update
|
||||||
|
.get_mut(entity)
|
||||||
|
.map(|forced_update| forced_update.update());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,10 +13,10 @@ use common::{
|
|||||||
calendar::Calendar,
|
calendar::Calendar,
|
||||||
comp::{
|
comp::{
|
||||||
self, agent, biped_small, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate,
|
self, agent, biped_small, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate,
|
||||||
Pos, Presence, Waypoint,
|
Pos, Presence, Teleporter, Waypoint,
|
||||||
},
|
},
|
||||||
event::{EventBus, NpcBuilder, ServerEvent},
|
event::{EventBus, NpcBuilder, ServerEvent},
|
||||||
generation::EntityInfo,
|
generation::{EntityInfo, SpecialEntity},
|
||||||
lottery::LootSpec,
|
lottery::LootSpec,
|
||||||
resources::{Time, TimeOfDay},
|
resources::{Time, TimeOfDay},
|
||||||
slowjob::SlowJobPool,
|
slowjob::SlowJobPool,
|
||||||
@ -222,6 +222,9 @@ impl<'a> System<'a> for Sys {
|
|||||||
.with_loot(loot),
|
.with_loot(loot),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
NpcData::Teleporter(pos, teleporter) => {
|
||||||
|
server_emitter.emit(ServerEvent::CreateTeleporter(pos, teleporter));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -415,13 +418,14 @@ pub enum NpcData {
|
|||||||
loot: LootSpec<String>,
|
loot: LootSpec<String>,
|
||||||
},
|
},
|
||||||
Waypoint(Vec3<f32>),
|
Waypoint(Vec3<f32>),
|
||||||
|
Teleporter(Vec3<f32>, Teleporter),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NpcData {
|
impl NpcData {
|
||||||
pub fn from_entity_info(entity: EntityInfo) -> Self {
|
pub fn from_entity_info(entity: EntityInfo) -> Self {
|
||||||
let EntityInfo {
|
let EntityInfo {
|
||||||
// flags
|
// flags
|
||||||
is_waypoint,
|
special_entity,
|
||||||
has_agency,
|
has_agency,
|
||||||
agent_mark,
|
agent_mark,
|
||||||
alignment,
|
alignment,
|
||||||
@ -444,8 +448,11 @@ impl NpcData {
|
|||||||
pet: _, // TODO: I had no idea we have this.
|
pet: _, // TODO: I had no idea we have this.
|
||||||
} = entity;
|
} = entity;
|
||||||
|
|
||||||
if is_waypoint {
|
if let Some(special) = special_entity {
|
||||||
return Self::Waypoint(pos);
|
return match special {
|
||||||
|
SpecialEntity::Waypoint => Self::Waypoint(pos),
|
||||||
|
SpecialEntity::Teleporter(teleporter) => Self::Teleporter(pos, teleporter),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.unwrap_or_else(|| "Unnamed".to_string());
|
let name = name.unwrap_or_else(|| "Unnamed".to_string());
|
||||||
|
@ -46,7 +46,7 @@ use crate::{
|
|||||||
use common::{
|
use common::{
|
||||||
assets,
|
assets,
|
||||||
calendar::Calendar,
|
calendar::Calendar,
|
||||||
generation::{ChunkSupplement, EntityInfo},
|
generation::{ChunkSupplement, EntityInfo, SpecialEntity},
|
||||||
lod,
|
lod,
|
||||||
resources::TimeOfDay,
|
resources::TimeOfDay,
|
||||||
rtsim::ChunkResource,
|
rtsim::ChunkResource,
|
||||||
@ -487,7 +487,8 @@ impl World {
|
|||||||
.fold(SpawnRules::default(), |a, b| a.combine(b))
|
.fold(SpawnRules::default(), |a, b| a.combine(b))
|
||||||
.waypoints
|
.waypoints
|
||||||
{
|
{
|
||||||
supplement.add_entity(EntityInfo::at(waypoint_pos).into_waypoint());
|
supplement
|
||||||
|
.add_entity(EntityInfo::at(waypoint_pos).into_special(SpecialEntity::Waypoint));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
generation::EntityInfo,
|
generation::{EntityInfo, SpecialEntity},
|
||||||
terrain::{BlockKind, SpriteKind},
|
terrain::{BlockKind, SpriteKind},
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -752,7 +752,10 @@ impl Structure for CliffTower {
|
|||||||
// spawn campfire next to some clifftowers
|
// spawn campfire next to some clifftowers
|
||||||
if self.campfire {
|
if self.campfire {
|
||||||
let campfire_pos = (plot_center - 20).with_z(self.alt + 18);
|
let campfire_pos = (plot_center - 20).with_z(self.alt + 18);
|
||||||
painter.spawn(EntityInfo::at(campfire_pos.map(|e| e as f32)).into_waypoint());
|
painter.spawn(
|
||||||
|
EntityInfo::at(campfire_pos.map(|e| e as f32))
|
||||||
|
.into_special(SpecialEntity::Waypoint),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use crate::{
|
|||||||
util::{RandomField, Sampler, CARDINALS},
|
util::{RandomField, Sampler, CARDINALS},
|
||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::terrain::{BlockKind, SpriteKind};
|
use common::{terrain::{BlockKind, SpriteKind}, generation::SpecialEntity};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
@ -326,7 +326,7 @@ impl Structure for CoastalWorkshop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
painter.spawn(
|
painter.spawn(
|
||||||
EntityInfo::at((center - 2).with_z(base - 2).map(|e| e as f32 + 0.5)).into_waypoint(),
|
EntityInfo::at((center - 2).with_z(base - 2).map(|e| e as f32 + 0.5)).into_special(SpecialEntity::Waypoint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
generation::EntityInfo,
|
generation::{EntityInfo, SpecialEntity},
|
||||||
terrain::{Block, BlockKind, SpriteKind, Structure as PrefabStructure, StructuresGroup},
|
terrain::{Block, BlockKind, SpriteKind, Structure as PrefabStructure, StructuresGroup},
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -2432,7 +2432,10 @@ impl Structure for DesertCityMultiPlot {
|
|||||||
// spawn campfire in some multiplots that are not markethall
|
// spawn campfire in some multiplots that are not markethall
|
||||||
let campfire_pos = (center).with_z(base + 1);
|
let campfire_pos = (center).with_z(base + 1);
|
||||||
if self.campfire {
|
if self.campfire {
|
||||||
painter.spawn(EntityInfo::at(campfire_pos.map(|e| e as f32)).into_waypoint())
|
painter.spawn(
|
||||||
|
EntityInfo::at(campfire_pos.map(|e| e as f32))
|
||||||
|
.into_special(SpecialEntity::Waypoint),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ use crate::{
|
|||||||
use common::{
|
use common::{
|
||||||
assets::{self, AssetExt, AssetHandle},
|
assets::{self, AssetExt, AssetHandle},
|
||||||
astar::Astar,
|
astar::Astar,
|
||||||
generation::{ChunkSupplement, EntityInfo},
|
comp::Teleporter,
|
||||||
|
generation::{ChunkSupplement, EntityInfo, SpecialEntity},
|
||||||
store::{Id, Store},
|
store::{Id, Store},
|
||||||
terrain::{
|
terrain::{
|
||||||
BiomeKind, Block, BlockKind, SpriteKind, Structure, StructuresGroup, TerrainChunkSize,
|
BiomeKind, Block, BlockKind, SpriteKind, Structure, StructuresGroup, TerrainChunkSize,
|
||||||
@ -146,7 +147,7 @@ impl Dungeon {
|
|||||||
if area.contains_point(pos - self.origin) {
|
if area.contains_point(pos - self.origin) {
|
||||||
supplement.add_entity(
|
supplement.add_entity(
|
||||||
EntityInfo::at(Vec3::new(pos.x as f32, pos.y as f32, self.alt as f32) + 5.0)
|
EntityInfo::at(Vec3::new(pos.x as f32, pos.y as f32, self.alt as f32) + 5.0)
|
||||||
.into_waypoint(),
|
.into_special(SpecialEntity::Waypoint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,6 +616,14 @@ impl Floor {
|
|||||||
for y in area.min.y..area.max.y {
|
for y in area.min.y..area.max.y {
|
||||||
let tile_pos = Vec2::new(x, y).map(|e| e.div_euclid(TILE_SIZE)) - self.tile_offset;
|
let tile_pos = Vec2::new(x, y).map(|e| e.div_euclid(TILE_SIZE)) - self.tile_offset;
|
||||||
let wpos2d = origin.xy() + Vec2::new(x, y);
|
let wpos2d = origin.xy() + Vec2::new(x, y);
|
||||||
|
let is_boss_tile = self.tiles.get(tile_pos).filter(|_| self.final_level);
|
||||||
|
|
||||||
|
let tile_wcenter = origin
|
||||||
|
+ Vec3::from(
|
||||||
|
Vec2::new(x, y)
|
||||||
|
.map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(Tile::Room(room)) = self.tiles.get(tile_pos) {
|
if let Some(Tile::Room(room)) = self.tiles.get(tile_pos) {
|
||||||
let room = &self.rooms[*room];
|
let room = &self.rooms[*room];
|
||||||
|
|
||||||
@ -648,6 +657,25 @@ impl Floor {
|
|||||||
),
|
),
|
||||||
RoomKind::Peaceful | RoomKind::LavaPlatforming => {},
|
RoomKind::Peaceful | RoomKind::LavaPlatforming => {},
|
||||||
}
|
}
|
||||||
|
} else if let Some(Tile::UpStair(_, _)) = is_boss_tile && tile_wcenter.xy() == wpos2d {
|
||||||
|
// Create one teleporter at the top of the "stairs" and one at the botton
|
||||||
|
let bottom_pos = tile_wcenter.map(|v| v as f32) ;
|
||||||
|
let top_pos =
|
||||||
|
(tile_wcenter + Vec3::unit_z() * self.total_depth()).map(|v| v as f32);
|
||||||
|
|
||||||
|
// Move both a bit to the side to prevent teleportation loop, ideally we'd have the portals at another location
|
||||||
|
supplement.add_entity(EntityInfo::at(top_pos).into_special(
|
||||||
|
SpecialEntity::Teleporter(Teleporter {
|
||||||
|
target: bottom_pos+ Vec3::unit_x() * 5.,
|
||||||
|
requires_no_aggro: false,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
supplement.add_entity(EntityInfo::at(bottom_pos).into_special(
|
||||||
|
SpecialEntity::Teleporter(Teleporter {
|
||||||
|
target: top_pos+ Vec3::unit_x() * 5.,
|
||||||
|
requires_no_aggro: true,
|
||||||
|
}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ ChatGPT: Improving Code Quality, Speeding up finding Syntax Errors
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{site2::gen::PrimitiveTransform, Land};
|
use crate::{site2::gen::PrimitiveTransform, Land};
|
||||||
use common::terrain::Structure as PrefabStructure;
|
use common::{generation::SpecialEntity, terrain::Structure as PrefabStructure};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
@ -166,7 +166,8 @@ impl Structure for DwarvenMine {
|
|||||||
let entrance_offset: Vec3<f32> = (20.0, -20.0, 45.0).into();
|
let entrance_offset: Vec3<f32> = (20.0, -20.0, 45.0).into();
|
||||||
// Spawn waypoint
|
// Spawn waypoint
|
||||||
let waypoint_pos = (entrance_pos + Vec3::new(1, -1, 4)).map(|x| x as f32) + entrance_offset;
|
let waypoint_pos = (entrance_pos + Vec3::new(1, -1, 4)).map(|x| x as f32) + entrance_offset;
|
||||||
painter.spawn(EntityInfo::at(waypoint_pos.map(|e| e)).into_waypoint());
|
painter
|
||||||
|
.spawn(EntityInfo::at(waypoint_pos.map(|e| e)).into_special(SpecialEntity::Waypoint));
|
||||||
|
|
||||||
let miner_pos: Vec<(Vec3<f32>, &str)> = vec![
|
let miner_pos: Vec<(Vec3<f32>, &str)> = vec![
|
||||||
// Entrance
|
// Entrance
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
generation::EntityInfo,
|
generation::{EntityInfo, SpecialEntity},
|
||||||
terrain::{BlockKind, SpriteKind},
|
terrain::{BlockKind, SpriteKind},
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -942,7 +942,9 @@ impl Structure for SavannahPit {
|
|||||||
// campfire
|
// campfire
|
||||||
let campfire_pos = (center - 10).with_z(base);
|
let campfire_pos = (center - 10).with_z(base);
|
||||||
|
|
||||||
painter.spawn(EntityInfo::at(campfire_pos.map(|e| e as f32)).into_waypoint());
|
painter.spawn(
|
||||||
|
EntityInfo::at(campfire_pos.map(|e| e as f32)).into_special(SpecialEntity::Waypoint),
|
||||||
|
);
|
||||||
|
|
||||||
// underground market hall
|
// underground market hall
|
||||||
let stairs_pos = Vec2::new(center.x, center.y - (2 * length) + 1);
|
let stairs_pos = Vec2::new(center.x, center.y - (2 * length) + 1);
|
||||||
|
@ -3,7 +3,10 @@ use crate::{
|
|||||||
util::{RandomField, Sampler, CARDINALS, DIAGONALS},
|
util::{RandomField, Sampler, CARDINALS, DIAGONALS},
|
||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::terrain::{BlockKind, SpriteKind};
|
use common::{
|
||||||
|
generation::SpecialEntity,
|
||||||
|
terrain::{BlockKind, SpriteKind},
|
||||||
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::{f32::consts::TAU, sync::Arc};
|
use std::{f32::consts::TAU, sync::Arc};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
@ -302,7 +305,8 @@ impl Structure for SavannahWorkshop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
painter.spawn(
|
painter.spawn(
|
||||||
EntityInfo::at((center).with_z(base - 2).map(|e| e as f32 + 0.5)).into_waypoint(),
|
EntityInfo::at((center).with_z(base - 2).map(|e| e as f32 + 0.5))
|
||||||
|
.into_special(SpecialEntity::Waypoint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
Land,
|
Land,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
generation::EntityInfo,
|
generation::{EntityInfo, SpecialEntity},
|
||||||
terrain::{Block, BlockKind, SpriteKind},
|
terrain::{Block, BlockKind, SpriteKind},
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -145,7 +145,7 @@ impl Structure for Workshop {
|
|||||||
|
|
||||||
painter.spawn(
|
painter.spawn(
|
||||||
EntityInfo::at(self.bounds.center().with_z(base).map(|e| e as f32 + 0.5))
|
EntityInfo::at(self.bounds.center().with_z(base).map(|e| e as f32 + 0.5))
|
||||||
.into_waypoint(),
|
.into_special(SpecialEntity::Waypoint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user