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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
use std::{cmp::Ordering, collections::BinaryHeap};

use bevy::prelude::*;
use de_behaviour::{ChaseSet, ChaseTarget, ChaseTargetEvent};
use de_core::{gamestate::GameState, objects::ObjectTypeComponent};
use de_index::SpatialQuery;
use de_objects::{LaserCannon, SolidObjects};
use parry3d::query::Ray;

use crate::laser::LaserFireEvent;
use crate::{sightline::LineOfSight, AttackingSet};

/// Multiple of cannon range. The attacking entities will try to stay as close
/// or further from attacked targets.
const MIN_CHASE_DISTNACE: f32 = 0.4;
/// Multiple of cannon range. The attacking entities will try to stay as close
/// or closer from attacked targets.
const MAX_CHASE_DISTNACE: f32 = 0.9;

pub(crate) struct AttackPlugin;

impl Plugin for AttackPlugin {
    fn build(&self, app: &mut App) {
        app.add_event::<AttackEvent>()
            .add_systems(
                PreUpdate,
                (
                    attack
                        .in_set(AttackingSet::Attack)
                        .before(ChaseSet::ChaseTargetEvent),
                    update_positions.after(AttackingSet::Attack),
                )
                    .run_if(in_state(GameState::Playing)),
            )
            .add_systems(
                Update,
                (
                    charge.in_set(AttackingSet::Charge),
                    aim_and_fire
                        .after(AttackingSet::Charge)
                        .before(AttackingSet::Fire),
                )
                    .run_if(in_state(GameState::Playing)),
            );
    }
}

#[derive(Event)]
pub struct AttackEvent {
    attacker: Entity,
    enemy: Entity,
}

impl AttackEvent {
    /// # Arguments
    ///
    /// * `attacker` - an attacking entity. It must be a locally simulated
    ///   entity.
    ///
    /// * `enemy` - an attacked entity. It may be non-locally simulated entity.
    pub fn new(attacker: Entity, enemy: Entity) -> Self {
        Self { attacker, enemy }
    }

    fn attacker(&self) -> Entity {
        self.attacker
    }

    fn enemy(&self) -> Entity {
        self.enemy
    }
}

#[derive(Component)]
struct Attacking {
    enemy: Entity,
    muzzle: Vec3,
    target: Option<Vec3>,
}

impl Attacking {
    fn new(enemy: Entity) -> Self {
        Self {
            enemy,
            muzzle: Vec3::ZERO,
            target: None,
        }
    }

    fn distance(&self) -> Option<f32> {
        self.target.map(|target| target.distance(self.muzzle))
    }

    fn ray(&self) -> Option<Ray> {
        self.target.map(|target| {
            let direction = (target - self.muzzle).normalize();
            Ray::new(self.muzzle.into(), direction.into())
        })
    }
}

fn attack(
    mut commands: Commands,
    mut attack_events: EventReader<AttackEvent>,
    cannons: Query<&LaserCannon>,
    mut chase_events: EventWriter<ChaseTargetEvent>,
) {
    for event in attack_events.read() {
        if let Ok(cannon) = cannons.get(event.attacker()) {
            commands
                .entity(event.attacker())
                .insert(Attacking::new(event.enemy()));

            let target = ChaseTarget::new(
                event.enemy(),
                MIN_CHASE_DISTNACE * cannon.range(),
                MAX_CHASE_DISTNACE * cannon.range(),
            );
            chase_events.send(ChaseTargetEvent::new(event.attacker(), Some(target)));
        }
    }
}

fn update_positions(
    mut commands: Commands,
    solids: SolidObjects,
    mut cannons: Query<(Entity, &Transform, &LaserCannon, &mut Attacking)>,
    targets: Query<(&Transform, &ObjectTypeComponent)>,
    sightline: SpatialQuery<Entity>,
) {
    for (attacker, transform, cannon, mut attacking) in cannons.iter_mut() {
        match targets.get(attacking.enemy) {
            Ok((enemy_transform, &target_type)) => {
                attacking.muzzle = transform.translation + cannon.muzzle();

                let enemy_aabb = solids.get(*target_type).collider().aabb();
                let enemy_centroid = enemy_transform.translation + Vec3::from(enemy_aabb.center());
                let direction = (enemy_centroid - attacking.muzzle)
                    .try_normalize()
                    .expect("Attacker and target too close together");
                let cannon_ray = Ray::new(attacking.muzzle.into(), direction.into());

                attacking.target = sightline
                    .cast_ray(&cannon_ray, cannon.range(), Some(attacker))
                    .map(|intersection| cannon_ray.point_at(intersection.toi()).into());
            }
            Err(_) => {
                commands.entity(attacker).remove::<Attacking>();
            }
        }
    }
}

fn charge(time: Res<Time>, mut cannons: Query<(&mut LaserCannon, Option<&Attacking>)>) {
    for (mut cannon, attacking) in cannons.iter_mut() {
        let charge = attacking
            .and_then(|attacking| attacking.distance())
            .map_or(false, |distance| distance <= cannon.range());
        cannon.charge_mut().tick(time.delta(), charge);
    }
}

fn aim_and_fire(
    mut attackers: Query<(Entity, &mut LaserCannon, &Attacking)>,
    sightline: LineOfSight,
    mut events: EventWriter<LaserFireEvent>,
) {
    let attackers = attackers.iter_mut();
    // The queue is used so that attacking has the same result as if it was
    // done in real-time (unaffected by update frequency).
    let mut fire_queue = BinaryHeap::new();

    for (attacker, mut cannon, attacking) in attackers {
        let ray = attacking.ray().filter(|ray| {
            sightline
                .sight(ray, cannon.range(), attacker)
                .entity()
                .map_or(false, |e| e == attacking.enemy)
        });

        if let Some(ray) = ray {
            if cannon.charge().charged() {
                fire_queue.push(FireScheduleItem::new(attacker, ray, cannon.into_inner()));
            }
        } else {
            cannon.charge_mut().hold();
        }
    }

    while let Some(mut fire_schedule_item) = fire_queue.pop() {
        if fire_schedule_item.fire(&mut events) {
            fire_queue.push(fire_schedule_item);
        }
    }
}

struct FireScheduleItem<'a> {
    attacker: Entity,
    ray: Ray,
    cannon: &'a mut LaserCannon,
}

impl<'a> FireScheduleItem<'a> {
    fn new(attacker: Entity, ray: Ray, cannon: &'a mut LaserCannon) -> Self {
        Self {
            attacker,
            ray,
            cannon,
        }
    }

    fn fire(&mut self, events: &mut EventWriter<LaserFireEvent>) -> bool {
        events.send(LaserFireEvent::new(
            self.attacker,
            self.ray,
            self.cannon.range(),
            self.cannon.damage(),
        ));
        self.cannon.charge_mut().fire()
    }
}

impl<'a> Ord for FireScheduleItem<'a> {
    fn cmp(&self, other: &Self) -> Ordering {
        let ordering = self.cannon.charge().cmp(other.cannon.charge());
        if let Ordering::Equal = ordering {
            // Make it more deterministic, objects with smaller coordinates
            // have disadvantage.
            self.ray
                .origin
                .partial_cmp(&other.ray.origin)
                .unwrap_or(Ordering::Equal)
        } else {
            ordering
        }
    }
}

impl<'a> PartialOrd for FireScheduleItem<'a> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<'a> PartialEq for FireScheduleItem<'a> {
    fn eq(&self, other: &Self) -> bool {
        self.ray.origin == other.ray.origin && self.cannon.charge() == other.cannon.charge()
    }
}

impl<'a> Eq for FireScheduleItem<'a> {}