veloren_world/civ/
airship_travel.rs

1use crate::{
2    Index,
3    sim::WorldSim,
4    site::{self, Site, plot::PlotKindMeta},
5    util::{DHashMap, DHashSet, seed_expan},
6};
7use common::{
8    store::{Id, Store},
9    terrain::{MapSizeLg, TERRAIN_CHUNK_BLOCKS_LG},
10    util::Dir,
11};
12use delaunator::{Point, Triangulation, triangulate};
13use itertools::Itertools;
14use rand::{SeedableRng, prelude::*};
15use rand_chacha::ChaChaRng;
16use tracing::error;
17use vek::*;
18
19#[cfg(feature = "airship_maps")]
20use crate::civ::airship_route_map::*;
21
22#[cfg(debug_assertions)]
23macro_rules! debug_airships {
24    ($level:expr, $($arg:tt)*) => {
25        match $level {
26            0 => tracing::info!($($arg)*),
27            1 => tracing::warn!($($arg)*),
28            2 => tracing::error!($($arg)*),
29            3 => tracing::debug!($($arg)*),
30            4 => tracing::trace!($($arg)*),
31            _ => tracing::trace!($($arg)*),
32        }
33    }
34}
35
36#[cfg(not(debug_assertions))]
37macro_rules! debug_airships {
38    ($($arg:tt)*) => {};
39}
40
41/// A docking position (id, position). The docking position id is
42/// an index of all docking positions in the world.
43#[derive(Clone, Copy, Debug, Default, PartialEq)]
44pub struct AirshipDockingPosition(pub u32, pub Vec3<f32>);
45
46/// The AirshipDock Sites are always oriented along a cardinal direction.
47/// The docking platforms are likewise on the sides of the dock perpendicular
48/// to a cardinal axis.
49#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
50pub enum AirshipDockPlatform {
51    #[default]
52    NorthPlatform,
53    EastPlatform,
54    SouthPlatform,
55    WestPlatform,
56}
57
58/// An airship can dock with its port or starboard side facing the dock.
59#[derive(Debug, Copy, Clone, PartialEq, Default)]
60pub enum AirshipDockingSide {
61    #[default]
62    Port,
63    Starboard,
64}
65
66impl AirshipDockingSide {
67    const EAST_REF_VEC: Vec2<f32> = Vec2 { x: 1.0, y: 0.0 };
68    const NORTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: 1.0 };
69    const SOUTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: -1.0 };
70    const WEST_REF_VEC: Vec2<f32> = Vec2 { x: -1.0, y: 0.0 };
71
72    /// When docking, the side to use depends on the angle the airship is
73    /// approaching the dock from, and the platform of the airship dock that
74    /// the airship is docking at.
75    /// For example, when docking at the North Platform:
76    ///
77    /// | From the          |  Docking Side |
78    /// |:----------------- |:--------------|
79    /// | West              |  Starboard    |
80    /// | Northwest         |  Starboard    |
81    /// | North             |  Starboard    |
82    /// | Northeast         |  Port         |
83    /// | East              |  Port         |
84    /// | Southeast         |  Port         |
85    /// | South             |  Port         |
86    /// | Southwest         |  Starboard    |
87    pub fn from_dir_to_platform(dir: &Vec2<f32>, platform: &AirshipDockPlatform) -> Self {
88        // get the reference vector and precompute whether to flip the angle based on
89        // the dir input.
90        let (ref_vec, negate_angle) = match platform {
91            AirshipDockPlatform::NorthPlatform => (&AirshipDockingSide::NORTH_REF_VEC, dir.x < 0.0),
92            AirshipDockPlatform::EastPlatform => (&AirshipDockingSide::EAST_REF_VEC, dir.y > 0.0),
93            AirshipDockPlatform::SouthPlatform => (&AirshipDockingSide::SOUTH_REF_VEC, dir.x > 0.0),
94            AirshipDockPlatform::WestPlatform => (&AirshipDockingSide::WEST_REF_VEC, dir.y < 0.0),
95        };
96        let mut angle = dir.angle_between(*ref_vec).to_degrees();
97        if negate_angle {
98            angle = -angle;
99        }
100        match angle as i32 {
101            -360..=0 => AirshipDockingSide::Port,
102            _ => AirshipDockingSide::Starboard,
103        }
104    }
105}
106
107/// Information needed for an airship to travel to and dock at an AirshipDock
108/// plot.
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct AirshipDockingApproach {
111    /// The position of the airship when docked.
112    /// This is different from dock_pos because the airship is offset to align
113    /// the ramp with the dock.
114    pub airship_pos: Vec3<f32>,
115    /// The direction the airship is facing when docked.
116    pub airship_direction: Dir,
117    /// Then center of the AirshipDock Plot.
118    pub dock_center: Vec2<f32>,
119    /// The height above terrain the airship cruises at.
120    pub height: f32,
121    /// The midpoint of the cruise phase of flight.
122    pub midpoint: Vec2<f32>,
123    /// The end point of the cruise phase of flight.
124    pub approach_transition_pos: Vec2<f32>,
125    /// There are ramps on both the port and starboard sides of the airship.
126    /// This gives the side that the airship will dock on.
127    pub side: AirshipDockingSide,
128    /// The site where the airship will be docked at the end of the
129    /// approach.
130    pub site_id: Id<Site>,
131}
132
133/// The docking postions at an AirshipDock plot.
134#[derive(Clone, Debug)]
135pub struct AirshipDockPositions {
136    /// The center of the AirshipDock plot. From the world generation code.
137    pub center: Vec2<f32>,
138    /// The docking positions for the airship, derived from the
139    /// positions calculated in the world generation code.
140    pub docking_positions: Vec<AirshipDockingPosition>,
141    /// The id of the Site where the AirshipDock is located.
142    pub site_id: Id<site::Site>,
143}
144
145/// The flight phases of an airship.
146#[derive(Debug, Copy, Clone, PartialEq, Default)]
147#[repr(usize)]
148pub enum AirshipFlightPhase {
149    DepartureCruise,
150    ApproachCruise,
151    Transition,
152    Descent,
153    #[default]
154    Docked,
155    Ascent,
156}
157
158impl std::fmt::Display for AirshipFlightPhase {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            AirshipFlightPhase::DepartureCruise => write!(f, "DepartureCruise"),
162            AirshipFlightPhase::ApproachCruise => write!(f, "ApproachCruise"),
163            AirshipFlightPhase::Transition => write!(f, "Transition"),
164            AirshipFlightPhase::Descent => write!(f, "Descent"),
165            AirshipFlightPhase::Docked => write!(f, "Docked"),
166            AirshipFlightPhase::Ascent => write!(f, "Ascent"),
167        }
168    }
169}
170
171/// One flight phase of an airship route leg.
172/// The position is the destination (or only) location for the segment.
173#[derive(Clone, Default, Debug)]
174pub struct RouteLegSegment {
175    /// Flight phase describes what the airship is doing
176    /// and is used in the NPC logic to define how the airship moves to the
177    /// position.
178    pub flight_phase: AirshipFlightPhase,
179    /// The starting (or only) position for the segment.
180    pub from_world_pos: Vec2<f32>,
181    /// The destination (or only) position for the segment.
182    pub to_world_pos: Vec2<f32>,
183    /// The distance covered in world blocks.
184    pub distance: f32,
185    /// How long it's supposed to take to cover the distance in seconds.
186    pub duration: f32,
187    /// The time at which the airship is supposed to arrive at the destination,
188    /// or the end of the docking phase. This is the cumulative time for the
189    /// entire route including all previous leg segments.
190    pub route_time: f64,
191}
192
193/// One leg of an airship route.
194/// The leg starts when the airship leaves the docking area at the end of the
195/// ascent phase and ends at the end of the ascent phase at the docking
196/// destination. Leg segments are:
197/// - Departure Cruise (DC) from the end of the previous ascent to the leg
198///   midpoint.
199/// - Approach Cruise (AC) from the leg midpoint to the transition start.
200/// - Transition (T) from the transition pos to above the dock.
201/// - Descent (D) to the docking position.
202/// - Parked/Docked (P) at the dock.
203/// - Ascent (A) back to cruising height above the dock.
204#[derive(Clone, Default, Debug)]
205pub struct AirshipRouteLeg {
206    /// The index of the destination in Airships::docking_positions.
207    pub dest_index: usize,
208    /// The assigned docking platform at the destination dock for this leg.
209    pub platform: AirshipDockPlatform,
210    /// The leg segments.
211    pub segments: [RouteLegSegment; 6],
212}
213
214/// An airship route is a series of legs that form a continuous loop.
215/// Each leg goes from one airship docking site to another.
216#[derive(Debug, Clone)]
217pub struct AirshipRoute {
218    pub legs: Vec<AirshipRouteLeg>,
219    pub total_time: f64,
220    pub airship_time_spacing: f64,
221    pub cruising_height: f32,
222    pub spawning_locations: Vec<AirshipSpawningLocation>,
223}
224
225/// Data for airship operations. This is generated world data.
226#[derive(Clone, Default)]
227pub struct Airships {
228    /// The docking positions for all airship docks in the world.
229    pub airship_docks: Vec<AirshipDockPositions>,
230    /// The routes flown by the collective airships in the world.
231    pub routes: Vec<AirshipRoute>,
232    /// The speed of simulated airships (and the nominal speed of loaded
233    /// airships) in world blocks per second.
234    pub nominal_speed: f32,
235}
236
237/// Information needed for placing an airship in the world when the world is
238/// generated (each time the server starts).
239#[derive(Debug, Clone)]
240pub struct AirshipSpawningLocation {
241    /// The flight phase the airship is in when spawned.
242    pub flight_phase: AirshipFlightPhase,
243    /// The 2D position of the airship when spawned.
244    pub pos: Vec2<f32>,
245    /// The direction the airship is facing when spawned.
246    pub dir: Vec2<f32>,
247    /// For cruise and transition phases, the height above terrain.
248    /// For descent, docked, and ascent phases, the actual z position.
249    pub height: f32,
250    /// The index of the route the airship is flying.
251    pub route_index: usize,
252    /// The index of the leg in the route the airship is flying.
253    pub leg_index: usize,
254    /// The effective route time when the airship is spawned.
255    /// This will be less than the route time for the end
256    /// of the phase the airship is in.
257    pub spawn_route_time: f64,
258}
259
260// Internal data structures
261
262impl AirshipDockPositions {
263    fn from_plot_meta(
264        first_id: u32,
265        center: Vec2<i32>,
266        docking_positions: &[Vec3<i32>],
267        site_id: Id<site::Site>,
268    ) -> Self {
269        let mut dock_pos_id = first_id;
270        Self {
271            center: center.map(|i| i as f32),
272            docking_positions: docking_positions
273                .iter()
274                .map(|pos: &Vec3<i32>| {
275                    let docking_position =
276                        AirshipDockingPosition(dock_pos_id, pos.map(|i| i as f32));
277                    dock_pos_id += 1;
278                    docking_position
279                })
280                .collect(),
281            site_id,
282        }
283    }
284
285    /// Get the docking position that matches the given platform.
286    fn docking_position(&self, platform: AirshipDockPlatform) -> Vec3<f32> {
287        self.docking_positions
288            .iter()
289            .find_map(|&docking_position| {
290                // The docking position is the one that matches the platform.
291                // The platform is determined by the direction of the docking position
292                // relative to the center of the dock.
293                let docking_position_platform =
294                    AirshipDockPlatform::from_dir(docking_position.1.xy() - self.center);
295                if docking_position_platform == platform {
296                    Some(docking_position.1)
297                } else {
298                    None
299                }
300            })
301            .unwrap_or_else(|| {
302                // If no docking position is found, return the dock center.
303                self.center.with_z(1000.0)
304            })
305    }
306}
307
308// These are used in AirshipDockPlatform::choices_from_dir
309
310static SWEN_PLATFORMS: [AirshipDockPlatform; 4] = [
311    AirshipDockPlatform::SouthPlatform,
312    AirshipDockPlatform::WestPlatform,
313    AirshipDockPlatform::EastPlatform,
314    AirshipDockPlatform::NorthPlatform,
315];
316
317static SEWN_PLATFORMS: [AirshipDockPlatform; 4] = [
318    AirshipDockPlatform::SouthPlatform,
319    AirshipDockPlatform::EastPlatform,
320    AirshipDockPlatform::WestPlatform,
321    AirshipDockPlatform::NorthPlatform,
322];
323
324static WNSE_PLATFORMS: [AirshipDockPlatform; 4] = [
325    AirshipDockPlatform::WestPlatform,
326    AirshipDockPlatform::NorthPlatform,
327    AirshipDockPlatform::SouthPlatform,
328    AirshipDockPlatform::EastPlatform,
329];
330
331static WSNE_PLATFORMS: [AirshipDockPlatform; 4] = [
332    AirshipDockPlatform::WestPlatform,
333    AirshipDockPlatform::SouthPlatform,
334    AirshipDockPlatform::NorthPlatform,
335    AirshipDockPlatform::EastPlatform,
336];
337
338static NEWS_PLATFORMS: [AirshipDockPlatform; 4] = [
339    AirshipDockPlatform::NorthPlatform,
340    AirshipDockPlatform::EastPlatform,
341    AirshipDockPlatform::WestPlatform,
342    AirshipDockPlatform::SouthPlatform,
343];
344
345static NWES_PLATFORMS: [AirshipDockPlatform; 4] = [
346    AirshipDockPlatform::NorthPlatform,
347    AirshipDockPlatform::WestPlatform,
348    AirshipDockPlatform::EastPlatform,
349    AirshipDockPlatform::SouthPlatform,
350];
351
352static ESNW_PLATFORMS: [AirshipDockPlatform; 4] = [
353    AirshipDockPlatform::EastPlatform,
354    AirshipDockPlatform::SouthPlatform,
355    AirshipDockPlatform::NorthPlatform,
356    AirshipDockPlatform::WestPlatform,
357];
358
359static ENSW_PLATFORMS: [AirshipDockPlatform; 4] = [
360    AirshipDockPlatform::EastPlatform,
361    AirshipDockPlatform::NorthPlatform,
362    AirshipDockPlatform::SouthPlatform,
363    AirshipDockPlatform::WestPlatform,
364];
365
366/// The docking platforms used on each leg of the airship route segments is
367/// determined when the routes are generated. Route segments are continuous
368/// loops that are deconflicted by using only one docking platform for any given
369/// leg of a route segment. Since there are four docking platforms per airship
370/// dock, there are at most four route segments passing through a given airship
371/// dock. The docking platforms are also optimized so that on the incoming leg
372/// of a route segment, the airship uses the docking platform that is closest to
373/// the arrival direction (if possible), while still using only one docking
374/// platform per route segment leg.
375impl AirshipDockPlatform {
376    /// Get the preferred docking platform based on the direction vector.
377    pub fn from_dir(dir: Vec2<f32>) -> Self {
378        if let Some(dir) = dir.try_normalized() {
379            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
380            if dir.x < 0.0 {
381                angle = -angle;
382            }
383            match angle as i32 {
384                -360..=-135 => AirshipDockPlatform::SouthPlatform,
385                -134..=-45 => AirshipDockPlatform::WestPlatform,
386                -44..=45 => AirshipDockPlatform::NorthPlatform,
387                46..=135 => AirshipDockPlatform::EastPlatform,
388                136..=360 => AirshipDockPlatform::SouthPlatform,
389                _ => AirshipDockPlatform::NorthPlatform, // should never happen
390            }
391        } else {
392            AirshipDockPlatform::NorthPlatform // default value, should never happen
393        }
394    }
395
396    /// Get the platform choices in order of preference based on the direction
397    /// vector. The first choice is always the most direct plaform given the
398    /// approach direction. Then, the next two choices are the platforms for
399    /// the cardinal directions on each side of the approach direction, and
400    /// the last choice is the platform on the opposite side of the dock.
401    /// The return value is one of the ABCD_PLATFORMS arrays defined above.
402    pub fn choices_from_dir(dir: Vec2<f32>) -> &'static [AirshipDockPlatform] {
403        if let Some(dir) = dir.try_normalized() {
404            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
405            if dir.x < 0.0 {
406                angle = -angle;
407            }
408            // This code works similar to the Direction enum in the common crate.
409            // Angle between produces the smallest angle between two vectors,
410            // so when dir.x is negative, we force the angle to be negative.
411            // 0 or 360 is North. It is assumed that the angle ranges from -360 to 360
412            // degrees even though angles less than -180 or greater than 180
413            // should never be seen.
414            match angle as i32 {
415                -360..=-135 => {
416                    // primary is SouthPlatform
417                    // As a fallback (for when the south platform is already claimed),
418                    // if the direction is more towards the west, use the west platform,
419                    // and if the direction is more towards the east, use the east platform.
420                    // The north platform is always the last resort. All fallback blocks
421                    // below work similarly.
422                    if angle as i32 > -180 {
423                        &SWEN_PLATFORMS
424                    } else {
425                        &SEWN_PLATFORMS
426                    }
427                },
428                -134..=-45 => {
429                    // primary is WestPlatform
430                    if angle as i32 > -90 {
431                        &WNSE_PLATFORMS
432                    } else {
433                        &WSNE_PLATFORMS
434                    }
435                },
436                -44..=45 => {
437                    // primary is NorthPlatform
438                    if angle as i32 > 0 {
439                        &NEWS_PLATFORMS
440                    } else {
441                        &NWES_PLATFORMS
442                    }
443                },
444                46..=135 => {
445                    // primary is EastPlatform
446                    if angle as i32 > 90 {
447                        &ESNW_PLATFORMS
448                    } else {
449                        &ENSW_PLATFORMS
450                    }
451                },
452                136..=360 => {
453                    // primary is SouthPlatform
454                    if angle as i32 > 180 {
455                        &SWEN_PLATFORMS
456                    } else {
457                        &SEWN_PLATFORMS
458                    }
459                },
460                _ => &SEWN_PLATFORMS,
461            }
462        } else {
463            &SEWN_PLATFORMS
464        }
465    }
466
467    /// Get the direction vector that the airship would be facing when docked.
468    fn airship_dir_for_side(&self, side: AirshipDockingSide) -> Dir {
469        match self {
470            AirshipDockPlatform::NorthPlatform => match side {
471                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_x().with_z(0.0)),
472                AirshipDockingSide::Port => Dir::new(-Vec2::unit_x().with_z(0.0)),
473            },
474            AirshipDockPlatform::EastPlatform => match side {
475                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_y().with_z(0.0)),
476                AirshipDockingSide::Port => Dir::new(Vec2::unit_y().with_z(0.0)),
477            },
478            AirshipDockPlatform::SouthPlatform => match side {
479                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_x().with_z(0.0)),
480                AirshipDockingSide::Port => Dir::new(Vec2::unit_x().with_z(0.0)),
481            },
482            AirshipDockPlatform::WestPlatform => match side {
483                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_y().with_z(0.0)),
484                AirshipDockingSide::Port => Dir::new(-Vec2::unit_y().with_z(0.0)),
485            },
486        }
487    }
488}
489
490/// A node on the triangulation of the world docking sites, with
491/// data on the nodes that are connected to it.
492#[derive(Clone, Debug, Default, PartialEq)]
493pub struct DockNode {
494    /// An index into the array of all nodes in the graph.
495    pub node_id: usize,
496    /// True if the node is on the outer hull (convex hull) of the
497    /// triangulation.
498    pub on_hull: bool,
499    /// The nodes that are connected to this node.
500    pub connected: Vec<usize>,
501}
502
503impl Airships {
504    /// The duration of the ascent flight phase.
505    pub const AIRSHIP_ASCENT_DURATION: f64 = 30.0;
506    /// The duration of the descent flight phase.
507    pub const AIRSHIP_DESCENT_DURATION: f64 = 30.0;
508    /// The duration of the docked phase.
509    pub const AIRSHIP_DOCKING_DURATION: f64 = 60.0;
510    /// The time spacing between airships on the same route.
511    pub const AIRSHIP_TIME_SPACING: f64 = 240.0;
512    /// The Z offset between the docking alignment point and the AirshipDock
513    /// plot docking position.
514    const AIRSHIP_TO_DOCK_Z_OFFSET: f32 = -3.0;
515    /// The ratio of the speed used when transitioning from cruising to docking
516    /// as a percentage of the nominal airship speed.
517    pub const AIRSHIP_TRANSITION_SPEED_RATIO: f32 = 0.75;
518    /// The cruising height varies by route index and there can be only four
519    /// routes.
520    pub const CRUISE_HEIGHTS: [f32; 4] = [400.0, 475.0, 550.0, 625.0];
521    /// The distance from the docking position where the airship starts the
522    /// transition flight phase.
523    const DOCKING_TRANSITION_OFFSET: f32 = 175.0;
524    /// The vector from the dock alignment point when the airship is docked on
525    /// the port side.
526    const DOCK_ALIGN_POS_PORT: Vec2<f32> =
527        Vec2::new(Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
528    /// The vector from the dock alignment point on the airship when the airship
529    /// is docked on the starboard side.
530    const DOCK_ALIGN_POS_STARBOARD: Vec2<f32> =
531        Vec2::new(-Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
532    // TODO: These alignment offsets are specific to the airship model. If new
533    // models are added, a more generic way to determine the alignment offsets
534    // should be used.
535    /// The absolute offset from the airship's position to the docking alignment
536    /// point on the X axis. The airship is assumed to be facing positive Y.
537    const DOCK_ALIGN_X: f32 = 18.0;
538    /// The offset from the airship's position to the docking alignment point on
539    /// the Y axis. The airship is assumed to be facing positive Y.
540    /// This is positive if the docking alignment point is in front of the
541    /// airship's center position.
542    const DOCK_ALIGN_Y: f32 = 1.0;
543    /// The minimum distance from the route leg midpoint to the world
544    /// boundaries.
545    const ROUTE_LEG_MIDPOINT_MARGIN: f32 = 200.0;
546    /// The angle for calculating the route leg midpoint.
547    const ROUTE_LEG_MIDPOINT_OFFSET_RADIANS: f32 = 0.087266;
548
549    // 5 degrees
550
551    #[inline(always)]
552    pub fn docking_duration() -> f32 { Airships::AIRSHIP_DOCKING_DURATION as f32 }
553
554    /// Get all the airship docking positions from the world sites.
555    fn all_airshipdock_positions(sites: &Store<Site>) -> Vec<AirshipDockPositions> {
556        let mut dock_pos_id = 0;
557        sites
558            .iter()
559            .flat_map(|(site_id, site)| {
560                site.plots().flat_map(move |plot| {
561                    if let Some(PlotKindMeta::AirshipDock {
562                        center,
563                        docking_positions,
564                        ..
565                    }) = plot.kind().meta()
566                    {
567                        Some((center, docking_positions, site_id))
568                    } else {
569                        None
570                    }
571                })
572            })
573            .map(|(center, docking_positions, site_id)| {
574                let positions = AirshipDockPositions::from_plot_meta(
575                    dock_pos_id,
576                    center,
577                    docking_positions,
578                    site_id,
579                );
580
581                dock_pos_id += positions.docking_positions.len() as u32;
582                positions
583            })
584            .collect::<Vec<_>>()
585    }
586
587    /// Convenience function that returns the next route leg accounting for wrap
588    /// around.
589    pub fn increment_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
590        if route_index >= self.routes.len() {
591            error!("Invalid route index: {}", route_index);
592            return 0;
593        }
594        (leg_index + 1) % self.routes[route_index].legs.len()
595    }
596
597    /// Convienence function that returns the previous route leg accounting for
598    /// wrap around.
599    pub fn decrement_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
600        if route_index >= self.routes.len() {
601            error!("Invalid route index: {}", route_index);
602            return 0;
603        }
604        if leg_index > 0 {
605            leg_index - 1
606        } else {
607            self.routes[route_index].legs.len() - 1
608        }
609    }
610
611    /// Convienence function returning the number of routes.
612    pub fn route_count(&self) -> usize { self.routes.len() }
613
614    /// Safe function to get the number of AirshipDock sites on a route.
615    pub fn docking_site_count_for_route(&self, route_index: usize) -> usize {
616        if route_index >= self.routes.len() {
617            error!("Invalid route index: {}", route_index);
618            return 0;
619        }
620        self.routes[route_index].legs.len()
621    }
622
623    /// Calculate the legs for each route.
624    /// A docking platform is assigned for each leg of each route. Each route
625    /// in the route_segments argument is a series (Vec) of docking node indices
626    /// on the docking site graph. This function loops over the routes docking
627    /// nodes and assigns a docking platform based on the approach direction
628    /// to each dock node while making sure that no docking platform is used
629    /// more than once (globally, over all routes). It then calculates the
630    /// leg segments (flight phases) for each leg of each route. The output
631    /// is a Vec of up to four routes, each of which is a closed loop of
632    /// route legs.
633    fn create_route_legs(
634        &mut self,
635        route_segments: &[Vec<usize>],
636        dock_locations: &[Vec2<f32>],
637        map_size_lg: &MapSizeLg,
638    ) -> Vec<AirshipRoute> {
639        let mut incoming_edges = DHashMap::default();
640        for segment in route_segments.iter() {
641            if segment.len() < 3 {
642                continue;
643            }
644            let mut prev_node_id = segment[0];
645            segment.iter().skip(1).for_each(|&node_id| {
646                incoming_edges
647                    .entry(node_id)
648                    .or_insert_with(Vec::new)
649                    .push(prev_node_id);
650                prev_node_id = node_id;
651            });
652        }
653
654        let mut leg_platforms = DHashMap::default();
655
656        incoming_edges.iter().for_each(|(node_id, edges)| {
657            let dock_location = dock_locations[*node_id];
658            let mut used_platforms = DHashSet::default();
659            for origin in edges {
660                let origin_location = dock_locations[*origin];
661                // Determine the platform to dock using the direction from the dock location
662                // to the origin location
663                let rev_approach_dir = origin_location - dock_location;
664                let docking_platforms = AirshipDockPlatform::choices_from_dir(rev_approach_dir);
665                let docking_platform = docking_platforms
666                    .iter()
667                    .find(|&platform| !used_platforms.contains(platform))
668                    .copied()
669                    .unwrap_or(AirshipDockPlatform::NorthPlatform);
670                leg_platforms.insert((*origin, *node_id), docking_platform);
671                used_platforms.insert(docking_platform);
672            }
673        });
674
675        #[cfg(debug_assertions)]
676        {
677            debug_airships!(4, "Route segments: {:?}", route_segments);
678            debug_airships!(4, "Leg platforms: {:?}", leg_platforms);
679        }
680
681        self.nominal_speed = common::comp::ship::Body::DefaultAirship.get_speed();
682        debug_airships!(4, "Nominal speed {}", self.nominal_speed);
683
684        // The incoming edges control the docking platforms used for each leg of the
685        // route. The outgoing platform for leg i must match the incoming
686        // platform for leg i-1. For the first leg, get the 'from' platform from
687        // the last pair of nodes in the segment.
688        let mut routes = Vec::new();
689        route_segments
690            .iter()
691            .enumerate()
692            .for_each(|(route_index, segment)| {
693                assert!(
694                    segment.len() > 2,
695                    "Segments must have at least two nodes and they must wrap around."
696                );
697                let mut route_legs = Vec::new();
698                let mut route_time = 0.0;
699                let leg_start = &segment[segment.len() - 2..];
700                for leg_index in 0..segment.len() - 1 {
701                    let from_node = segment[leg_index];
702                    let to_node = segment[leg_index + 1];
703                    if leg_index == 0 {
704                        assert!(
705                            from_node == leg_start[1],
706                            "The 'previous' leg's 'to' node must match the current leg's 'from' \
707                             node."
708                        );
709                    }
710                    let to_platform = leg_platforms.get(&(from_node, to_node)).copied().unwrap_or(
711                        AirshipDockPlatform::from_dir(
712                            dock_locations[from_node] - dock_locations[to_node],
713                        ),
714                    );
715                    let dest_dock_positions = &self.airship_docks[to_node];
716                    let from_dock_positions = &self.airship_docks[from_node];
717                    let (midpoint, approach_transition_pos) = self.approach_waypoints(
718                        &from_dock_positions.center,
719                        dest_dock_positions,
720                        to_platform,
721                        map_size_lg,
722                    );
723                    let dest_dock_pos = dest_dock_positions.docking_position(to_platform).xy();
724
725                    // depature cruise (DC) from the end of the previous ascent to the leg midpoint.
726                    // The departure platform is not known here, so just use the dock center.
727                    // distance is from the departure dock center to the midpoint.
728                    // duration is distance / nominal speed
729                    let dc_dist = from_dock_positions
730                        .center
731                        .as_::<f64>()
732                        .distance(midpoint.as_());
733                    let dc_dur = dc_dist / self.nominal_speed as f64;
734                    let dc_completion_time = route_time + dc_dur;
735
736                    // approach cruise (AC) from the leg midpoint to the transition start.
737                    // distance is from the midpoint to approach_transition_pos.
738                    // duration is distance / nominal speed
739                    let ac_dist = midpoint
740                        .as_::<f64>()
741                        .distance(approach_transition_pos.as_());
742                    let ac_dur = ac_dist / self.nominal_speed as f64;
743                    let ac_completion_time = dc_completion_time + ac_dur;
744
745                    // transition (T) from approach_transition_pos to above the dock.
746                    // distance is from approach_transition_pos to the dock position.
747                    // duration is distance / (nominal speed * AIRSHIP_TRANSITION_SPEED_RATIO)
748                    let t_dist = approach_transition_pos
749                        .as_::<f64>()
750                        .distance(dest_dock_pos.as_());
751                    let t_dur = t_dist
752                        / (self.nominal_speed * Airships::AIRSHIP_TRANSITION_SPEED_RATIO) as f64;
753                    let t_completion_time = ac_completion_time + t_dur;
754
755                    // descent (D) to the docking position.
756                    // distance is 0 (no x,y movement)
757                    // duration is fixed at AIRSHIP_DESCENT_DURATION
758                    let d_completion_time = t_completion_time + Airships::AIRSHIP_DESCENT_DURATION;
759
760                    // parked/docked (P) at the dock.
761                    // distance is 0
762                    // duration is fixed at AIRSHIP_DOCKING_DURATION
763                    let p_completion_time = d_completion_time + Airships::AIRSHIP_DOCKING_DURATION;
764
765                    // ascent (A) back to cruising height above the dock.
766                    // distance is 0
767                    // duration is fixed at AIRSHIP_ASCENT_DURATION
768                    let a_completion_time = p_completion_time + Airships::AIRSHIP_ASCENT_DURATION;
769
770                    route_legs.push(AirshipRouteLeg {
771                        dest_index: to_node,
772                        platform: to_platform,
773                        segments: [
774                            RouteLegSegment {
775                                flight_phase: AirshipFlightPhase::DepartureCruise,
776                                from_world_pos: from_dock_positions.center,
777                                to_world_pos: midpoint,
778                                distance: from_dock_positions.center.distance(midpoint),
779                                duration: dc_dur as f32,
780                                route_time: dc_completion_time,
781                            },
782                            RouteLegSegment {
783                                flight_phase: AirshipFlightPhase::ApproachCruise,
784                                from_world_pos: midpoint,
785                                to_world_pos: approach_transition_pos,
786                                distance: midpoint.distance(approach_transition_pos),
787                                duration: ac_dur as f32,
788                                route_time: ac_completion_time,
789                            },
790                            RouteLegSegment {
791                                flight_phase: AirshipFlightPhase::Transition,
792                                from_world_pos: approach_transition_pos,
793                                to_world_pos: dest_dock_pos,
794                                distance: approach_transition_pos.distance(dest_dock_pos),
795                                duration: t_dur as f32,
796                                route_time: t_completion_time,
797                            },
798                            RouteLegSegment {
799                                flight_phase: AirshipFlightPhase::Descent,
800                                from_world_pos: dest_dock_pos,
801                                to_world_pos: dest_dock_pos,
802                                distance: 0.0,
803                                duration: Airships::AIRSHIP_DESCENT_DURATION as f32,
804                                route_time: d_completion_time,
805                            },
806                            RouteLegSegment {
807                                flight_phase: AirshipFlightPhase::Docked,
808                                from_world_pos: dest_dock_pos,
809                                to_world_pos: dest_dock_pos,
810                                distance: 0.0,
811                                duration: Airships::AIRSHIP_DOCKING_DURATION as f32,
812                                route_time: p_completion_time,
813                            },
814                            RouteLegSegment {
815                                flight_phase: AirshipFlightPhase::Ascent,
816                                from_world_pos: dest_dock_pos,
817                                to_world_pos: dest_dock_pos,
818                                distance: 0.0,
819                                duration: Airships::AIRSHIP_ASCENT_DURATION as f32,
820                                route_time: a_completion_time,
821                            },
822                        ],
823                    });
824                    route_time = a_completion_time;
825                }
826                let spawning_location_count =
827                    (route_time / Airships::AIRSHIP_TIME_SPACING).floor() as usize;
828                let route_sep = (route_time / spawning_location_count as f64).floor();
829                debug_airships!(
830                    4,
831                    "Route {} total_time: {}, spawning_location_count: {}, route_sep: {}",
832                    route_index,
833                    route_time,
834                    spawning_location_count,
835                    route_sep
836                );
837
838                routes.push(AirshipRoute {
839                    legs: route_legs,
840                    total_time: route_time,
841                    airship_time_spacing: route_sep,
842                    cruising_height: Airships::CRUISE_HEIGHTS
843                        [route_index % Airships::CRUISE_HEIGHTS.len()],
844                    spawning_locations: Vec::new(),
845                });
846            });
847        #[cfg(debug_assertions)]
848        {
849            routes.iter().enumerate().for_each(|(i, route)| {
850                debug_airships!(4, "Route {} legs: {}", i, route.legs.len());
851                route.legs.iter().enumerate().for_each(|(j, leg)| {
852                    debug_airships!(4, "  Leg {}: dest_index: {}", j, leg.dest_index);
853                    leg.segments.iter().enumerate().for_each(|(k, seg)| {
854                        debug_airships!(
855                            4,
856                            "route {} leg {} segment {}: phase: {:?}, from: {}, to: {}, dist: {}, \
857                             route_time: {}",
858                            i,
859                            j,
860                            k,
861                            seg.flight_phase,
862                            seg.from_world_pos,
863                            seg.to_world_pos,
864                            seg.distance,
865                            seg.route_time
866                        );
867                    });
868                });
869            });
870        }
871        routes
872    }
873
874    pub fn calculate_spawning_locations(&mut self) {
875        // Spawn airships according to route time so that they are spaced out evenly in
876        // time.
877        for (route_index, route) in self.routes.iter_mut().enumerate() {
878            let spawn_time_limit = route.total_time - route.airship_time_spacing;
879            let mut next_spawn_time = 0.0;
880            let mut prev_seg_route_time = 0.0;
881            let nominal_speed = common::comp::ship::Body::DefaultAirship.get_speed();
882            let mut spawning_locations = Vec::new();
883            // if route.legs.is_empty() || route.legs[0].segments.is_empty() {
884            //     continue;
885            // }
886            //let mut prev_leg_segment = &route.legs[route.legs.len() -
887            // 1].segments[route.legs[route.legs.len() - 1].segments.len() - 1];
888            for (leg_index, leg) in route.legs.iter().enumerate() {
889                let leg_count = route.legs.len();
890                let from_route_leg = &route.legs[(leg_index + leg_count - 1) % leg_count];
891                let dest_dock_positions = &self.airship_docks[leg.dest_index];
892                let from_dock_positions = &self.airship_docks[from_route_leg.dest_index];
893
894                for seg in leg.segments.iter() {
895                    while next_spawn_time <= seg.route_time && next_spawn_time <= spawn_time_limit {
896                        // spawn an airship on this leg segment at time next_spawn_time
897                        // The spawning location depends on the flight phase.
898                        // DepartureCruise:
899                        //     dist = (next_spawn_time - prev_leg.segments[5].route_time) *
900                        // nominal_speed     pos = (midpoint - previous leg
901                        // docking position).normalized() * dist + previous leg docking position
902                        // ApproachCruise:
903                        //     dist = (next_spawn_time - leg.segments[0].route_time) * nominal_speed
904                        //     pos = (transition_pos - midpoint).normalized() * dist + midpoint
905                        // Transition:
906                        //     dist = (next_spawn_time - leg.segments[1].route_time)
907                        //     pos = (dock_pos - transition_pos).normalized() * dist +
908                        // transition_pos Descent:
909                        //     pos = dock_pos
910                        // Docked:
911                        //     pos = dock_pos
912                        // Ascent:
913                        //     pos = dock_pos
914                        match seg.flight_phase {
915                            AirshipFlightPhase::DepartureCruise => {
916                                let dist =
917                                    (next_spawn_time - prev_seg_route_time) as f32 * nominal_speed;
918                                let dir = seg.to_world_pos - seg.from_world_pos;
919                                let pos = seg.from_world_pos
920                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
921                                spawning_locations.push(AirshipSpawningLocation {
922                                    flight_phase: seg.flight_phase,
923                                    pos,
924                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
925                                    height: route.cruising_height,
926                                    route_index,
927                                    leg_index,
928                                    spawn_route_time: next_spawn_time,
929                                });
930                                debug_airships!(
931                                    4,
932                                    "route {} leg {} DepartureCruise prev_seg_route_time: {}, \
933                                     next_spawn_time: {}, seg.route_time: {}",
934                                    route_index,
935                                    leg_index,
936                                    prev_seg_route_time,
937                                    next_spawn_time,
938                                    seg.route_time
939                                );
940                                next_spawn_time += route.airship_time_spacing;
941                            },
942                            AirshipFlightPhase::ApproachCruise => {
943                                let dist =
944                                    (next_spawn_time - prev_seg_route_time) as f32 * nominal_speed;
945                                let dir = seg.to_world_pos - seg.from_world_pos;
946                                let pos = seg.from_world_pos
947                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
948                                spawning_locations.push(AirshipSpawningLocation {
949                                    flight_phase: seg.flight_phase,
950                                    pos,
951                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
952                                    height: route.cruising_height,
953                                    route_index,
954                                    leg_index,
955                                    spawn_route_time: next_spawn_time,
956                                });
957                                debug_airships!(
958                                    4,
959                                    "route {} leg {} ApproachCruise prev_seg_route_time: {}, \
960                                     next_spawn_time: {}, seg.route_time: {}",
961                                    route_index,
962                                    leg_index,
963                                    prev_seg_route_time,
964                                    next_spawn_time,
965                                    seg.route_time
966                                );
967                                next_spawn_time += route.airship_time_spacing;
968                            },
969                            AirshipFlightPhase::Transition => {
970                                let dist = (next_spawn_time - prev_seg_route_time) as f32
971                                    * nominal_speed
972                                    * Airships::AIRSHIP_TRANSITION_SPEED_RATIO;
973                                let dir = seg.to_world_pos - seg.from_world_pos;
974                                let pos = seg.from_world_pos
975                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
976                                spawning_locations.push(AirshipSpawningLocation {
977                                    flight_phase: seg.flight_phase,
978                                    pos,
979                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
980                                    height: route.cruising_height,
981                                    route_index,
982                                    leg_index,
983                                    spawn_route_time: next_spawn_time,
984                                });
985                                debug_airships!(
986                                    4,
987                                    "route {} leg {} Transition prev_seg_route_time: {}, \
988                                     next_spawn_time: {}, seg.route_time: {}",
989                                    route_index,
990                                    leg_index,
991                                    prev_seg_route_time,
992                                    next_spawn_time,
993                                    seg.route_time
994                                );
995                                next_spawn_time += route.airship_time_spacing;
996                            },
997                            AirshipFlightPhase::Descent => {
998                                let (airship_pos, airship_direction) =
999                                    Airships::docking_position_and_dir_for_route_and_leg(
1000                                        from_dock_positions,
1001                                        dest_dock_positions,
1002                                        leg.platform,
1003                                    );
1004                                let dt = next_spawn_time - prev_seg_route_time;
1005                                let dd = route.cruising_height - airship_pos.z;
1006                                let height = airship_pos.z
1007                                    + dd * (dt / Airships::AIRSHIP_DESCENT_DURATION) as f32;
1008                                let dir = airship_direction
1009                                    .vec()
1010                                    .xy()
1011                                    .try_normalized()
1012                                    .unwrap_or(Vec2::unit_y());
1013                                spawning_locations.push(AirshipSpawningLocation {
1014                                    flight_phase: seg.flight_phase,
1015                                    pos: seg.from_world_pos,
1016                                    dir,
1017                                    height,
1018                                    route_index,
1019                                    leg_index,
1020                                    spawn_route_time: next_spawn_time,
1021                                });
1022                                debug_airships!(
1023                                    4,
1024                                    "route {} leg {} Descent prev_seg_route_time: {}, \
1025                                     next_spawn_time: {}, seg.route_time: {}",
1026                                    route_index,
1027                                    leg_index,
1028                                    prev_seg_route_time,
1029                                    next_spawn_time,
1030                                    seg.route_time
1031                                );
1032                                next_spawn_time += route.airship_time_spacing;
1033                            },
1034                            AirshipFlightPhase::Docked => {
1035                                let (airship_pos, airship_direction) =
1036                                    Airships::docking_position_and_dir_for_route_and_leg(
1037                                        from_dock_positions,
1038                                        dest_dock_positions,
1039                                        leg.platform,
1040                                    );
1041                                let dir = airship_direction
1042                                    .vec()
1043                                    .xy()
1044                                    .try_normalized()
1045                                    .unwrap_or(Vec2::unit_y());
1046                                spawning_locations.push(AirshipSpawningLocation {
1047                                    flight_phase: seg.flight_phase,
1048                                    pos: seg.from_world_pos,
1049                                    dir,
1050                                    height: airship_pos.z,
1051                                    route_index,
1052                                    leg_index,
1053                                    spawn_route_time: next_spawn_time,
1054                                });
1055                                debug_airships!(
1056                                    4,
1057                                    "route {} leg {} Docked prev_seg_route_time: {}, \
1058                                     next_spawn_time: {}, seg.route_time: {}",
1059                                    route_index,
1060                                    leg_index,
1061                                    prev_seg_route_time,
1062                                    next_spawn_time,
1063                                    seg.route_time
1064                                );
1065                                next_spawn_time += route.airship_time_spacing;
1066                            },
1067                            AirshipFlightPhase::Ascent => {
1068                                let (airship_pos, airship_direction) =
1069                                    Airships::docking_position_and_dir_for_route_and_leg(
1070                                        from_dock_positions,
1071                                        dest_dock_positions,
1072                                        leg.platform,
1073                                    );
1074                                let dt = next_spawn_time - prev_seg_route_time;
1075                                let dd = route.cruising_height - airship_pos.z;
1076                                let height = airship_pos.z
1077                                    + dd * (dt / Airships::AIRSHIP_ASCENT_DURATION) as f32;
1078                                let dir = airship_direction
1079                                    .vec()
1080                                    .xy()
1081                                    .try_normalized()
1082                                    .unwrap_or(Vec2::unit_y());
1083                                spawning_locations.push(AirshipSpawningLocation {
1084                                    flight_phase: seg.flight_phase,
1085                                    pos: seg.from_world_pos,
1086                                    dir,
1087                                    height,
1088                                    route_index,
1089                                    leg_index,
1090                                    spawn_route_time: next_spawn_time,
1091                                });
1092                                debug_airships!(
1093                                    4,
1094                                    "route {} leg {} Ascent prev_seg_route_time: {}, \
1095                                     next_spawn_time: {}, seg.route_time: {}",
1096                                    route_index,
1097                                    leg_index,
1098                                    prev_seg_route_time,
1099                                    next_spawn_time,
1100                                    seg.route_time
1101                                );
1102                                next_spawn_time += route.airship_time_spacing;
1103                            },
1104                        }
1105                    }
1106                    prev_seg_route_time = seg.route_time;
1107                }
1108            }
1109            route.spawning_locations = spawning_locations;
1110        }
1111    }
1112
1113    /// Generates the airship routes.
1114    pub fn generate_airship_routes_inner(
1115        &mut self,
1116        map_size_lg: &MapSizeLg,
1117        seed: u32,
1118        _index: Option<&Index>,
1119        _sampler: Option<&WorldSim>,
1120        _map_image_path: Option<&str>,
1121    ) {
1122        let all_dock_points = self
1123            .airship_docks
1124            .iter()
1125            .map(|dock| Point {
1126                x: dock.center.x as f64,
1127                y: dock.center.y as f64,
1128            })
1129            .collect::<Vec<_>>();
1130        debug_airships!(4, "all_dock_points: {:?}", all_dock_points);
1131
1132        // Run the delaunay triangulation on the docking points.
1133        let triangulation = triangulate(&all_dock_points);
1134
1135        #[cfg(feature = "airship_maps")]
1136        save_airship_routes_triangulation(
1137            &triangulation,
1138            &all_dock_points,
1139            map_size_lg,
1140            seed,
1141            _index,
1142            _sampler,
1143            _map_image_path,
1144        );
1145
1146        // Docking positions are specified in world coordinates, not chunks.
1147        // Limit the max route leg length to 1000 chunks no matter the world size.
1148        let blocks_per_chunk = 1 << TERRAIN_CHUNK_BLOCKS_LG;
1149        let max_route_leg_length = 1000.0 * blocks_per_chunk as f32;
1150
1151        // eulerized_route_segments is fairly expensive as the number of graph nodes
1152        // (docking sites) increases. The number of graph nodes grows linearly
1153        // with the world area and is given by the number of airship docks,
1154        // which is all_dock_points.len(). Experimentally, good results are
1155        // obtained without excessive processing time by limiting the number of
1156        // iterations based on the number of docking sites.
1157        // Docking Sites    Iterations
1158        //   35             50
1159        //   175            25
1160        //   415            5
1161        //   850            2
1162        //   1680           1
1163        // The best fit for this data is a exponential decay function of the form:
1164        // iterations = a0 * (1-alpha)^x, where x is the number of docking sites
1165        // (all_dock_points.len()). a0 = 60, alpha = 0.005450282
1166        /*
1167           R code for fitting the curve:
1168           rm(list = ls())
1169           docks <- c(35, 175, 415, 850, 1680)
1170           iter <- c(50, 25, 5, 1.5, 1)
1171           df = data.frame(docks, iter)
1172           plot(df$docks, df$iter, main="Number of Docks to Iterations", xlab="Docks", ylab="Iterations")
1173           fit <- nls(iter ~ SSasymp(docks, yf, y0, log_alpha), data = df)
1174           summary(fit)
1175           lines(df$docks, predict(fit), col="red")
1176           coefs <- coef(fit)
1177           yf <- coefs["yf"]
1178           y0 <- coefs["y0"]
1179           alpha <- exp(coefs["log_alpha"])
1180           cat("Final Value (yf):", yf, "\n")
1181           cat("Initial Value (y0):", y0, "\n")
1182           cat("Decay Rate (alpha):", alpha, "\n")
1183           tdocks <- c(0, 1, 4, 5, 9, 10, 15, 20, 25, 45, 55, 85, 100, 200, 300, 500, 1500)
1184           testfn <- function(t) {
1185               result <- y0 * (1 - alpha) ^ t
1186               return (result)
1187           }
1188           titers <- sapply(tdocks, testfn)
1189           lines(tdocks, titers, type="l", col="blue")
1190        */
1191
1192        let max_iterations = (60.0f32 * (1.0f32 - 0.005450282).powf(all_dock_points.len() as f32))
1193            .clamp(1.0, 60.0)
1194            .round() as usize;
1195
1196        if let Some((best_segments, _, _max_seg_len, _min_spread, _iteration)) = triangulation
1197            .eulerized_route_segments(
1198                &all_dock_points,
1199                max_iterations,
1200                max_route_leg_length as f64,
1201                seed,
1202            )
1203        {
1204            #[cfg(debug_assertions)]
1205            {
1206                debug_airships!(4, "Max segment length: {}", _max_seg_len);
1207                debug_airships!(4, "Min spread: {}", _min_spread);
1208                debug_airships!(4, "Iteration: {}", _iteration);
1209                debug_airships!(4, "Segments count:");
1210                let mut bidirectional_segments = Vec::new();
1211                best_segments.iter().enumerate().for_each(|segment| {
1212                    debug_airships!(4, "  {} : {}", segment.0, segment.1.len());
1213                    let seg_bidir = {
1214                        if segment.1.len() > 2 {
1215                            let slen = segment.1.len();
1216                            let mut bidir_found = false;
1217                            for index in 0..slen {
1218                                let back2 = segment.1[(index + slen - 2) % slen];
1219                                let curr = segment.1[index];
1220                                if curr == back2 {
1221                                    debug_airships!(
1222                                        4,
1223                                        "Segment {} bidir at index {}",
1224                                        segment.0,
1225                                        index
1226                                    );
1227                                    bidir_found = true;
1228                                }
1229                            }
1230                            bidir_found
1231                        } else {
1232                            false
1233                        }
1234                    };
1235                    bidirectional_segments.push(seg_bidir);
1236                });
1237                debug_airships!(4, "Best segments: {:?}", best_segments);
1238                debug_airships!(4, "Bi-dir: {:?}", bidirectional_segments);
1239                #[cfg(feature = "airship_maps")]
1240                if let Some(index) = _index
1241                    && let Some(world_sim) = _sampler
1242                    && let Err(e) = export_world_map(index, world_sim)
1243                {
1244                    eprintln!("Failed to export world map: {:?}", e);
1245                }
1246            }
1247
1248            self.routes = self.create_route_legs(
1249                &best_segments,
1250                all_dock_points
1251                    .iter()
1252                    .map(|p| Vec2::new(p.x as f32, p.y as f32))
1253                    .collect::<Vec<_>>()
1254                    .as_slice(),
1255                map_size_lg,
1256            );
1257
1258            // Calculate the spawning locations for airships on the routes.
1259            self.calculate_spawning_locations();
1260
1261            #[cfg(debug_assertions)]
1262            {
1263                self.routes.iter().enumerate().for_each(|(i, route)| {
1264                    debug_airships!(4, "Route {} spawning locations", i);
1265                    route.spawning_locations.iter().for_each(|loc| {
1266                        debug_airships!(
1267                            4,
1268                            "{} {:02} {:7.1}, {:7.1}, {} {}",
1269                            loc.route_index,
1270                            loc.leg_index,
1271                            loc.pos.x,
1272                            loc.pos.y,
1273                            loc.flight_phase,
1274                            loc.height,
1275                        );
1276                    });
1277                });
1278            }
1279
1280            #[cfg(feature = "airship_maps")]
1281            save_airship_route_segments(
1282                &self.routes,
1283                &all_dock_points,
1284                &self.airship_spawning_locations(),
1285                map_size_lg,
1286                seed,
1287                _index,
1288                _sampler,
1289                _map_image_path,
1290            );
1291        } else {
1292            eprintln!("Error - cannot eulerize the dock points.");
1293        }
1294    }
1295
1296    pub fn generate_airship_routes(&mut self, world_sim: &mut WorldSim, index: &Index) {
1297        self.airship_docks = Airships::all_airshipdock_positions(&index.sites);
1298
1299        self.generate_airship_routes_inner(
1300            &world_sim.map_size_lg(),
1301            index.seed,
1302            Some(index),
1303            Some(world_sim),
1304            None,
1305        );
1306    }
1307
1308    /// Compute the route midpoint and the transition point where the airship
1309    /// should stop the cruise flight phase and start the docking phase.
1310    /// ```text
1311    ///  F : From position
1312    ///  M : Midpoint
1313    ///  T : Transition point
1314    ///  D : Docking position
1315    ///  C : Center of the airship dock
1316    ///  X : Airship dock
1317    ///
1318    ///                            F
1319    ///                         •
1320    ///                      •
1321    ///                   M
1322    ///                  ∙
1323    ///                 ∙
1324    ///                T
1325    ///               ∙
1326    ///              ∙
1327    ///             D
1328    ///
1329    ///           XXXXX
1330    ///         XX     XX
1331    ///        X         X
1332    ///        X    C    X
1333    ///        X         X
1334    ///         XX     XX
1335    ///           XXXXX
1336    /// ```
1337    /// The midpoint is for route leg deconfliction and is where the airship
1338    /// makes a coarse correction to point at the destination. The
1339    /// transition point (T) between cruise flight and docking approach is
1340    /// on a line between the route leg midpoint (M) and the docking
1341    /// position (D), short of the docking position by
1342    /// Airships::DOCKING_TRANSITION_OFFSET blocks.
1343    ///
1344    /// # Arguments
1345    ///
1346    /// * `dock_index` - The airship dock index in airship_docks.
1347    /// * `route_index` - The index of the route (outer vector of
1348    ///   airships.routes). This is used to determine the cruise height.
1349    /// * `platform` - The platform on the airship dock where the airship is to
1350    ///   dock.
1351    /// * `from` - The position from which the airship is approaching the dock.
1352    /// # Returns
1353    /// The 2D position calculated with the Z coordinate set to the
1354    /// docking_position.z + cruise height.
1355    pub fn approach_waypoints(
1356        &self,
1357        from_dock_center: &Vec2<f32>,
1358        to_dock_positions: &AirshipDockPositions,
1359        platform: AirshipDockPlatform,
1360        map_size_lg: &MapSizeLg,
1361    ) -> (Vec2<f32>, Vec2<f32>) {
1362        // Get the route leg midpoint. This is the vector from the from_dock_position
1363        // to the to_dock_position rotated ROUTE_LEG_MID_POINT_OFFSET_RADIANS
1364        // at 1/2 the distance from the from_dock_position to the to_dock_position (so
1365        // not quite the exact midpoint but close enough).
1366        // Clamp midpoint so that it stays within the world bounds (with some margin).
1367        let blocks_per_chunk = 1 << TERRAIN_CHUNK_BLOCKS_LG;
1368        let world_blocks = map_size_lg.chunks().map(|u| u as f32) * blocks_per_chunk as f32;
1369        let midpoint = {
1370            // let from_pos = from_dock_positions.center;
1371            // let to_pos = dest_dock_positions.center;
1372            let dir = (to_dock_positions.center - from_dock_center).normalized();
1373            let mid_len = from_dock_center.distance(to_dock_positions.center) * 0.5;
1374            let mid_dir = dir.rotated_z(Airships::ROUTE_LEG_MIDPOINT_OFFSET_RADIANS);
1375            from_dock_center + mid_dir * mid_len
1376        }
1377        .clamped(
1378            Vec2::new(
1379                Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1380                Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1381            ),
1382            Vec2::new(
1383                world_blocks.x - Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1384                world_blocks.y - Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1385            ),
1386        );
1387
1388        let transition_point = {
1389            // calculate the transition point looking from the destination position back to
1390            // the midpoint.
1391            let to_dir_rev = (midpoint - to_dock_positions.center).normalized();
1392            let docking_position = to_dock_positions.docking_position(platform);
1393            docking_position.xy() + to_dir_rev * Airships::DOCKING_TRANSITION_OFFSET
1394        };
1395
1396        (midpoint, transition_point)
1397    }
1398
1399    fn vec3_relative_eq(a: &vek::Vec3<f32>, b: &vek::Vec3<f32>, epsilon: f32) -> bool {
1400        (a.x - b.x).abs() < epsilon && (a.y - b.y).abs() < epsilon && (a.z - b.z).abs() < epsilon
1401    }
1402
1403    pub fn docking_position_and_dir_for_route_and_leg(
1404        from_dock_positions: &AirshipDockPositions,
1405        to_dock_positions: &AirshipDockPositions,
1406        platform: AirshipDockPlatform,
1407    ) -> (Vec3<f32>, Dir) {
1408        let docking_side = AirshipDockingSide::from_dir_to_platform(
1409            &(to_dock_positions.center - from_dock_positions.center),
1410            &platform,
1411        );
1412
1413        // get the airship position and direction when docked
1414        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
1415            to_dock_positions.docking_position(platform),
1416            to_dock_positions.center,
1417            Some(docking_side),
1418        );
1419        (airship_pos, airship_direction)
1420    }
1421
1422    pub fn approach_for_route_and_leg(
1423        &self,
1424        route_index: usize,
1425        leg_index: usize,
1426        map_size_lg: &MapSizeLg,
1427    ) -> AirshipDockingApproach {
1428        // Get the docking positions for the route and leg.
1429        let to_route_leg = &self.routes[route_index].legs[leg_index];
1430        let leg_count = self.routes[route_index].legs.len();
1431        let from_route_leg =
1432            &self.routes[route_index].legs[(leg_index + leg_count - 1) % leg_count];
1433        let dest_dock_positions = &self.airship_docks[to_route_leg.dest_index];
1434        let from_dock_positions = &self.airship_docks[from_route_leg.dest_index];
1435
1436        let docking_side = AirshipDockingSide::from_dir_to_platform(
1437            &(dest_dock_positions.center - from_dock_positions.center),
1438            &to_route_leg.platform,
1439        );
1440
1441        // get the airship position and direction when docked
1442        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
1443            dest_dock_positions.docking_position(to_route_leg.platform),
1444            dest_dock_positions.center,
1445            Some(docking_side),
1446        );
1447
1448        // get the leg midpoint and transition point
1449        let (midpoint, approach_transition_pos) = self.approach_waypoints(
1450            &from_dock_positions.center,
1451            dest_dock_positions,
1452            to_route_leg.platform,
1453            map_size_lg,
1454        );
1455
1456        AirshipDockingApproach {
1457            airship_pos,
1458            airship_direction,
1459            dock_center: dest_dock_positions.center,
1460            height: Airships::CRUISE_HEIGHTS[route_index],
1461            midpoint,
1462            approach_transition_pos,
1463            side: docking_side,
1464            site_id: dest_dock_positions.site_id,
1465        }
1466    }
1467
1468    pub fn airship_spawning_locations(&self) -> Vec<AirshipSpawningLocation> {
1469        // collect all spawning locations from all routes
1470        self.routes
1471            .iter()
1472            .flat_map(|route| route.spawning_locations.iter())
1473            .cloned()
1474            .collect()
1475    }
1476
1477    /// Get the position a route leg originates from.
1478    pub fn route_leg_departure_location(&self, route_index: usize, leg_index: usize) -> Vec2<f32> {
1479        if route_index >= self.routes.len() || leg_index >= self.routes[route_index].legs.len() {
1480            error!("Invalid index: rt {}, leg {}", route_index, leg_index);
1481            return Vec2::zero();
1482        }
1483
1484        let prev_leg = if leg_index == 0 {
1485            &self.routes[route_index].legs[self.routes[route_index].legs.len() - 1]
1486        } else {
1487            &self.routes[route_index].legs[leg_index - 1]
1488        };
1489
1490        self.airship_docks[prev_leg.dest_index]
1491            .docking_position(prev_leg.platform)
1492            .xy()
1493    }
1494
1495    /// Get the position and direction for the airship to dock at the given
1496    /// docking position. If use_starboard_boarding is None, the side for
1497    /// boarding is randomly chosen. The center of the airship position with
1498    /// respect to the docking position is an asymmetrical offset depending on
1499    /// which side of the airship will be used for boarding and where the
1500    /// captain is located on the airship. The returned position is the
1501    /// position where the captain will be when the airship is docked
1502    /// (because the captain NPC is the one that is positioned in the agent
1503    /// or rtsim code).
1504    pub fn airship_vec_for_docking_pos(
1505        docking_pos: Vec3<f32>,
1506        airship_dock_center: Vec2<f32>,
1507        docking_side: Option<AirshipDockingSide>,
1508    ) -> (Vec3<f32>, Dir) {
1509        // choose a random side for docking if not specified
1510        let dock_side = docking_side.unwrap_or_else(|| {
1511            if rand::rng().random::<bool>() {
1512                AirshipDockingSide::Starboard
1513            } else {
1514                AirshipDockingSide::Port
1515            }
1516        });
1517        // Get the vector from the dock alignment position on the airship to the
1518        // captain's position and the rotation angle for the ship to dock on the
1519        // specified side. The dock alignment position is where the airship
1520        // should touch or come closest to the dock. The side_rotation is the
1521        // angle the ship needs to rotate from to be perpendicular to the vector
1522        // from the dock center to the docking position. For example, if the docking
1523        // position is directly north (0 degrees, or aligned with the unit_y
1524        // vector), the ship needs to rotate 90 degrees CCW to dock on the port
1525        // side or 270 degrees CCW to dock on the starboard side.
1526        let (dock_align_to_captain, side_rotation) = if dock_side == AirshipDockingSide::Starboard {
1527            (
1528                Airships::DOCK_ALIGN_POS_STARBOARD,
1529                3.0 * std::f32::consts::FRAC_PI_2,
1530            )
1531        } else {
1532            (Airships::DOCK_ALIGN_POS_PORT, std::f32::consts::FRAC_PI_2)
1533        };
1534        // get the vector from the dock center to the docking platform point where the
1535        // airship should touch or come closest to.
1536        let dock_pos_offset = (docking_pos - airship_dock_center).xy();
1537        // The airship direction when docked is the dock_pos_offset rotated by the
1538        // side_rotation angle.
1539        let airship_dir =
1540            Dir::from_unnormalized(dock_pos_offset.rotated_z(side_rotation).with_z(0.0))
1541                .unwrap_or_default();
1542        // The dock_align_to_captain vector is rotated by the angle between unit_y and
1543        // the airship direction.
1544        let ship_dock_rotation =
1545            Airships::angle_between_vectors_ccw(Vec2::unit_y(), airship_dir.vec().xy());
1546        let captain_offset = dock_align_to_captain
1547            .rotated_z(ship_dock_rotation)
1548            .with_z(Airships::AIRSHIP_TO_DOCK_Z_OFFSET);
1549
1550        // To get the location of the pilot when the ship is docked, add the
1551        // captain_offset to the docking position.
1552        (docking_pos + captain_offset, airship_dir)
1553    }
1554
1555    /// Returns the angle from vec v1 to vec v2 in the CCW direction.
1556    pub fn angle_between_vectors_ccw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1557        let dot_product = v1.dot(v2);
1558        let det = v1.x * v2.y - v1.y * v2.x; // determinant
1559        let angle = det.atan2(dot_product); // atan2(det, dot_product) gives the CCW angle
1560        if angle < 0.0 {
1561            angle + std::f32::consts::TAU
1562        } else {
1563            angle
1564        }
1565    }
1566
1567    /// Returns the angle from vec v1 to vec v2 in the CW direction.
1568    pub fn angle_between_vectors_cw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1569        let ccw_angle = Airships::angle_between_vectors_ccw(v1, v2);
1570        std::f32::consts::TAU - ccw_angle
1571    }
1572}
1573
1574fn time_is_in_cruise_phase(time: f32, cruise_segments: &[(f32, f32)]) -> bool {
1575    for seg in cruise_segments {
1576        if time >= seg.0 && time < seg.1 {
1577            return true;
1578        }
1579        if seg.1 > time {
1580            // segments are in order, so if this segment ends after the time,
1581            // no need to check further segments
1582            break;
1583        }
1584    }
1585    false
1586}
1587
1588#[cfg(debug_assertions)]
1589macro_rules! debug_airship_eulerization {
1590    ($($arg:tt)*) => {
1591        tracing::trace!($($arg)*);
1592    };
1593}
1594
1595#[cfg(not(debug_assertions))]
1596macro_rules! debug_airship_eulerization {
1597    ($($arg:tt)*) => {};
1598}
1599
1600/// A map of node index to DockNode, where DockNode contains a list of
1601/// nodes that the node is connected to.
1602type DockNodeGraph = DHashMap<usize, DockNode>;
1603
1604/// Extension functions for Triangulation (from the triangulate crate).
1605trait TriangulationExt {
1606    fn all_edges(&self) -> DHashSet<(usize, usize)>;
1607    fn is_hull_node(&self, index: usize) -> bool;
1608    fn node_connections(&self) -> DockNodeGraph;
1609    fn eulerized_route_segments(
1610        &self,
1611        all_dock_points: &[Point],
1612        iterations: usize,
1613        max_route_leg_length: f64,
1614        seed: u32,
1615    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)>;
1616}
1617
1618/// Find the first node in the graph where the DockNode has an odd number of
1619/// connections to other nodes.
1620fn first_odd_node(
1621    search_order: &[usize],
1622    start: usize,
1623    nodes: &DockNodeGraph,
1624) -> Option<(usize, usize)> {
1625    search_order
1626        .iter()
1627        .enumerate()
1628        .skip(start)
1629        .find_map(|(index, &node_index)| {
1630            if let Some(dock_node) = nodes.get(&node_index) {
1631                if dock_node.connected.len() % 2 == 1 {
1632                    Some((index, node_index))
1633                } else {
1634                    None
1635                }
1636            } else {
1637                None
1638            }
1639        })
1640}
1641
1642/// Removes an edge between two nodes in the tesselation graph.
1643fn remove_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1644    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1645        // Remove the edge from node_id1 to node_id2.
1646        // The edge may be present more than once, just remove one instance.
1647        if let Some(index) = dock_node
1648            .connected
1649            .iter()
1650            .position(|&node_id| node_id == edge.1)
1651        {
1652            dock_node.connected.remove(index);
1653        }
1654    }
1655    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1656        // Remove the edge from node_id2 to node_id1.
1657        // The edge may be present more than once, just remove one instance.
1658        if let Some(index) = dock_node
1659            .connected
1660            .iter()
1661            .position(|&node_id| node_id == edge.0)
1662        {
1663            dock_node.connected.remove(index);
1664        }
1665    }
1666}
1667
1668/// Adds an edge between two nodes in the tesselation graph.
1669fn add_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1670    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1671        dock_node.connected.push(edge.1);
1672    }
1673    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1674        dock_node.connected.push(edge.0);
1675    }
1676}
1677
1678/// Implementation of extension functions for the Triangulation struct.
1679impl TriangulationExt for Triangulation {
1680    /// Get the set of all edges in the triangulation.
1681    fn all_edges(&self) -> DHashSet<(usize, usize)> {
1682        let mut edges = DHashSet::default();
1683        for t in self.triangles.chunks(3) {
1684            let a = t[0];
1685            let b = t[1];
1686            let c = t[2];
1687            // The edges hashset must have edges specified in increasing order to avoid
1688            // duplicates.
1689            edges.insert(if a < b { (a, b) } else { (b, a) });
1690            edges.insert(if b < c { (b, c) } else { (c, b) });
1691            edges.insert(if a < c { (a, c) } else { (c, a) });
1692        }
1693        edges
1694    }
1695
1696    /// For all triangles in the tessellation, create a map of nodes to their
1697    /// connected nodes.
1698    fn node_connections(&self) -> DockNodeGraph {
1699        let mut connections = DHashMap::default();
1700
1701        self.triangles.chunks(3).for_each(|t| {
1702            for &node in t {
1703                let dock_node = connections.entry(node).or_insert_with(|| DockNode {
1704                    node_id: node,
1705                    on_hull: self.is_hull_node(node),
1706                    connected: Vec::default(),
1707                });
1708                for &connected_node in t {
1709                    if connected_node != node && !dock_node.connected.contains(&connected_node) {
1710                        dock_node.connected.push(connected_node);
1711                    }
1712                }
1713            }
1714        });
1715        for (_, dock_node) in connections.iter_mut() {
1716            dock_node.connected = dock_node.connected.to_vec();
1717        }
1718        connections
1719    }
1720
1721    /// True if the node is on the outer hull of the triangulation.
1722    fn is_hull_node(&self, index: usize) -> bool { self.hull.contains(&index) }
1723
1724    /// Calculates the best way to modify the triangulation so that
1725    /// all nodes have an even number of connections (all nodes have
1726    /// an even 'degree'). The steps are:
1727    ///
1728    /// 1. Remove very long edges (not important for eurelization, but this is a
1729    ///    goal of the airship routes design.
1730    /// 2. Remove the shortest edges from all nodes that have more than 8
1731    ///    connections to other nodes. This is because the airship docking sites
1732    ///    have at most 4 docking positions, and for deconfliction purposes, no
1733    ///    two "routes" can use the same docking position.
1734    /// 3. Add edges to the triangulation so that all nodes have an even number
1735    ///    of connections to other nodes. There are many combinations of added
1736    ///    edges that can make all nodes have an even number of connections. The
1737    ///    goal is to find a graph with the maximum number of 'routes'
1738    ///    (sub-graphs of connected nodes that form a closed loop), where the
1739    ///    routes are all the same length. Since this is a graph, the algorithm
1740    ///    is sensitive to the starting point. Several iterations are tried with
1741    ///    different starting points (node indices), and the best result is
1742    ///    returned.
1743    ///
1744    /// Returns a tuple with the following elements:
1745    ///  - best_route_segments (up to 4 routes, each route is a vector of node
1746    ///    indices)
1747    ///  - best_circuit (the full eulerian circuit)
1748    ///  - max_seg_len (the length of the longest route segment)
1749    ///  - min_spread (the standard deviation of the route segment lengths)
1750    ///  - best_iteration (for debugging, the iteration that produced the best
1751    ///    result)
1752    fn eulerized_route_segments(
1753        &self,
1754        all_dock_points: &[Point],
1755        iterations: usize,
1756        max_route_leg_length: f64,
1757        seed: u32,
1758    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
1759        let mut edges_to_remove = DHashSet::default();
1760
1761        // There can be at most four incoming and four outgoing edges per node because
1762        // there are only four docking positions per docking site and for deconfliction
1763        // purposes, no two "routes" can use the same docking position. This means that
1764        // the maximum number of edges per node is 8. Remove the shortest edges from
1765        // nodes with more than 8 edges.
1766
1767        // The tessellation algorithm produces convex hull, and there can be edges
1768        // connecting outside nodes where the distance between the points is a
1769        // significant fraction of the hull diameter. We want to keep airship
1770        // route legs as short as possible, while not removing interior edges
1771        // that may already be fairly long due to the configuration of the
1772        // docking sites relative to the entire map. For the standard world map,
1773        // with 2^10 chunks (1024x1024), the hull diameter is about 1000 chunks.
1774        // Experimentally, the standard world map can have interior edges that are
1775        // around 800 chunks long. A world map with 2^12 chunks (4096x4096) can
1776        // have hull edges that are around 2000 chunks long, but interior edges
1777        // still have a max of around 800 chunks. For the larger world maps,
1778        // removing edges that are longer than 1000 chunks is a good heuristic.
1779
1780        // First, use these heuristics to remove excess edges from the node graph.
1781        // 1. remove edges that are longer than 1000 blocks.
1782        // 2. remove the shortest edges from nodes with more than 8 edges
1783
1784        let max_distance_squared = max_route_leg_length.powi(2);
1785
1786        let all_edges = self.all_edges();
1787        for edge in all_edges.iter() {
1788            let pt1 = &all_dock_points[edge.0];
1789            let pt2 = &all_dock_points[edge.1];
1790            let v1 = Vec2 { x: pt1.x, y: pt1.y };
1791            let v2 = Vec2 { x: pt2.x, y: pt2.y };
1792            // Remove the edge if the distance between the points is greater than
1793            // max_leg_length
1794            if v1.distance_squared(v2) > max_distance_squared {
1795                edges_to_remove.insert(*edge);
1796            }
1797        }
1798
1799        #[cfg(debug_assertions)]
1800        let long_edges = edges_to_remove.len();
1801
1802        debug_airship_eulerization!(
1803            "Found {} long edges to remove out of {} total edges",
1804            edges_to_remove.len(),
1805            all_edges.len()
1806        );
1807
1808        let node_connections = self.node_connections();
1809        node_connections.iter().for_each(|(&node_id, node)| {
1810            if node.connected.len() > 8 {
1811                let excess_edges_count = node.connected.len() - 8;
1812                // Find the shortest edge and remove it
1813                let mut connected_node_info = node
1814                    .connected
1815                    .iter()
1816                    .map(|&connected_node_id| {
1817                        let pt1 = &all_dock_points[node_id];
1818                        let pt2 = &all_dock_points[connected_node_id];
1819                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1820                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1821                        (connected_node_id, v1.distance_squared(v2) as i64)
1822                    })
1823                    .collect::<Vec<_>>();
1824                connected_node_info.sort_by(|a, b| a.1.cmp(&b.1));
1825                let mut excess_edges_remaining = excess_edges_count;
1826                let mut remove_index = 0;
1827                while excess_edges_remaining > 0 && remove_index < connected_node_info.len() {
1828                    let (connected_node_id, _) = connected_node_info[remove_index];
1829                    let edge = if node_id < connected_node_id {
1830                        (node_id, connected_node_id)
1831                    } else {
1832                        (connected_node_id, node_id)
1833                    };
1834                    if !edges_to_remove.contains(&edge) {
1835                        edges_to_remove.insert(edge);
1836                        excess_edges_remaining -= 1;
1837                    }
1838                    remove_index += 1;
1839                }
1840            }
1841        });
1842
1843        let mut mutable_node_connections = node_connections.clone();
1844
1845        debug_airship_eulerization!(
1846            "Removing {} long edges and {} excess edges for a total of {} removed edges out of a \
1847             total of {} edges",
1848            long_edges,
1849            edges_to_remove.len() - long_edges,
1850            edges_to_remove.len(),
1851            all_edges.len(),
1852        );
1853
1854        for edge in edges_to_remove {
1855            remove_edge(edge, &mut mutable_node_connections);
1856        }
1857
1858        #[cfg(debug_assertions)]
1859        {
1860            // count the number of nodes with an odd connected count
1861            let odd_connected_count0 = mutable_node_connections
1862                .iter()
1863                .filter(|(_, node)| node.connected.len() % 2 == 1)
1864                .count();
1865            let total_connections1 = mutable_node_connections
1866                .iter()
1867                .map(|(_, node)| node.connected.len())
1868                .sum::<usize>();
1869            debug_airship_eulerization!(
1870                "After Removing, odd connected count: {} in {} nodes, total connections: {}",
1871                odd_connected_count0,
1872                mutable_node_connections.len(),
1873                total_connections1
1874            );
1875        }
1876
1877        // Now eurlerize the node graph by adding edges to connect nodes with an odd
1878        // number of connections. Eurlerization means that every node will have
1879        // an even number of degrees (edges), which is a requirement for
1880        // creating a Eulerian Circuit.
1881
1882        // Get the keys (node ids, which is the same as the node's index in the
1883        // all_dock_points vector) of nodes with an odd number of edges.
1884        let mut odd_keys: Vec<usize> = mutable_node_connections
1885            .iter()
1886            .filter(|(_, node)| node.connected.len() % 2 == 1)
1887            .map(|(node_id, _)| *node_id)
1888            .collect();
1889
1890        let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
1891
1892        // There will always be an even number of odd nodes in a connected graph (one
1893        // where all nodes are reachable from any other node). The goal is to
1894        // pair the odd nodes, adding edges between each pair such that the
1895        // added edges are as short as possible. After adding edges, the graph
1896        // will have an even number of edges for each node.
1897
1898        // The starting node index for finding pairs is arbitrary, and starting from
1899        // different nodes will yield different Eulerian circuits.
1900
1901        // Do a number of iterations and find the best results. The criteria is
1902        // 1. The number of route groups (the outer Vec in best_route_segments) This
1903        //    will be a maximum of 4 because there are at most 4 docking positions per
1904        //    docking site. More is better.
1905        // 2. The 'spread' of the lengths of the inner Vecs in best_route_segments. The
1906        //    calculated spread is the standard deviation of the lengths. Smaller is
1907        //    better (more uniform lengths of the route groups.)
1908        let mut best_circuit = Vec::new();
1909        let mut best_route_segments = Vec::new();
1910        let mut best_max_seg_len = 0;
1911        let mut best_min_spread = f32::MAX;
1912        let mut best_iteration = 0;
1913
1914        for i in 0..iterations {
1915            // Deterministically randomize the node order to search for the best route
1916            // segments.
1917            let mut eulerized_node_connections = mutable_node_connections.clone();
1918
1919            let mut odd_connected_count = odd_keys.len();
1920            assert!(
1921                odd_connected_count.is_multiple_of(2),
1922                "Odd connected count should be even, got {}",
1923                odd_connected_count
1924            );
1925            assert!(
1926                odd_keys.len()
1927                    == eulerized_node_connections
1928                        .iter()
1929                        .filter(|(_, node)| node.connected.len() % 2 == 1)
1930                        .count()
1931            );
1932
1933            // It's possible that the graphs starts with no odd nodes after removing edges
1934            // above.
1935            if odd_connected_count > 0 {
1936                odd_keys.shuffle(&mut rng);
1937
1938                // The edges to be added. An edge is a tuple of two node ids/indices.
1939                let mut edges_to_add = DHashSet::default();
1940                // Each odd node will be paired with only one other odd node.
1941                // Keep track of which nodes have been paired already.
1942                let mut paired_odd_nodes = DHashSet::default();
1943
1944                for node_key in odd_keys.iter() {
1945                    // Skip nodes that are already paired.
1946                    if paired_odd_nodes.contains(node_key) {
1947                        continue;
1948                    }
1949                    if let Some(node) = mutable_node_connections.get(node_key) {
1950                        // find the closest node other than nodes that are already connected to
1951                        // this one.
1952                        let mut closest_node_id = None;
1953                        let mut closest_distance = f64::MAX;
1954                        for candidate_key in odd_keys.iter() {
1955                            // Skip nodes that are already paired.
1956                            if paired_odd_nodes.contains(candidate_key) {
1957                                continue;
1958                            }
1959                            if let Some(candidate_node) =
1960                                mutable_node_connections.get(candidate_key)
1961                            {
1962                                // Skip the node itself and nodes that are already connected to this
1963                                // node.
1964                                if *candidate_key != *node_key
1965                                    && !node.connected.contains(candidate_key)
1966                                    && !candidate_node.connected.contains(node_key)
1967                                {
1968                                    // make sure the edge is specified in increasing node index
1969                                    // order to
1970                                    // avoid duplicates.
1971                                    let edge_to_add = if *node_key < *candidate_key {
1972                                        (*node_key, *candidate_key)
1973                                    } else {
1974                                        (*candidate_key, *node_key)
1975                                    };
1976                                    // Skip the edge if it is already in the edges_to_add set.
1977                                    if !edges_to_add.contains(&edge_to_add) {
1978                                        let pt1 = &all_dock_points[*node_key];
1979                                        let pt2 = &all_dock_points[*candidate_key];
1980                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1981                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1982                                        let distance = v1.distance_squared(v2);
1983                                        if distance < closest_distance {
1984                                            closest_distance = distance;
1985                                            closest_node_id = Some(*candidate_key);
1986                                        }
1987                                    }
1988                                }
1989                            }
1990                        }
1991                        // It's possible that the only odd nodes remaining are already connected to
1992                        // this node, but we still need to pair them. In
1993                        // this case, the connections become bidirectional,
1994                        // but that's okay for Eulerization and airships will still only follow each
1995                        // other in one direction.
1996                        if closest_node_id.is_none() {
1997                            // If no suitable node was found, repeat the search but allow
1998                            // connecting to nodes that are already connected to this one.
1999                            for candidate_key in odd_keys.iter() {
2000                                // Skip nodes that are already paired.
2001                                if paired_odd_nodes.contains(candidate_key) {
2002                                    continue;
2003                                }
2004                                // Skip the node itself
2005                                if *candidate_key != *node_key {
2006                                    // make sure the edge is specified in increasing node index
2007                                    // order to
2008                                    // avoid duplicates.
2009                                    let edge_to_add = if *node_key < *candidate_key {
2010                                        (*node_key, *candidate_key)
2011                                    } else {
2012                                        (*candidate_key, *node_key)
2013                                    };
2014                                    // Skip the edge if it is already in the edges_to_add set.
2015                                    if !edges_to_add.contains(&edge_to_add) {
2016                                        let pt1 = &all_dock_points[*node_key];
2017                                        let pt2 = &all_dock_points[*candidate_key];
2018                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
2019                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
2020                                        let distance = v1.distance_squared(v2);
2021                                        if distance < closest_distance {
2022                                            closest_distance = distance;
2023                                            closest_node_id = Some(*candidate_key);
2024                                        }
2025                                    }
2026                                }
2027                            }
2028                        }
2029                        // If a closest node was found that is not already paired, add the edge.
2030                        // Note that this should not fail since we are guaranteed to have
2031                        // an even number of odd nodes.
2032                        if let Some(close_node_id) = closest_node_id {
2033                            // add the edge between node_id and closest_node_id
2034                            let edge_to_add = if *node_key < close_node_id {
2035                                (*node_key, close_node_id)
2036                            } else {
2037                                (close_node_id, *node_key)
2038                            };
2039                            edges_to_add.insert(edge_to_add);
2040                            paired_odd_nodes.insert(*node_key);
2041                            paired_odd_nodes.insert(close_node_id);
2042                        } else {
2043                            error!("Cannot pair all odd nodes, this should not happen.");
2044                        }
2045                    }
2046                    if edges_to_add.len() == odd_connected_count / 2 {
2047                        // If we have paired all odd nodes, break out of the loop.
2048                        // The break is necessary because the outer loop iterates over
2049                        // all odd keys but we only need make 1/2 that many pairs of nodes.
2050                        break;
2051                    }
2052                }
2053                for edge in edges_to_add {
2054                    add_edge(edge, &mut eulerized_node_connections);
2055                }
2056                // count the number of nodes with an odd connected count
2057                odd_connected_count = eulerized_node_connections
2058                    .iter()
2059                    .filter(|(_, node)| node.connected.len() % 2 == 1)
2060                    .count();
2061
2062                #[cfg(debug_assertions)]
2063                {
2064                    let total_connections = eulerized_node_connections
2065                        .iter()
2066                        .map(|(_, node)| node.connected.len())
2067                        .sum::<usize>();
2068                    debug_airship_eulerization!(
2069                        "Outer Iteration: {}, After Adding, odd connected count: {} in {} nodes, \
2070                         total connections: {}",
2071                        i,
2072                        odd_connected_count,
2073                        eulerized_node_connections.len(),
2074                        total_connections
2075                    );
2076                }
2077            }
2078
2079            // If all nodes have an even number of edges, proceed with finding the best
2080            // Eulerian circuit for the given node configuration.
2081            if odd_connected_count == 0 {
2082                // Find the best Eulerian circuit for the current node connections
2083                if let Some((route_segments, circuit, max_seg_len, min_spread, _)) =
2084                    find_best_eulerian_circuit(&eulerized_node_connections)
2085                {
2086                    #[cfg(debug_assertions)]
2087                    {
2088                        debug_airship_eulerization!("Outer Iteration: {}", i);
2089                        debug_airship_eulerization!("Max segment length: {}", max_seg_len);
2090                        debug_airship_eulerization!("Min spread: {}", min_spread);
2091                        debug_airship_eulerization!("Segments count:");
2092                        route_segments.iter().enumerate().for_each(|segment| {
2093                            debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
2094                        });
2095                    }
2096                    // A Eulerian circuit was found, apply the goal criteria to find the best
2097                    // circuit.
2098                    if max_seg_len > best_max_seg_len
2099                        || (max_seg_len == best_max_seg_len && min_spread < best_min_spread)
2100                    {
2101                        best_circuit = circuit;
2102                        best_route_segments = route_segments;
2103                        best_max_seg_len = max_seg_len;
2104                        best_min_spread = min_spread;
2105                        best_iteration = i;
2106                    }
2107                }
2108            } else {
2109                debug_airship_eulerization!(
2110                    "Error, this should not happen: iteration {}, odd connected count: {} of {} \
2111                     nodes, total connections: {}, SKIPPING iteration",
2112                    i,
2113                    odd_connected_count,
2114                    eulerized_node_connections.len(),
2115                    eulerized_node_connections
2116                        .iter()
2117                        .map(|(_, node)| node.connected.len())
2118                        .sum::<usize>()
2119                );
2120                error!(
2121                    "Eulerian circuit not found on iteration {}. Odd connected count is not zero, \
2122                     this should not happen",
2123                    i
2124                );
2125            }
2126        }
2127        #[cfg(debug_assertions)]
2128        {
2129            debug_airship_eulerization!("Max segment length: {}", best_max_seg_len);
2130            debug_airship_eulerization!("Min spread: {}", best_min_spread);
2131            debug_airship_eulerization!("Iteration: {}", best_iteration);
2132            debug_airship_eulerization!("Segments count:");
2133            best_route_segments.iter().enumerate().for_each(|segment| {
2134                debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
2135            });
2136        }
2137
2138        if best_route_segments.is_empty() {
2139            return None;
2140        }
2141        Some((
2142            best_route_segments,
2143            best_circuit,
2144            best_max_seg_len,
2145            best_min_spread,
2146            best_iteration,
2147        ))
2148    }
2149}
2150
2151/// Find the best Eulerian circuit for the given graph of dock nodes.
2152/// Try each node as the starting point for a circuit.
2153/// The best circuit is the one with the longest routes (sub-segments
2154/// of the circuit), and where the route lengths are equal as possible.
2155fn find_best_eulerian_circuit(
2156    graph: &DockNodeGraph,
2157) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
2158    let mut best_circuit = Vec::new();
2159    let mut best_route_segments = Vec::new();
2160    let mut best_max_seg_len = 0;
2161    let mut best_min_spread = f32::MAX;
2162    let mut best_iteration = 0;
2163
2164    let graph_keys = graph.keys().copied().collect::<Vec<_>>();
2165
2166    // Repeat for each node as the starting point.
2167    for (i, &start_vertex) in graph_keys.iter().enumerate() {
2168        let mut graph = graph.clone();
2169        let mut circuit = Vec::new();
2170        let mut stack = Vec::new();
2171        let mut circuit_nodes = DHashSet::default();
2172
2173        let mut current_vertex = start_vertex;
2174
2175        // The algorithm for finding a Eulerian Circuit (Hierholzer's algorithm).
2176        while !stack.is_empty() || !graph[&current_vertex].connected.is_empty() {
2177            if graph[&current_vertex].connected.is_empty() {
2178                circuit.push(current_vertex);
2179                circuit_nodes.insert(current_vertex);
2180                current_vertex = stack.pop()?;
2181            } else {
2182                stack.push(current_vertex);
2183                if let Some(&next_vertex) = graph
2184                    .get(&current_vertex)?
2185                    .connected
2186                    .iter()
2187                    .find(|&vertex| !circuit_nodes.contains(vertex))
2188                    .or(graph.get(&current_vertex)?.connected.first())
2189                {
2190                    remove_edge((current_vertex, next_vertex), &mut graph);
2191                    current_vertex = next_vertex;
2192                } else {
2193                    return None;
2194                }
2195            }
2196        }
2197        circuit.push(current_vertex);
2198        circuit.reverse();
2199
2200        if let Some((route_segments, max_seg_len, min_spread)) =
2201            best_eulerian_circuit_segments(&graph, &circuit)
2202            && (max_seg_len > best_max_seg_len
2203                || (max_seg_len == best_max_seg_len && min_spread < best_min_spread))
2204        {
2205            best_circuit = circuit.clone();
2206            best_route_segments = route_segments;
2207            best_max_seg_len = max_seg_len;
2208            best_min_spread = min_spread;
2209            best_iteration = i;
2210        }
2211    }
2212    if best_route_segments.is_empty() {
2213        return None;
2214    }
2215    Some((
2216        best_route_segments,
2217        best_circuit,
2218        best_max_seg_len,
2219        best_min_spread,
2220        best_iteration,
2221    ))
2222}
2223
2224/// Get the optimal grouping of Eulerian Circuit nodes and edges such that a
2225/// maximum number of sub-circuits are created, and the length of each
2226/// sub-circuit is as similar as possible.
2227///
2228/// The Airship dock nodes are connected in a Eulerian Circuit, where each edge
2229/// of the tessellation is traversed exactly once. The circuit is a closed loop,
2230/// so the first and last node are the same. The single circuit can be broken
2231/// into multiple segments, each being also a closed loop. This is desirable for
2232/// airship routes, to limit the number of airships associated with each "route"
2233/// where a route is a closed circuit of docking sites. Since each edge is flown
2234/// in only one direction, the maximum number of possible closed loop segments
2235/// is equal to the maximum number of edges connected to any node, divided by 2.
2236fn best_eulerian_circuit_segments(
2237    graph: &DockNodeGraph,
2238    circuit: &[usize],
2239) -> Option<(Vec<Vec<usize>>, usize, f32)> {
2240    // get the node_connections keys, which are node ids.
2241    // Sort the nodes (node ids) by the number of connections to other nodes.
2242    let sorted_node_ids: Vec<usize> = graph
2243        .keys()
2244        .copied()
2245        .sorted_by_key(|&node_id| graph[&node_id].connected.len())
2246        .rev()
2247        .collect();
2248
2249    let mut max_segments_count = 0;
2250    let mut min_segments_len_spread = f32::MAX;
2251    let mut best_segments = Vec::new();
2252
2253    // For each node_id in the sorted node ids,
2254    // break the circuit into circular segments that start and end with that
2255    // node_id. The best set of segments is the one with the most segments and
2256    // where the length of the segments differ the least.
2257    sorted_node_ids.iter().for_each(|&node_id| {
2258        let mut segments = Vec::new();
2259        let mut current_segment = Vec::new();
2260        let circuit_len = circuit.len();
2261        let mut starting_index = usize::MAX;
2262        let mut end_index = usize::MAX;
2263        let mut prev_value = usize::MAX;
2264
2265        for (index, &value) in circuit.iter().cycle().enumerate() {
2266            if value == node_id {
2267                if starting_index == usize::MAX {
2268                    starting_index = index;
2269                    if starting_index > 0 {
2270                        end_index = index + circuit_len - 1;
2271                    } else {
2272                        end_index = index + circuit_len - 2;
2273                    }
2274                }
2275                if !current_segment.is_empty() {
2276                    current_segment.push(value);
2277                    segments.push(current_segment);
2278                    current_segment = Vec::new();
2279                }
2280            }
2281            if starting_index < usize::MAX {
2282                if value != prev_value {
2283                    current_segment.push(value);
2284                }
2285                prev_value = value;
2286            }
2287
2288            // Stop cycling once we've looped back to the value before the starting index.
2289            // There is a bug here if only checking for index == end_index, because
2290            // sorted_node_ids may contain node ids that are no longer in the
2291            // circuit. This happens for world maps where one or both dimensions
2292            // are smaller than 2^8 or maybe 2^9 chunks because the tessellation
2293            // algorithm produces a hull with fewer nodes, and the Eulerization
2294            // process may not include all nodes. To prevent an infinite loop, also break if
2295            // the index has cycled more than twice the circuit length.
2296            if index == end_index || index > 2 * circuit_len {
2297                break;
2298            }
2299        }
2300
2301        // Add the last segment
2302        if !current_segment.is_empty() {
2303            current_segment.push(node_id);
2304            segments.push(current_segment);
2305        }
2306
2307        let avg_segment_length = segments.iter().map(|segment| segment.len()).sum::<usize>() as f32
2308            / segments.len() as f32;
2309
2310        // We want similar segment lengths, so calculate the spread as the
2311        // standard deviation of the segment lengths.
2312        let seg_lengths_spread = segments
2313            .iter()
2314            .map(|segment| (segment.len() as f32 - avg_segment_length).powi(2))
2315            .sum::<f32>()
2316            .sqrt()
2317            / segments.len() as f32;
2318
2319        // First take the longest segment count, then if the segment count is the same
2320        // as the longest so far, take the one with the least length spread.
2321        if segments.len() > max_segments_count {
2322            max_segments_count = segments.len();
2323            min_segments_len_spread = seg_lengths_spread;
2324            best_segments = segments;
2325        } else if segments.len() == max_segments_count
2326            && seg_lengths_spread < min_segments_len_spread
2327        {
2328            min_segments_len_spread = seg_lengths_spread;
2329            best_segments = segments;
2330        }
2331    });
2332    if best_segments.is_empty() {
2333        return None;
2334    }
2335    Some((best_segments, max_segments_count, min_segments_len_spread))
2336}
2337
2338#[cfg(test)]
2339mod tests {
2340    use super::{AirshipDockPlatform, AirshipDockingSide, Airships, approx::assert_relative_eq};
2341    use vek::{Vec2, Vec3};
2342
2343    #[test]
2344    fn vec_angles_test() {
2345        let refvec = Vec3::new(0.0f32, 10.0, 0.0);
2346
2347        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
2348        let vec2 = Vec3::new(10.0f32, 0.0, 0.0);
2349        let vec3 = Vec3::new(0.0f32, -10.0, 0.0);
2350        let vec4 = Vec3::new(-10.0f32, 0.0, 0.0);
2351
2352        let a1r = vec1.angle_between(refvec);
2353        let a1r3 = Airships::angle_between_vectors_ccw(vec1.xy(), refvec.xy());
2354        assert!(a1r == 0.0f32);
2355        assert!(a1r3 == 0.0f32);
2356
2357        let a2r = vec2.angle_between(refvec);
2358        let a2r3 = Airships::angle_between_vectors_ccw(vec2.xy(), refvec.xy());
2359        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2360        assert_relative_eq!(a2r3, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2361
2362        let a3r: f32 = vec3.angle_between(refvec);
2363        let a3r3 = Airships::angle_between_vectors_ccw(vec3.xy(), refvec.xy());
2364        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
2365        assert_relative_eq!(a3r3, std::f32::consts::PI, epsilon = 0.00001);
2366
2367        let a4r = vec4.angle_between(refvec);
2368        let a4r3 = Airships::angle_between_vectors_ccw(vec4.xy(), refvec.xy());
2369        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2370        assert_relative_eq!(a4r3, std::f32::consts::FRAC_PI_2 * 3.0, epsilon = 0.00001);
2371    }
2372
2373    #[test]
2374    fn airship_angles_test() {
2375        let refvec = Vec2::new(0.0f32, 37.0);
2376        let ovec = Vec2::new(-4.0f32, -14.0);
2377        let oveccw0 = Vec2::new(-4, -14);
2378        let oveccw90 = Vec2::new(-14, 4);
2379        let oveccw180 = Vec2::new(4, 14);
2380        let oveccw270 = Vec2::new(14, -4);
2381        let ovecccw0 = Vec2::new(-4, -14);
2382        let ovecccw90 = Vec2::new(14, -4);
2383        let ovecccw180 = Vec2::new(4, 14);
2384        let ovecccw270 = Vec2::new(-14, 4);
2385
2386        let vec1 = Vec2::new(0.0f32, 37.0);
2387        let vec2 = Vec2::new(37.0f32, 0.0);
2388        let vec3 = Vec2::new(0.0f32, -37.0);
2389        let vec4 = Vec2::new(-37.0f32, 0.0);
2390
2391        assert!(
2392            ovec.rotated_z(Airships::angle_between_vectors_cw(vec1, refvec))
2393                .map(|x| x.round() as i32)
2394                == oveccw0
2395        );
2396        assert!(
2397            ovec.rotated_z(Airships::angle_between_vectors_cw(vec2, refvec))
2398                .map(|x| x.round() as i32)
2399                == oveccw90
2400        );
2401        assert!(
2402            ovec.rotated_z(Airships::angle_between_vectors_cw(vec3, refvec))
2403                .map(|x| x.round() as i32)
2404                == oveccw180
2405        );
2406        assert!(
2407            ovec.rotated_z(Airships::angle_between_vectors_cw(vec4, refvec))
2408                .map(|x| x.round() as i32)
2409                == oveccw270
2410        );
2411
2412        assert!(
2413            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec1, refvec))
2414                .map(|x| x.round() as i32)
2415                == ovecccw0
2416        );
2417        assert!(
2418            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec2, refvec))
2419                .map(|x| x.round() as i32)
2420                == ovecccw90
2421        );
2422        assert!(
2423            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec3, refvec))
2424                .map(|x| x.round() as i32)
2425                == ovecccw180
2426        );
2427        assert!(
2428            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec4, refvec))
2429                .map(|x| x.round() as i32)
2430                == ovecccw270
2431        );
2432    }
2433
2434    #[test]
2435    fn airship_vec_test() {
2436        {
2437            let dock_pos = Vec3::new(10.0f32, 10.0, 0.0);
2438            let airship_dock_center = Vec2::new(0.0, 0.0);
2439            let mut left_tested = false;
2440            let mut right_tested = false;
2441            {
2442                for _ in 0..1000 {
2443                    let (airship_pos, airship_dir) =
2444                        Airships::airship_vec_for_docking_pos(dock_pos, airship_dock_center, None);
2445                    if airship_pos.x > 23.0 {
2446                        assert_relative_eq!(
2447                            airship_pos,
2448                            Vec3 {
2449                                x: 23.435028,
2450                                y: 22.020815,
2451                                z: -3.0
2452                            },
2453                            epsilon = 0.00001
2454                        );
2455                        assert_relative_eq!(
2456                            airship_dir.to_vec(),
2457                            Vec3 {
2458                                x: -0.70710677,
2459                                y: 0.70710677,
2460                                z: 0.0
2461                            },
2462                            epsilon = 0.00001
2463                        );
2464                        left_tested = true;
2465                    } else {
2466                        assert_relative_eq!(
2467                            airship_pos,
2468                            Vec3 {
2469                                x: 22.020815,
2470                                y: 23.435028,
2471                                z: -3.0
2472                            },
2473                            epsilon = 0.00001
2474                        );
2475                        assert_relative_eq!(
2476                            airship_dir.to_vec(),
2477                            Vec3 {
2478                                x: 0.70710677,
2479                                y: -0.70710677,
2480                                z: 0.0
2481                            },
2482                            epsilon = 0.00001
2483                        );
2484                        right_tested = true;
2485                    }
2486                    if left_tested && right_tested {
2487                        break;
2488                    }
2489                }
2490            }
2491            {
2492                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2493                    dock_pos,
2494                    airship_dock_center,
2495                    Some(AirshipDockingSide::Port),
2496                );
2497                assert_relative_eq!(
2498                    airship_pos,
2499                    Vec3 {
2500                        x: 23.435028,
2501                        y: 22.020815,
2502                        z: -3.0
2503                    },
2504                    epsilon = 0.00001
2505                );
2506                assert_relative_eq!(
2507                    airship_dir.to_vec(),
2508                    Vec3 {
2509                        x: -0.70710677,
2510                        y: 0.70710677,
2511                        z: 0.0
2512                    },
2513                    epsilon = 0.00001
2514                );
2515            }
2516            {
2517                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2518                    dock_pos,
2519                    airship_dock_center,
2520                    Some(AirshipDockingSide::Starboard),
2521                );
2522                assert_relative_eq!(
2523                    airship_pos,
2524                    Vec3 {
2525                        x: 22.020815,
2526                        y: 23.435028,
2527                        z: -3.0
2528                    },
2529                    epsilon = 0.00001
2530                );
2531                assert_relative_eq!(
2532                    airship_dir.to_vec(),
2533                    Vec3 {
2534                        x: 0.70710677,
2535                        y: -0.70710677,
2536                        z: 0.0
2537                    },
2538                    epsilon = 0.00001
2539                );
2540            }
2541        }
2542        {
2543            let dock_pos = Vec3::new(28874.0, 18561.0, 0.0);
2544            let airship_dock_center = Vec2::new(28911.0, 18561.0);
2545            {
2546                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2547                    dock_pos,
2548                    airship_dock_center,
2549                    Some(AirshipDockingSide::Port),
2550                );
2551                assert_relative_eq!(
2552                    airship_pos,
2553                    Vec3 {
2554                        x: 28856.0,
2555                        y: 18562.0,
2556                        z: -3.0
2557                    },
2558                    epsilon = 0.00001
2559                );
2560                assert_relative_eq!(
2561                    airship_dir.to_vec(),
2562                    Vec3 {
2563                        x: 4.371139e-8,
2564                        y: -1.0,
2565                        z: 0.0
2566                    },
2567                    epsilon = 0.00001
2568                );
2569            }
2570            {
2571                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2572                    dock_pos,
2573                    airship_dock_center,
2574                    Some(AirshipDockingSide::Starboard),
2575                );
2576                assert_relative_eq!(
2577                    airship_pos,
2578                    Vec3 {
2579                        x: 28856.0,
2580                        y: 18560.0,
2581                        z: -3.0
2582                    },
2583                    epsilon = 0.00001
2584                );
2585                assert_relative_eq!(
2586                    airship_dir.to_vec(),
2587                    Vec3 {
2588                        x: -1.1924881e-8,
2589                        y: 1.0,
2590                        z: 0.0
2591                    },
2592                    epsilon = 0.00001
2593                );
2594            }
2595        }
2596    }
2597
2598    #[test]
2599    fn docking_side_for_platform_test() {
2600        // Approximately: 0, 22, 45, 67, 90, 112, 135, 157, 180, 202, 225, 247, 270,
2601        // 292, 315, 337 degrees
2602        let dirs = [
2603            Vec2::new(0.0, 100.0) - Vec2::zero(),
2604            Vec2::new(100.0, 100.0) - Vec2::zero(),
2605            Vec2::new(100.0, 0.0) - Vec2::zero(),
2606            Vec2::new(100.0, -100.0) - Vec2::zero(),
2607            Vec2::new(0.0, -100.0) - Vec2::zero(),
2608            Vec2::new(-100.0, -100.0) - Vec2::zero(),
2609            Vec2::new(-100.0, 0.0) - Vec2::zero(),
2610            Vec2::new(-100.0, 100.0) - Vec2::zero(),
2611        ];
2612        let expected = [
2613            AirshipDockingSide::Port,
2614            AirshipDockingSide::Starboard,
2615            AirshipDockingSide::Starboard,
2616            AirshipDockingSide::Starboard,
2617            AirshipDockingSide::Starboard,
2618            AirshipDockingSide::Port,
2619            AirshipDockingSide::Port,
2620            AirshipDockingSide::Port,
2621            AirshipDockingSide::Port,
2622            AirshipDockingSide::Port,
2623            AirshipDockingSide::Port,
2624            AirshipDockingSide::Starboard,
2625            AirshipDockingSide::Starboard,
2626            AirshipDockingSide::Starboard,
2627            AirshipDockingSide::Starboard,
2628            AirshipDockingSide::Port,
2629            AirshipDockingSide::Starboard,
2630            AirshipDockingSide::Port,
2631            AirshipDockingSide::Port,
2632            AirshipDockingSide::Port,
2633            AirshipDockingSide::Port,
2634            AirshipDockingSide::Starboard,
2635            AirshipDockingSide::Starboard,
2636            AirshipDockingSide::Starboard,
2637            AirshipDockingSide::Starboard,
2638            AirshipDockingSide::Starboard,
2639            AirshipDockingSide::Starboard,
2640            AirshipDockingSide::Port,
2641            AirshipDockingSide::Port,
2642            AirshipDockingSide::Port,
2643            AirshipDockingSide::Port,
2644            AirshipDockingSide::Starboard,
2645        ];
2646        for platform in [
2647            AirshipDockPlatform::NorthPlatform,
2648            AirshipDockPlatform::EastPlatform,
2649            AirshipDockPlatform::SouthPlatform,
2650            AirshipDockPlatform::WestPlatform,
2651        ]
2652        .iter()
2653        {
2654            for (i, dir) in dirs.iter().enumerate() {
2655                let side = AirshipDockingSide::from_dir_to_platform(dir, platform);
2656                assert_eq!(side, expected[*platform as usize * 8 + i]);
2657            }
2658        }
2659    }
2660}