veloren_world/civ/
airship_travel.rs

1use crate::{
2    sim::WorldSim,
3    site::{self, Site},
4    site2::plot::PlotKindMeta,
5    util::{DHashMap, DHashSet, seed_expan},
6};
7use common::{
8    store::{Id, Store},
9    terrain::CoordinateConversions,
10    util::Dir,
11};
12use rand::prelude::*;
13use rand_chacha::ChaChaRng;
14use std::{fs::OpenOptions, io::Write};
15use tracing::{debug, warn};
16use vek::*;
17
18const AIRSHIP_TRAVEL_DEBUG: bool = false;
19
20macro_rules! debug_airships {
21    ($($arg:tt)*) => {
22        if AIRSHIP_TRAVEL_DEBUG {
23            debug!($($arg)*);
24        }
25    }
26}
27
28/// A docking position (id, position). The docking position id is
29/// an index of all docking positions in the world.
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub struct AirshipDockingPosition(pub u32, pub Vec3<f32>);
32
33/// An airship can dock with its port or starboard side facing the dock.
34#[derive(Debug, Copy, Clone, PartialEq, Default)]
35pub enum AirshipDockingSide {
36    #[default]
37    Port,
38    Starboard,
39}
40
41/// An approach for the airship provides the data needed to fly to a docking
42/// position and stop at the docking position. The approach provides a target
43/// 'final' postion that is offset from the docking postion such that
44/// when the airship flys from the final position to the docking position, the
45/// airship will be naturally aligned with the direction of the docking
46/// position, and only very small orientation adjustments will be needed
47/// before docking. The approach final position is selected to minimize the
48/// change of direction when flying from the takeoff location to the target
49/// docking position.
50#[derive(Clone, Debug, PartialEq)]
51pub struct AirshipDockingApproach {
52    pub dock_pos: AirshipDockingPosition,
53    /// The position of the airship when docked.
54    /// This is different from dock_pos because the airship is offset to align
55    /// the ramp with the dock.
56    pub airship_pos: Vec3<f32>,
57    /// The direction the airship is facing when docked.
58    pub airship_direction: Dir,
59    /// Then center of the AirshipDock Plot.
60    pub dock_center: Vec2<f32>,
61    /// The height above terrain the airship cruises at.
62    pub height: f32,
63    /// A 3D position that is offset from the direct line between the dock sites
64    /// to allow for a transition to the direction the airship will be
65    /// facing when it is docked.
66    pub approach_initial_pos: Vec2<f32>,
67    /// Intermediate position from the initial position to smooth out the
68    /// directional changes.
69    pub approach_final_pos: Vec2<f32>,
70    /// There are ramps on both the port and starboard sides of the airship.
71    /// This gives the side that the airship will dock on.
72    pub side: AirshipDockingSide,
73    /// The site name where the airship will be docked at the end of the
74    /// approach.
75    pub site_id: Id<Site>,
76}
77
78/// A route that an airship flies round-trip between two sites.
79#[derive(Clone, Debug)]
80pub struct AirshipRoute {
81    /// site[0] is the home site, site[1] is the away site.
82    pub sites: [Id<site::Site>; 2],
83    /// approaches[0] is flying from the home site to the away site.
84    /// approaches[1] is flying from the away site to the home site.
85    pub approaches: [AirshipDockingApproach; 2],
86    /// The distance between the two sites.
87    pub distance: u32,
88}
89
90impl AirshipRoute {
91    fn new(
92        site1: Id<site::Site>,
93        site2: Id<site::Site>,
94        approaches: [AirshipDockingApproach; 2],
95        distance: u32,
96    ) -> Self {
97        Self {
98            sites: [site1, site2],
99            approaches,
100            distance,
101        }
102    }
103}
104
105/// Airship routes are identified by a unique serial number starting from zero.
106type AirshipRouteId = u32;
107
108/// Data for airship operations. This is generated world data.
109#[derive(Clone, Default)]
110pub struct Airships {
111    /// The airship routes between sites.
112    pub routes: DHashMap<AirshipRouteId, AirshipRoute>,
113}
114
115// Internal data structures
116
117/// The docking postions at an AirshipDock plot.
118/// The center is the center of the plot. The docking_positions
119/// are the positions where the airship can dock.
120#[derive(Clone, Debug)]
121struct AirshipDockPositions {
122    pub center: Vec2<f32>,
123    pub docking_positions: Vec<AirshipDockingPosition>,
124    pub site_id: Id<site::Site>,
125}
126
127impl AirshipDockPositions {
128    fn from_plot_meta(
129        first_id: u32,
130        center: Vec2<i32>,
131        docking_positions: &[Vec3<i32>],
132        site_id: Id<site::Site>,
133    ) -> Self {
134        let mut dock_pos_id = first_id;
135        Self {
136            center: center.map(|i| i as f32),
137            docking_positions: docking_positions
138                .iter()
139                .map(|pos: &Vec3<i32>| {
140                    let docking_position =
141                        AirshipDockingPosition(dock_pos_id, pos.map(|i| i as f32));
142                    dock_pos_id += 1;
143                    docking_position
144                })
145                .collect(),
146            site_id,
147        }
148    }
149}
150
151/// Used while generating the airship routes to connect the airship docks.
152/// Encapsulates the connection between two airship docks, including the angle
153/// and distance.
154#[derive(Clone, Debug)]
155struct AirRouteConnection<'a> {
156    pub dock1: &'a AirshipDockPositions,
157    pub dock2: &'a AirshipDockPositions,
158    pub angle: f32,    // angle from dock1 to dock2, from dock2 the angle is -angle
159    pub distance: i64, // distance squared between dock1 and dock2
160}
161
162impl<'a> AirRouteConnection<'a> {
163    fn new(dock1: &'a AirshipDockPositions, dock2: &'a AirshipDockPositions) -> Self {
164        let angle = Airships::angle_between_vectors_ccw(
165            Airships::ROUTES_NORTH,
166            dock2.center - dock1.center,
167        );
168        let distance = dock1.center.distance_squared(dock2.center) as i64;
169        Self {
170            dock1,
171            dock2,
172            angle,
173            distance,
174        }
175    }
176}
177
178/// Dock connnections are a hash map (DHashMap) of DockConnectionHashKey to
179/// AirRouteConnection. The hash map is used internally during the generation of
180/// the airship routes.
181#[derive(Eq, PartialEq, Hash, Debug)]
182struct DockConnectionHashKey(Id<site::Site>, Id<site::Site>);
183
184/// Represents potential connections between two airship docks. Used during the
185/// generation of the airship routes.
186#[derive(Clone, Debug)]
187struct DockConnection<'a> {
188    pub dock: &'a AirshipDockPositions,
189    pub available_connections: usize,
190    pub connections: Vec<&'a AirRouteConnection<'a>>,
191}
192
193impl<'a> DockConnection<'a> {
194    fn new(dock: &'a AirshipDockPositions) -> Self {
195        Self {
196            dock,
197            available_connections: dock.docking_positions.len(),
198            connections: Vec::new(),
199        }
200    }
201
202    fn add_connection(&mut self, connection: &'a AirRouteConnection<'a>) {
203        self.connections.push(connection);
204        self.available_connections -= 1;
205    }
206}
207
208impl Airships {
209    const AIRSHIP_PORT_OFFSET: Vec2<f32> = Vec2::new(
210        -Airships::AIRSHIP_TO_DOCK_CENTERLINE_OFFSET,
211        -Airships::AIRSHIP_TO_DOCK_FORE_AFT_OFFSET,
212    );
213    const AIRSHIP_STARBOARD_OFFSET: Vec2<f32> = Vec2::new(
214        -Airships::AIRSHIP_TO_DOCK_CENTERLINE_OFFSET,
215        Airships::AIRSHIP_TO_DOCK_FORE_AFT_OFFSET,
216    );
217    const AIRSHIP_TO_DOCK_CENTERLINE_OFFSET: f32 = 18.0;
218    const AIRSHIP_TO_DOCK_FORE_AFT_OFFSET: f32 = 3.0;
219    const DEFAULT_DOCK_DURATION: f32 = 90.0;
220    const ROUTES_NORTH: Vec2<f32> = Vec2::new(0.0, 15000.0);
221    const STD_CRUISE_HAT: f32 = 300.0;
222    const TAKEOFF_ASCENT_ALT: f32 = 150.0;
223
224    #[inline(always)]
225    pub fn docking_duration() -> f32 { Airships::DEFAULT_DOCK_DURATION }
226
227    #[inline(always)]
228    pub fn takeoff_ascent_hat() -> f32 { Airships::TAKEOFF_ASCENT_ALT }
229
230    #[inline(always)]
231    pub fn std_cruise_hat() -> f32 { Airships::STD_CRUISE_HAT }
232
233    /// Get all the airship docking positions from the world sites.
234    fn all_airshipdock_positions(sites: &mut Store<Site>) -> Vec<AirshipDockPositions> {
235        let mut dock_pos_id = 0;
236        sites
237            .iter()
238            .flat_map(|(site_id, site)| site.site2().map(|site2| (site_id, site2)))
239            .flat_map(|(site_id, site2)| {
240                site2.plots().flat_map(move |plot| {
241                    if let Some(PlotKindMeta::AirshipDock {
242                        center,
243                        docking_positions,
244                        ..
245                    }) = plot.kind().meta()
246                    {
247                        Some((center, docking_positions, site_id))
248                    } else {
249                        None
250                    }
251                })
252            })
253            .map(|(center, docking_positions, site_id)| {
254                let positions = AirshipDockPositions::from_plot_meta(
255                    dock_pos_id,
256                    center,
257                    docking_positions,
258                    site_id,
259                );
260
261                dock_pos_id += positions.docking_positions.len() as u32;
262                positions
263            })
264            .collect::<Vec<_>>()
265    }
266
267    /// Generate the network of airship routes between all the sites with
268    /// airship docks. This is called only from the world generation code.
269    ///
270    /// After world sites are generated, the airship operations center creates a
271    /// network of airship routes between all the sites containing an
272    /// airship dock plot, and there are airships placed at each docking
273    /// position that will be used for an airship route. Each airship travels
274    /// between two sites. This is the airship's route (out and back).  When
275    /// an airship is created, the ops center internally assigns the airship
276    /// a route based on the airship's home docking position and the airship
277    /// routing network. Since a route is between two sites, and therefore
278    /// between two docking positions, there are two airships flying in opposite
279    /// directions.
280    ///
281    /// Todo: On longer routes, it should be possible to determine the flight
282    /// time and add airships to the route to maintain a schedule. The
283    /// airships would be spawned midair so that they don't appear our of
284    /// nowhere near the ground.
285    ///
286    /// Airships are assigned a flying height based on the direction of
287    /// travel to deconflict as much as possible.
288    pub fn generate_airship_routes(
289        &mut self,
290        sites: &mut Store<Site>,
291        world_sim: &mut WorldSim,
292        seed: u32,
293    ) {
294        let all_docking_positions = Airships::all_airshipdock_positions(sites);
295        // Create a map of all possible dock to dock connections.
296        let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
297        let mut routes = DHashMap::<DockConnectionHashKey, AirRouteConnection>::default();
298        all_docking_positions.iter().for_each(|from_dock| {
299            all_docking_positions
300                .iter()
301                .filter(|to_dock| to_dock.site_id != from_dock.site_id)
302                .for_each(|to_dock| {
303                    routes.insert(
304                        DockConnectionHashKey(from_dock.site_id, to_dock.site_id),
305                        AirRouteConnection::new(from_dock, to_dock),
306                    );
307                });
308        });
309
310        // Do four rounds of connections.
311        // In each round, attempt to connect each dock to another dock that has at least
312        // one connection remaining. Assign scores to each candidate route based
313        // on:
314        // 1. How close the candidate route angle is to the optimal angle. The optimal
315        //    angle is calculated as follows:
316        //    - 0 existing connections - any angle, all angles are equally good.
317        //    - 1 existing connection - the angle for the vector opposite to the vector
318        //      for the existing connection.
319        //    - 2 existing connections - calculate the triangle formed by the first two
320        //      route endpoints and the dock center. Find the centroid of the triangle
321        //      and calculate the angle from the dock center to the centroid. The
322        //      optimal angle is the angle opposite to the vector from the dock center
323        //      to the centroid.
324        //    - 3 existing connections - calculate the triangle formed by the first
325        //      three connection endpoints. Find the centroid of the triangle and
326        //      calculate the angle from the dock center to the centroid. The optimal
327        //      angle is the angle opposite to the vector from the dock center to the
328        //      centroid.
329        // 2. The distance from the dock center to the connection endpoint. Generally,
330        //    the further the better, but the score function should be logarithmic to
331        //    not favor so much amongst the longer distances.
332
333        let mut dock_connections = all_docking_positions
334            .iter()
335            .map(DockConnection::new)
336            .collect::<Vec<_>>();
337
338        // The simple angle score is how close a2 is to the opposite of a1. E.g. if a1
339        // is 0.0, the the best score is when a2 is PI.
340        let angle_score_fn = |a1: f32, a2: f32| {
341            let optimal_angle = (a1 + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU);
342            let angle_diff = (optimal_angle - a2)
343                .abs()
344                .min(std::f32::consts::TAU - (optimal_angle - a2).abs());
345            1.0 - (angle_diff / std::f32::consts::PI)
346        };
347
348        // The centroid angle score function calculates the angle from the dock center
349        // to the given 'centroid' vector, then applies the angle score function
350        // with a1 being the centroid angle and a2 being the route angle.
351        let centroid_angle_score_fn =
352            |centroid: Vec2<f32>, dock_center: Vec2<f32>, rt: &AirRouteConnection| {
353                let centroid_dir = centroid - dock_center;
354                if centroid_dir.is_approx_zero() {
355                    return 0.0;
356                }
357                let centroid_angle =
358                    Airships::angle_between_vectors_ccw(Airships::ROUTES_NORTH, centroid_dir);
359                angle_score_fn(centroid_angle, rt.angle)
360            };
361
362        // The distance score function is logarithmic and favors long distances (but not
363        // by much). The lower cutoff is so that docks within 5000 blocks of
364        // each other are not connected unless there are no other options. The final
365        // division and subtraction are used to scale the score so that middle
366        // distances have a score of around 1.0.
367        let distance_score_fn = |distance: i64| {
368            // Note the distance argument is the square of the actual distance.
369            if distance > 25000000 {
370                (((distance - 24999000) / 1000) as f32).ln() / 8.0 - 0.5
371            } else {
372                0.0
373            }
374        };
375
376        // overall score function
377        let score_fn = |con: &DockConnection, rt: &AirRouteConnection| {
378            let mut angle_score = match con.connections.len() {
379                // Anything goes
380                0 => 1.0,
381                // Opposite angle
382                1 => angle_score_fn(con.connections[0].angle, rt.angle),
383                // Centroid angle of triangle formed by the first two connections and the dock
384                // center
385                2 => {
386                    let centroid = (con.connections[0].dock2.center
387                        + con.connections[1].dock2.center
388                        + con.dock.center)
389                        / 3.0;
390                    centroid_angle_score_fn(centroid, con.dock.center, rt)
391                },
392                // Centroid angle of triangle formed by the first three connections
393                3 => {
394                    let centroid = (con.connections[0].dock2.center
395                        + con.connections[1].dock2.center
396                        + con.connections[2].dock2.center)
397                        / 3.0;
398                    centroid_angle_score_fn(centroid, con.dock.center, rt)
399                },
400                _ => 0.0,
401            };
402            let distance_score = distance_score_fn(rt.distance);
403            // The 5.0 multiplier was established by trial and error. Without the
404            // multiplier, the routes tend to have a long distance bias. Giving
405            // the angle score more weight helps to balance the resulting route
406            // network.
407            angle_score *= 5.0;
408            (angle_score, distance_score, angle_score + distance_score)
409        };
410
411        for _ in 0..4 {
412            let mut best_trial: Option<(Vec<(Id<site::Site>, Id<site::Site>)>, f32)> = None;
413            // 100 loops to shuffle the dock connections and try different combinations is
414            // enough. Using 1000 loops doesn't improve the results.
415            for _ in 0..100 {
416                dock_connections.shuffle(&mut rng);
417                let candidates = dock_connections
418                    .iter()
419                    .filter(|con| con.available_connections > 0)
420                    .collect::<Vec<_>>();
421                let mut trial = Vec::new();
422                let mut trial_score = 0f32;
423                for chunk in candidates.chunks(2) {
424                    if let [con1, con2] = chunk {
425                        let dock1_id = con1.dock.site_id;
426                        let dock2_id = con2.dock.site_id;
427                        let dock1_route = routes
428                            .get(&DockConnectionHashKey(dock1_id, dock2_id))
429                            .unwrap();
430                        let dock2_route = routes
431                            .get(&DockConnectionHashKey(dock2_id, dock1_id))
432                            .unwrap();
433                        let score1 = score_fn(con1, dock1_route);
434                        let score2 = score_fn(con2, dock2_route);
435                        trial_score += score1.2 + score2.2;
436                        trial.push((dock1_id, dock2_id));
437                    }
438                }
439                if let Some(current_best_trial) = best_trial.as_mut() {
440                    if trial_score > current_best_trial.1 {
441                        *current_best_trial = (trial, trial_score);
442                    }
443                } else {
444                    best_trial = Some((trial, trial_score));
445                }
446            }
447            if let Some(ref final_best_trial) = best_trial {
448                for (site1, site2) in final_best_trial.0.iter() {
449                    let dock1_route = routes.get(&DockConnectionHashKey(*site1, *site2)).unwrap();
450                    let dock2_route = routes.get(&DockConnectionHashKey(*site2, *site1)).unwrap();
451                    let con1 = dock_connections
452                        .iter_mut()
453                        .find(|con| con.dock.site_id == *site1)
454                        .unwrap();
455                    if con1.available_connections > 0 {
456                        con1.add_connection(dock1_route);
457                    }
458                    let con2 = dock_connections
459                        .iter_mut()
460                        .find(|con| con.dock.site_id == *site2)
461                        .unwrap();
462                    if con2.available_connections > 0 {
463                        con2.add_connection(dock2_route);
464                    }
465                }
466            }
467        }
468
469        // The dock connections are now set.
470        // At this point, we now have a network of airship routes between all the sites
471        // with airship docks, and we have a list of docking positions for each
472        // site. As airships are generated, they can be assigned a route based
473        // on their home docking position and the airship routing network.
474        // The number of airships per dock is determined by the number of connections at
475        // the dock. The docking positions used at the dock can be random. Each
476        // airship will have a route assigned that it will fly, out and back,
477        // round trip between two sites. This needs to remain constant so that
478        // travelers can know where the airship is going. The routes can be generated
479        // before the airships, and when an airship is generated, the appropriate route
480        // can be found by finding the docking position id for the docking position with
481        // the wpos closest to the airship position. When an airship is is loaded from
482        // saved RTSim data, the assigned routes will already be available. The airship
483        // routes will be persisted in the rtsim data.
484
485        let mut routes_added = DHashSet::<DockConnectionHashKey>::default();
486        // keep track of the docking positions that have been used on either end of the
487        // route.
488        let mut used_docking_positions = DHashSet::<u32>::default();
489
490        let mut random_dock_pos_fn =
491            |dock: &AirshipDockPositions, used_positions: &DHashSet<u32>| {
492                let mut dock_pos_index = rng.gen_range(0..dock.docking_positions.len());
493                let begin = dock_pos_index;
494                while used_positions.contains(&dock.docking_positions[dock_pos_index].0) {
495                    dock_pos_index = (dock_pos_index + 1) % dock.docking_positions.len();
496                    if dock_pos_index == begin {
497                        return None;
498                    }
499                }
500                Some(dock_pos_index)
501            };
502
503        let mut airship_route_id: u32 = 0;
504        dock_connections.iter().for_each(|con| {
505            con.connections.iter().for_each(|rt| {
506                if !routes_added
507                    .contains(&DockConnectionHashKey(rt.dock1.site_id, rt.dock2.site_id))
508                {
509                    if let Some(from_dock_pos_index) =
510                        random_dock_pos_fn(rt.dock1, &used_docking_positions)
511                    {
512                        if let Some(to_dock_pos_index) =
513                            random_dock_pos_fn(rt.dock2, &used_docking_positions)
514                        {
515                            let from_dock_pos_id =
516                                rt.dock1.docking_positions[from_dock_pos_index].0;
517                            let to_dock_pos_id = rt.dock2.docking_positions[to_dock_pos_index].0;
518                            let approaches = Airships::airship_approaches_for_route(
519                                world_sim,
520                                rt,
521                                from_dock_pos_id,
522                                to_dock_pos_id,
523                            );
524                            let distance = rt.dock1.docking_positions[from_dock_pos_index]
525                                .1
526                                .xy()
527                                .distance(rt.dock2.docking_positions[to_dock_pos_index].1.xy())
528                                as u32;
529
530                            self.routes.insert(
531                                airship_route_id,
532                                AirshipRoute::new(
533                                    rt.dock1.site_id,
534                                    rt.dock2.site_id,
535                                    approaches,
536                                    distance,
537                                ),
538                            );
539                            airship_route_id += 1;
540
541                            used_docking_positions.insert(from_dock_pos_id);
542                            used_docking_positions.insert(to_dock_pos_id);
543                            routes_added
544                                .insert(DockConnectionHashKey(rt.dock1.site_id, rt.dock2.site_id));
545                            routes_added
546                                .insert(DockConnectionHashKey(rt.dock2.site_id, rt.dock1.site_id));
547                        }
548                    }
549                }
550            });
551        });
552    }
553
554    /// Given a docking position, find the airship route and approach index
555    /// where the approach endpoint is closest to the docking position.
556    /// Return the route id (u32) and the approach index (0 or 1).
557    pub fn airship_route_for_docking_pos(
558        &self,
559        docking_pos: Vec3<f32>,
560    ) -> Option<(AirshipRouteId, usize)> {
561        // Find the route where where either approach.dock_pos is equal (very close to)
562        // the given docking_pos.
563        if let Some((route_id, min_index, _)) = self
564            .routes
565            .iter()
566            .flat_map(|(rt_id, rt)| {
567                rt.approaches
568                    .iter()
569                    .enumerate()
570                    .map(move |(index, approach)| {
571                        let distance =
572                            approach.dock_pos.1.xy().distance_squared(docking_pos.xy()) as i64;
573                        (rt_id, index, distance)
574                    })
575            })
576            .min_by_key(|(_, _, distance)| *distance)
577        {
578            Some((*route_id, min_index))
579        } else {
580            // It should be impossible to get here if
581            // should_spawn_airship_at_docking_position is working correctly.
582            warn!(
583                "No airship route has a docking postion near {:?}",
584                docking_pos
585            );
586            None
587        }
588    }
589
590    /// Given a airship dock docking position, determine if an airship should be
591    /// spawned at the docking position. Some airship docks will not have
592    /// the docking positions completely filled because some docks are not
593    /// connected to the maximum number of sites. E.g., if there are an odd
594    /// number of sites with airship docks. Another reason is the way the
595    /// routes are generated.
596    pub fn should_spawn_airship_at_docking_position(
597        &self,
598        docking_pos: &Vec3<i32>,
599        site_name: &str,
600    ) -> bool {
601        let use_docking_pos = self.routes.iter().any(|(_, rt)| {
602            rt.approaches.iter().any(|approach| {
603                approach
604                    .dock_pos
605                    .1
606                    .xy()
607                    .distance_squared(docking_pos.map(|i| i as f32).xy())
608                    < 10.0
609            })
610        });
611        if !use_docking_pos {
612            debug_airships!(
613                "Skipping docking position {:?} for site {}",
614                docking_pos,
615                site_name
616            );
617        }
618        use_docking_pos
619    }
620
621    /// Get the position and direction for the airship to dock at the given
622    /// docking position. If use_starboard_boarding is None, the side for
623    /// boarding is randomly chosen. This assumes that the edge of the
624    /// docking position is perpendicular to vector from the dock plot
625    /// center to the docking position. The center of the airship position with
626    /// respect to the docking position is an asymmetrical offset depending on
627    /// which side of the airship will be used for boarding.
628    pub fn airship_vec_for_docking_pos(
629        docking_pos: Vec3<f32>,
630        airship_dock_center: Vec2<f32>,
631        docking_side: Option<AirshipDockingSide>,
632    ) -> (Vec3<f32>, Dir) {
633        // get the vector from the dock center to the docking station.
634        let dock_pos_offset = (docking_pos - airship_dock_center).xy();
635        // get the offset from the ship's center to the docking position and whether the
636        // ship will touch the dock on the left or right side.
637        let (ship_center_offset, reverse_ship) = match docking_side {
638            // Todo: get the docking platform offsets from the ship body?
639            Some(AirshipDockingSide::Starboard) => (Airships::AIRSHIP_STARBOARD_OFFSET, false), /* dock on ship's right side */
640            Some(AirshipDockingSide::Port) => (Airships::AIRSHIP_PORT_OFFSET, true), /* dock on ship's left side */
641            None => {
642                if thread_rng().gen::<bool>() {
643                    (Airships::AIRSHIP_STARBOARD_OFFSET, false) // randomly choose right side
644                } else {
645                    (Airships::AIRSHIP_PORT_OFFSET, true) // randomly choose left side
646                }
647            },
648        };
649        // Use unit_y as the reference vector.
650        let refvec = Vec2::unit_y();
651        // get the CW angle from the reference vector to the docking position direction.
652        let dock_pos_anglecw = Airships::angle_between_vectors_cw(refvec, dock_pos_offset);
653        // rotate the ship center offset by the angle from the reference vector to the
654        // docking position direction plus 90 degrees (because the ship will be
655        // rotated 90 degrees to the right or left when docking). Note that the
656        // Vec3::rotated_z function rotates the vector CCW, so account for that.
657        let airship_offset = ship_center_offset
658            .rotated_z(std::f32::consts::TAU - (dock_pos_anglecw + std::f32::consts::FRAC_PI_2))
659            .with_z(3.0);
660        // rotate the dock_pos_offset 90 degrees so that the side of the ship touching
661        // the dock is perpendicular to the docking pos. (rotate 90 degrees for
662        // the right side, 270 degrees for the left side, again accounting for the CCW
663        // rotation).
664        let rotation_angle = if reverse_ship {
665            std::f32::consts::FRAC_PI_2
666        } else {
667            std::f32::consts::FRAC_PI_2 * 3.0
668        };
669        let airship_dir =
670            Dir::from_unnormalized(dock_pos_offset.rotated_z(rotation_angle).with_z(0.0))
671                .unwrap_or_default();
672
673        // The docking positions seem to be a little low.
674        (docking_pos + airship_offset, airship_dir)
675    }
676
677    // Get the docking approach for the given docking position.
678    fn docking_approach_for(
679        depart_center: Vec2<f32>,
680        dest_center: Vec2<f32>,
681        docking_pos: &AirshipDockingPosition,
682        depart_to_dest_angle: f32,
683        map_center: Vec2<f32>,
684        max_dims: Vec2<f32>,
685        site_id: Id<Site>,
686    ) -> AirshipDockingApproach {
687        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
688            docking_pos.1,
689            dest_center,
690            Some(AirshipDockingSide::Starboard),
691        );
692        // calculate port final point. It is a 500 block extension from the docking
693        // position in the direction of the docking direction.
694        let port_final_pos = docking_pos.1.xy() + airship_direction.to_vec().xy() * 500.0;
695        let starboard_final_pos = docking_pos.1.xy() - airship_direction.to_vec().xy() * 500.0;
696        // calculate the turn angle required to align with the port. The port final
697        // point is the origin. One vector is the reverse of the vector from the
698        // port final point to the departure center. The other vector is from
699        // the port final point to the docking position.
700        let port_final_angle =
701            (airship_pos.xy() - port_final_pos).angle_between(-(depart_center - port_final_pos));
702        // The starboard angle is calculated similarly.
703        let starboard_final_angle = (airship_pos.xy() - starboard_final_pos)
704            .angle_between(-(depart_center - starboard_final_pos));
705
706        // If the angles are approximately equal, it means the departure position and
707        // the docking position are on the same line (angle near zero) or are
708        // perpendicular to each other (angle near 90). If perpendicular, pick
709        // the side where the final approach point is furthest from the edge of the map.
710        // If on the same line, pick the side where the final approach point is closest
711        // to the departure position.
712        let side = if (port_final_angle - starboard_final_angle).abs() < 0.1 {
713            // equal angles
714            if port_final_angle < std::f32::consts::FRAC_PI_4 {
715                // same line
716                if port_final_pos.distance_squared(depart_center)
717                    < starboard_final_pos.distance_squared(depart_center)
718                {
719                    // dock on port side
720                    AirshipDockingSide::Port
721                } else {
722                    // dock on starboard side
723                    AirshipDockingSide::Starboard
724                }
725            } else {
726                // perpendicular
727                // Use the final point closest to the center of the map.
728                if port_final_pos.distance_squared(map_center)
729                    < starboard_final_pos.distance_squared(map_center)
730                {
731                    // dock on port side
732                    AirshipDockingSide::Port
733                } else {
734                    // dock on starboard side
735                    AirshipDockingSide::Starboard
736                }
737            }
738        } else {
739            // pick the side with the least turn angle.
740            if port_final_angle < starboard_final_angle {
741                // port side
742                AirshipDockingSide::Port
743            } else {
744                // starboard side
745                AirshipDockingSide::Starboard
746            }
747        };
748
749        let height = if depart_to_dest_angle < std::f32::consts::PI {
750            Airships::STD_CRUISE_HAT
751        } else {
752            Airships::STD_CRUISE_HAT + 100.0
753        };
754
755        let check_pos_fn = |pos: Vec2<f32>, what: &str| {
756            if pos.x < 0.0 || pos.y < 0.0 || pos.x > max_dims.x || pos.y > max_dims.y {
757                warn!("{} pos out of bounds: {:?}", what, pos);
758            }
759        };
760
761        let initial_pos_fn = |final_pos: Vec2<f32>| {
762            // Get the angle between a line (1) connecting the final_pos and the
763            // depart_center and line (2) from the final_pos to the docking
764            // position. divide the angle in half then rotate line 1 CCW by that
765            // angle + 270 degrees. The initial approach point is on this
766            // rotated line 1, 500 blocks from the final_pos.
767            let line1 = (depart_center - final_pos).normalized();
768            let angle = line1.angle_between((airship_pos.xy() - final_pos).normalized());
769            let initial_pos_line = line1.rotated_z(angle / 2.0 + 3.0 * std::f32::consts::FRAC_PI_2);
770            let initial_pos = final_pos + initial_pos_line * 500.0;
771            check_pos_fn(final_pos, "final_pos");
772            check_pos_fn(initial_pos, "initial_pos");
773            initial_pos
774        };
775
776        if side == AirshipDockingSide::Starboard {
777            AirshipDockingApproach {
778                dock_pos: *docking_pos,
779                airship_pos,
780                airship_direction,
781                dock_center: dest_center,
782                height,
783                approach_initial_pos: initial_pos_fn(starboard_final_pos),
784                approach_final_pos: starboard_final_pos,
785                side,
786                site_id,
787            }
788        } else {
789            AirshipDockingApproach {
790                dock_pos: *docking_pos,
791                airship_pos,
792                airship_direction: -airship_direction,
793                dock_center: dest_center,
794                height,
795                approach_initial_pos: initial_pos_fn(port_final_pos),
796                approach_final_pos: port_final_pos,
797                side,
798                site_id,
799            }
800        }
801    }
802
803    /// Builds approaches for the given route connection.
804    /// Each docking position has two possible approaches, based on the
805    /// port and starboard sides of the airship. The approaches are aligned
806    /// with the docking position direction, which is always perpendicular
807    /// to the vector from the airship dock plot center to the docking position.
808    /// The airship can pivot around the z axis, but it does so slowly. To
809    /// ensure that the airship is oriented in the correct direction for
810    /// landing, and to make it more realistic, the airship approaches
811    /// the docking position pre-aligned with the landing direction. The
812    /// approach consists of two positions, the initial point where the
813    /// airship will turn toward the final point, at the final point it will
814    /// turn toward the docking position and will be aligned with the docking
815    /// direction.
816    fn airship_approaches_for_route(
817        world_sim: &mut WorldSim,
818        route: &AirRouteConnection,
819        dock1_position_id: u32,
820        dock2_position_id: u32,
821    ) -> [AirshipDockingApproach; 2] {
822        /*  o Pick the docking side with the least rotation angle from the departure position.
823              If the angles are approximately equal, it means the departure position and
824              the docking position are on the same line (angle near zero) or are perpendicular to
825              each other (angle near 90). If perpendicular, pick the side where the final approach
826              point is furthest from the edge of the map. If on the same line, pick the side where
827              the final approach point is closest to the departure position.
828            o The cruising height above terrain is based on the angle between North and the
829              line between the docking positions.
830        */
831
832        let map_size_chunks = world_sim.get_size().map(|u| u as i32);
833        let max_dims = map_size_chunks.cpos_to_wpos().map(|u| u as f32);
834        let map_center = Vec2::new(max_dims.x / 2.0, max_dims.y / 2.0);
835
836        let dock1_positions = &route.dock1;
837        let dock2_positions = &route.dock2;
838        let dock1_center = dock1_positions.center;
839        let dock2_center = dock2_positions.center;
840        let docking_pos1 = dock1_positions
841            .docking_positions
842            .iter()
843            .find(|dp| dp.0 == dock1_position_id)
844            .unwrap();
845        let docking_pos2 = dock2_positions
846            .docking_positions
847            .iter()
848            .find(|dp| dp.0 == dock2_position_id)
849            .unwrap();
850        let dock1_to_dock2_angle = Airships::angle_between_vectors_ccw(
851            Airships::ROUTES_NORTH,
852            docking_pos2.1.xy() - docking_pos1.1.xy(),
853        );
854        let dock2_to_dock1_angle = std::f32::consts::TAU - dock1_to_dock2_angle;
855        debug_airships!(
856            "airship_approaches_for_route - dock1_pos:{:?}, dock2_pos:{:?}, \
857             dock1_to_dock2_angle:{}, dock2_to_dock1_angle:{}",
858            docking_pos1,
859            docking_pos2,
860            dock1_to_dock2_angle,
861            dock2_to_dock1_angle
862        );
863
864        [
865            Airships::docking_approach_for(
866                dock1_center,
867                dock2_center,
868                docking_pos2,
869                dock1_to_dock2_angle,
870                map_center,
871                max_dims,
872                dock2_positions.site_id,
873            ),
874            Airships::docking_approach_for(
875                dock2_center,
876                dock1_center,
877                docking_pos1,
878                dock2_to_dock1_angle,
879                map_center,
880                max_dims,
881                dock1_positions.site_id,
882            ),
883        ]
884    }
885
886    /// Returns the angle from vec v1 to vec v2 in the CCW direction.
887    fn angle_between_vectors_ccw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
888        let dot_product = v1.dot(v2);
889        let det = v1.x * v2.y - v1.y * v2.x; // determinant
890        let angle = det.atan2(dot_product); // atan2(det, dot_product) gives the CCW angle
891        if angle < 0.0 {
892            angle + std::f32::consts::TAU
893        } else {
894            angle
895        }
896    }
897
898    /// Returns the angle from vec v1 to vec v2 in the CW direction.
899    fn angle_between_vectors_cw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
900        let ccw_angle = Airships::angle_between_vectors_ccw(v1, v2);
901        std::f32::consts::TAU - ccw_angle
902    }
903}
904
905/// For debuging the airship routes. Writes the airship routes to a json file.
906fn write_airship_routes_log(file_path: &str, jsonstr: &str) -> std::io::Result<()> {
907    let mut file = OpenOptions::new()
908        .write(true)
909        .create(true)
910        .truncate(true)
911        .open(file_path)?;
912    file.write_all(jsonstr.as_bytes())?;
913    Ok(())
914}
915
916#[cfg(test)]
917mod tests {
918    use super::{AirshipDockingSide, Airships, approx::assert_relative_eq};
919    use vek::{Quaternion, Vec2, Vec3};
920
921    #[test]
922    fn basic_vec_test() {
923        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
924        let vec2 = Vec3::new(10.0, 0.0, 0.0);
925        let a12 = vec2.angle_between(vec1);
926        assert_relative_eq!(a12, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
927
928        let rotc2 = Quaternion::rotation_z(a12);
929        let vec3 = rotc2 * vec2;
930        assert!(vec3 == vec1);
931    }
932
933    #[test]
934    fn std_vec_angles_test() {
935        let refvec = Vec2::new(0.0f32, 10.0);
936
937        let vec1 = Vec2::new(0.0f32, 10.0);
938        let vec2 = Vec2::new(10.0f32, 0.0);
939        let vec3 = Vec2::new(0.0f32, -10.0);
940        let vec4 = Vec2::new(-10.0f32, 0.0);
941
942        let a1r = vec1.angle_between(refvec);
943        assert!(a1r == 0.0f32);
944
945        let a2r = vec2.angle_between(refvec);
946        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
947
948        let a3r: f32 = vec3.angle_between(refvec);
949        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
950
951        let a4r = vec4.angle_between(refvec);
952        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
953    }
954
955    #[test]
956    fn vec_angles_test() {
957        let refvec = Vec3::new(0.0f32, 10.0, 0.0);
958
959        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
960        let vec2 = Vec3::new(10.0f32, 0.0, 0.0);
961        let vec3 = Vec3::new(0.0f32, -10.0, 0.0);
962        let vec4 = Vec3::new(-10.0f32, 0.0, 0.0);
963
964        let a1r = vec1.angle_between(refvec);
965        let a1r3 = Airships::angle_between_vectors_ccw(vec1.xy(), refvec.xy());
966        assert!(a1r == 0.0f32);
967        assert!(a1r3 == 0.0f32);
968
969        let a2r = vec2.angle_between(refvec);
970        let a2r3 = Airships::angle_between_vectors_ccw(vec2.xy(), refvec.xy());
971        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
972        assert_relative_eq!(a2r3, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
973
974        let a3r: f32 = vec3.angle_between(refvec);
975        let a3r3 = Airships::angle_between_vectors_ccw(vec3.xy(), refvec.xy());
976        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
977        assert_relative_eq!(a3r3, std::f32::consts::PI, epsilon = 0.00001);
978
979        let a4r = vec4.angle_between(refvec);
980        let a4r3 = Airships::angle_between_vectors_ccw(vec4.xy(), refvec.xy());
981        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
982        assert_relative_eq!(a4r3, std::f32::consts::FRAC_PI_2 * 3.0, epsilon = 0.00001);
983    }
984
985    #[test]
986    fn airship_angles_test() {
987        let refvec = Vec2::new(0.0f32, 37.0);
988        let ovec = Vec2::new(-4.0f32, -14.0);
989        let oveccw0 = Vec2::new(-4, -14);
990        let oveccw90 = Vec2::new(-14, 4);
991        let oveccw180 = Vec2::new(4, 14);
992        let oveccw270 = Vec2::new(14, -4);
993        let ovecccw0 = Vec2::new(-4, -14);
994        let ovecccw90 = Vec2::new(14, -4);
995        let ovecccw180 = Vec2::new(4, 14);
996        let ovecccw270 = Vec2::new(-14, 4);
997
998        let vec1 = Vec2::new(0.0f32, 37.0);
999        let vec2 = Vec2::new(37.0f32, 0.0);
1000        let vec3 = Vec2::new(0.0f32, -37.0);
1001        let vec4 = Vec2::new(-37.0f32, 0.0);
1002
1003        assert!(
1004            ovec.rotated_z(Airships::angle_between_vectors_cw(vec1, refvec))
1005                .map(|x| x.round() as i32)
1006                == oveccw0
1007        );
1008        assert!(
1009            ovec.rotated_z(Airships::angle_between_vectors_cw(vec2, refvec))
1010                .map(|x| x.round() as i32)
1011                == oveccw90
1012        );
1013        assert!(
1014            ovec.rotated_z(Airships::angle_between_vectors_cw(vec3, refvec))
1015                .map(|x| x.round() as i32)
1016                == oveccw180
1017        );
1018        assert!(
1019            ovec.rotated_z(Airships::angle_between_vectors_cw(vec4, refvec))
1020                .map(|x| x.round() as i32)
1021                == oveccw270
1022        );
1023
1024        assert!(
1025            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec1, refvec))
1026                .map(|x| x.round() as i32)
1027                == ovecccw0
1028        );
1029        assert!(
1030            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec2, refvec))
1031                .map(|x| x.round() as i32)
1032                == ovecccw90
1033        );
1034        assert!(
1035            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec3, refvec))
1036                .map(|x| x.round() as i32)
1037                == ovecccw180
1038        );
1039        assert!(
1040            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec4, refvec))
1041                .map(|x| x.round() as i32)
1042                == ovecccw270
1043        );
1044    }
1045
1046    #[test]
1047    fn airship_vec_test() {
1048        {
1049            let dock_pos = Vec3::new(10.0f32, 10.0, 0.0);
1050            let airship_dock_center = Vec2::new(0.0, 0.0);
1051            {
1052                let (airship_pos, airship_dir) =
1053                    Airships::airship_vec_for_docking_pos(dock_pos, airship_dock_center, None);
1054                if airship_pos.x > 21.0 {
1055                    assert_relative_eq!(
1056                        airship_pos,
1057                        Vec3 {
1058                            x: 24.84924,
1059                            y: 20.606606,
1060                            z: 3.0
1061                        },
1062                        epsilon = 0.00001
1063                    );
1064                    assert_relative_eq!(
1065                        airship_dir.to_vec(),
1066                        Vec3 {
1067                            x: 0.70710677,
1068                            y: -0.70710677,
1069                            z: 0.0
1070                        },
1071                        epsilon = 0.00001
1072                    );
1073                } else {
1074                    assert_relative_eq!(
1075                        airship_pos,
1076                        Vec3 {
1077                            x: 20.606598,
1078                            y: 24.849243,
1079                            z: 3.0
1080                        },
1081                        epsilon = 0.00001
1082                    );
1083                    assert_relative_eq!(
1084                        airship_dir.to_vec(),
1085                        Vec3 {
1086                            x: -0.70710677,
1087                            y: 0.70710677,
1088                            z: 0.0
1089                        },
1090                        epsilon = 0.00001
1091                    );
1092                }
1093            }
1094            {
1095                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
1096                    dock_pos,
1097                    airship_dock_center,
1098                    Some(AirshipDockingSide::Port),
1099                );
1100                assert_relative_eq!(
1101                    airship_pos,
1102                    Vec3 {
1103                        x: 20.606598,
1104                        y: 24.849243,
1105                        z: 3.0
1106                    },
1107                    epsilon = 0.00001
1108                );
1109                assert_relative_eq!(
1110                    airship_dir.to_vec(),
1111                    Vec3 {
1112                        x: -0.70710677,
1113                        y: 0.70710677,
1114                        z: 0.0
1115                    },
1116                    epsilon = 0.00001
1117                );
1118            }
1119            {
1120                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
1121                    dock_pos,
1122                    airship_dock_center,
1123                    Some(AirshipDockingSide::Starboard),
1124                );
1125                assert_relative_eq!(
1126                    airship_pos,
1127                    Vec3 {
1128                        x: 24.84924,
1129                        y: 20.606606,
1130                        z: 3.0
1131                    },
1132                    epsilon = 0.00001
1133                );
1134                assert_relative_eq!(
1135                    airship_dir.to_vec(),
1136                    Vec3 {
1137                        x: 0.70710677,
1138                        y: -0.70710677,
1139                        z: 0.0
1140                    },
1141                    epsilon = 0.00001
1142                );
1143            }
1144        }
1145        {
1146            let dock_pos = Vec3::new(28874.0, 18561.0, 0.0);
1147            let airship_dock_center = Vec2::new(28911.0, 18561.0);
1148            {
1149                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
1150                    dock_pos,
1151                    airship_dock_center,
1152                    Some(AirshipDockingSide::Port),
1153                );
1154                assert_relative_eq!(
1155                    airship_pos,
1156                    Vec3 {
1157                        x: 28856.0,
1158                        y: 18558.0,
1159                        z: 3.0
1160                    },
1161                    epsilon = 0.00001
1162                );
1163                assert_relative_eq!(
1164                    airship_dir.to_vec(),
1165                    Vec3 {
1166                        x: 4.371139e-8,
1167                        y: -1.0,
1168                        z: 0.0
1169                    },
1170                    epsilon = 0.00001
1171                );
1172            }
1173            {
1174                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
1175                    dock_pos,
1176                    airship_dock_center,
1177                    Some(AirshipDockingSide::Starboard),
1178                );
1179                assert_relative_eq!(
1180                    airship_pos,
1181                    Vec3 {
1182                        x: 28856.0,
1183                        y: 18564.0,
1184                        z: 3.0
1185                    },
1186                    epsilon = 0.00001
1187                );
1188                assert_relative_eq!(
1189                    airship_dir.to_vec(),
1190                    Vec3 {
1191                        x: -1.1924881e-8,
1192                        y: 1.0,
1193                        z: 0.0
1194                    },
1195                    epsilon = 0.00001
1196                );
1197            }
1198        }
1199    }
1200
1201    #[test]
1202    fn angle_score_test() {
1203        let rt_angles = [
1204            0.0,
1205            std::f32::consts::FRAC_PI_2,
1206            std::f32::consts::PI,
1207            std::f32::consts::FRAC_PI_2 * 3.0,
1208        ];
1209        let con_angles = [
1210            0.0,
1211            std::f32::consts::FRAC_PI_2,
1212            std::f32::consts::PI,
1213            std::f32::consts::FRAC_PI_2 * 3.0,
1214        ];
1215        let scores = [
1216            [0.0, 2.5, 5.0, 2.5],
1217            [2.5, 0.0, 2.5, 5.0],
1218            [5.0, 2.5, 0.0, 2.5],
1219            [2.5, 5.0, 2.5, 0.0],
1220        ];
1221        let score_fn2 = |a1: f32, a2: f32| {
1222            let optimal_angle = (a1 + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU);
1223            let angle_diff = (optimal_angle - a2)
1224                .abs()
1225                .min(std::f32::consts::TAU - (optimal_angle - a2).abs());
1226            (1.0 - (angle_diff / std::f32::consts::PI)) * 5.0
1227        };
1228        let mut i = 0;
1229        let mut j = 0;
1230        rt_angles.iter().for_each(|rt_angle| {
1231            j = 0;
1232            con_angles.iter().for_each(|con_angle| {
1233                let score = score_fn2(*con_angle, *rt_angle);
1234                assert_relative_eq!(score, scores[i][j], epsilon = 0.00001);
1235                j += 1;
1236            });
1237            i += 1;
1238        });
1239    }
1240
1241    #[test]
1242    fn distance_score_test() {
1243        let distances = [1, 1000, 5001, 6000, 15000, 30000, 48000];
1244        let scores = [
1245            0.0,
1246            0.0,
1247            -0.20026308,
1248            0.66321766,
1249            1.0257597,
1250            1.2102475,
1251            1.329906,
1252        ];
1253        let score_fn = |distance: i64| {
1254            if distance > 25000000 {
1255                (((distance - 24999000) / 1000) as f32).ln() / 8.0 - 0.5
1256            } else {
1257                0.0
1258            }
1259        };
1260        let mut i = 0;
1261        distances.iter().for_each(|distance| {
1262            let dist2 = *distance * *distance;
1263            let score = score_fn(dist2);
1264            assert_relative_eq!(score, scores[i], epsilon = 0.00001);
1265            i += 1;
1266        });
1267    }
1268}