veloren_server/sys/
teleporter.rs

1use common::{
2    CachedSpatialGrid,
3    comp::{Agent, Alignment, CharacterState, Object, Pos, Teleporting},
4    consts::TELEPORTER_RADIUS,
5    event::{EventBus, TeleportToPositionEvent},
6    outcome::Outcome,
7    resources::Time,
8    uid::Uid,
9};
10use common_ecs::{Origin, Phase, System};
11use specs::{Entities, Join, LendJoin, Read, ReadStorage, WriteStorage};
12use vek::Vec3;
13
14const MAX_AGGRO_DIST: f32 = 200.; // If an entity further than this is aggroed at a player, the portal will still work
15const PET_TELEPORT_RADIUS: f32 = 20.;
16
17#[derive(Default)]
18pub struct Sys;
19
20fn in_portal_range(player_pos: Vec3<f32>, portal_pos: Vec3<f32>) -> bool {
21    player_pos.distance_squared(portal_pos) <= TELEPORTER_RADIUS.powi(2)
22}
23
24impl<'a> System<'a> for Sys {
25    type SystemData = (
26        Entities<'a>,
27        ReadStorage<'a, Pos>,
28        ReadStorage<'a, Uid>,
29        ReadStorage<'a, Alignment>,
30        ReadStorage<'a, Agent>,
31        ReadStorage<'a, Object>,
32        WriteStorage<'a, Teleporting>,
33        ReadStorage<'a, CharacterState>,
34        Read<'a, CachedSpatialGrid>,
35        Read<'a, Time>,
36        Read<'a, EventBus<TeleportToPositionEvent>>,
37        Read<'a, EventBus<Outcome>>,
38    );
39
40    const NAME: &'static str = "teleporter";
41    const ORIGIN: Origin = Origin::Server;
42    const PHASE: Phase = Phase::Create;
43
44    fn run(
45        _job: &mut common_ecs::Job<Self>,
46        (
47            entities,
48            positions,
49            uids,
50            alignments,
51            agent,
52            objects,
53            mut teleporting,
54            character_states,
55            spatial_grid,
56            time,
57            teleport_to_position_events,
58            outcome_bus,
59        ): Self::SystemData,
60    ) {
61        let mut teleport_to_position_emitter = teleport_to_position_events.emitter();
62        let mut outcome_emitter = outcome_bus.emitter();
63        let check_aggro = |entity, pos: Vec3<f32>| {
64            spatial_grid
65                .0
66                .in_circle_aabr(pos.xy(), MAX_AGGRO_DIST)
67                .any(|agent_entity| {
68                    agent.get(agent_entity).is_some_and(|agent| {
69                        agent.target.is_some_and(|agent_target| {
70                            agent_target.target == entity && agent_target.aggro_on
71                        })
72                    })
73                })
74        };
75
76        let mut cancel_teleporting = Vec::new();
77
78        for (entity, uid, position, teleporting, character_state) in (
79            &entities,
80            &uids,
81            &positions,
82            &teleporting,
83            &character_states,
84        )
85            .join()
86        {
87            let portal_pos = positions.get(teleporting.portal);
88            let Some(Object::Portal {
89                target,
90                requires_no_aggro,
91                ..
92            }) = objects.get(teleporting.portal)
93            else {
94                cancel_teleporting.push(entity);
95                continue;
96            };
97
98            if portal_pos.is_none_or(|portal_pos| !in_portal_range(position.0, portal_pos.0))
99                || (*requires_no_aggro && check_aggro(entity, position.0))
100                || !matches!(
101                    character_state,
102                    CharacterState::Idle(_)
103                        | CharacterState::Wielding(_)
104                        | CharacterState::Sit
105                        | CharacterState::Dance
106                )
107            {
108                cancel_teleporting.push(entity);
109            } else if teleporting.end_time.0 <= time.0 {
110                // Send teleport events for all nearby pets and the owner
111                let nearby = spatial_grid
112                    .0
113                    .in_circle_aabr(position.0.xy(), PET_TELEPORT_RADIUS)
114                    .filter_map(|entity| {
115                        (&entities, &positions, &alignments)
116                            .lend_join()
117                            .get(entity, &entities)
118                    })
119                    .filter_map(|(nearby_entity, entity_position, alignment)| {
120                        (matches!(alignment, Alignment::Owned(entity_uid) if entity_uid == uid)
121                            && entity_position.0.distance_squared(position.0)
122                                <= PET_TELEPORT_RADIUS.powi(2)
123                            // Allow for non-players to teleport too
124                            || entity == nearby_entity)
125                            .then_some(nearby_entity)
126                    });
127
128                for entity in nearby {
129                    cancel_teleporting.push(entity);
130                    teleport_to_position_emitter.emit(TeleportToPositionEvent {
131                        entity,
132                        position: *target,
133                    });
134                    outcome_emitter.emit(Outcome::TeleportedByPortal { pos: *target });
135                }
136            }
137        }
138
139        for entity in cancel_teleporting {
140            let _ = teleporting.remove(entity);
141        }
142    }
143}