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