Skip to main content

veloren_world/civ/
airship_travel.rs

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