1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use common::{
    comp::{Agent, Alignment, CharacterState, Object, Pos, Teleporting},
    consts::TELEPORTER_RADIUS,
    event::{EventBus, TeleportToPositionEvent},
    outcome::Outcome,
    resources::Time,
    uid::Uid,
    CachedSpatialGrid,
};
use common_ecs::{Origin, Phase, System};
use specs::{Entities, Join, LendJoin, Read, ReadStorage, WriteStorage};
use vek::Vec3;

const MAX_AGGRO_DIST: f32 = 200.; // If an entity further than this is aggroed at a player, the portal will still work
const PET_TELEPORT_RADIUS: f32 = 20.;

#[derive(Default)]
pub struct Sys;

fn in_portal_range(player_pos: Vec3<f32>, portal_pos: Vec3<f32>) -> bool {
    player_pos.distance_squared(portal_pos) <= TELEPORTER_RADIUS.powi(2)
}

impl<'a> System<'a> for Sys {
    type SystemData = (
        Entities<'a>,
        ReadStorage<'a, Pos>,
        ReadStorage<'a, Uid>,
        ReadStorage<'a, Alignment>,
        ReadStorage<'a, Agent>,
        ReadStorage<'a, Object>,
        WriteStorage<'a, Teleporting>,
        ReadStorage<'a, CharacterState>,
        Read<'a, CachedSpatialGrid>,
        Read<'a, Time>,
        Read<'a, EventBus<TeleportToPositionEvent>>,
        Read<'a, EventBus<Outcome>>,
    );

    const NAME: &'static str = "teleporter";
    const ORIGIN: Origin = Origin::Server;
    const PHASE: Phase = Phase::Create;

    fn run(
        _job: &mut common_ecs::Job<Self>,
        (
            entities,
            positions,
            uids,
            alignments,
            agent,
            objects,
            mut teleporting,
            character_states,
            spatial_grid,
            time,
            teleport_to_position_events,
            outcome_bus,
        ): Self::SystemData,
    ) {
        let mut teleport_to_position_emitter = teleport_to_position_events.emitter();
        let mut outcome_emitter = outcome_bus.emitter();
        let check_aggro = |entity, pos: Vec3<f32>| {
            spatial_grid
                .0
                .in_circle_aabr(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
                        })
                    })
                })
        };

        let mut cancel_teleporting = Vec::new();

        for (entity, uid, position, teleporting, character_state) in (
            &entities,
            &uids,
            &positions,
            &teleporting,
            &character_states,
        )
            .join()
        {
            let portal_pos = positions.get(teleporting.portal);
            let Some(Object::Portal {
                target,
                requires_no_aggro,
                ..
            }) = objects.get(teleporting.portal)
            else {
                cancel_teleporting.push(entity);
                continue;
            };

            if portal_pos.map_or(true, |portal_pos| {
                !in_portal_range(position.0, portal_pos.0)
            }) || (*requires_no_aggro && check_aggro(entity, position.0))
                || !matches!(
                    character_state,
                    CharacterState::Idle(_)
                        | CharacterState::Wielding(_)
                        | CharacterState::Sit
                        | CharacterState::Dance
                )
            {
                cancel_teleporting.push(entity);
            } else if teleporting.end_time.0 <= time.0 {
                // Send teleport events for all nearby pets and the owner
                let nearby = spatial_grid
                    .0
                    .in_circle_aabr(position.0.xy(), PET_TELEPORT_RADIUS)
                    .filter_map(|entity| {
                        (&entities, &positions, &alignments)
                            .lend_join()
                            .get(entity, &entities)
                    })
                    .filter_map(|(nearby_entity, entity_position, alignment)| {
                        (matches!(alignment, Alignment::Owned(entity_uid) if entity_uid == uid)
                            && entity_position.0.distance_squared(position.0)
                                <= PET_TELEPORT_RADIUS.powi(2)
                            // Allow for non-players to teleport too
                            || entity == nearby_entity)
                            .then_some(nearby_entity)
                    });

                for entity in nearby {
                    cancel_teleporting.push(entity);
                    teleport_to_position_emitter.emit(TeleportToPositionEvent {
                        entity,
                        position: *target,
                    });
                    outcome_emitter.emit(Outcome::TeleportedByPortal { pos: *target });
                }
            }
        }

        for entity in cancel_teleporting {
            let _ = teleporting.remove(entity);
        }
    }
}