veloren_world/civ/
airship_travel.rs

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