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