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