1#[cfg(feature = "airship_log")]
2use crate::rule::npc_ai::airship_logger::airship_logger;
3use crate::{
4 ai::{Action, NpcCtx, State, finish, just, now, predicate::timeout},
5 data::npc::SimulationMode,
6};
7
8use common::{
9 comp::{
10 Content,
11 agent::{BrakingMode, FlightMode},
12 compass::Direction,
13 },
14 consts::AIR_DENSITY,
15 rtsim::NpcId,
16 util::Dir,
17};
18use num_traits::cast::FromPrimitive;
19use rand::prelude::*;
20use std::{cmp::Ordering, collections::VecDeque, fmt, time::Duration};
21use vek::*;
22use world::{
23 civ::airship_travel::{AirshipDockingApproach, Airships},
24 util::CARDINALS,
25};
26
27#[cfg(debug_assertions)] use tracing::debug;
28
29use tracing::warn;
30
31const CLOSE_TO_DOCKING_SITE_DISTANCE_SQR: f32 = 225.0f32 * 225.0f32;
32const VERY_CLOSE_AIRSHIP_DISTANCE_SQR: f32 = 400.0f32 * 400.0f32;
33const CRUISE_CHECKPOINT_DISTANCE: f32 = 800.0;
34const NEXT_PILOT_CRUISE_SPEED_TOLERANCE: f32 = 2.0;
35const MOVING_AVERAGE_SCALE_FACTOR: f64 = 10000.0;
36const NEXT_PILOT_MOVING_VELOCITY_AVERAGE_CAPACITY: usize = 5;
37const NEXT_PILOT_MOVING_VELOCITY_AVERAGE_MIN_SIZE: usize = 3;
38const NEXT_PILOT_MOVING_DIST_AVERAGE_CAPACITY: usize = 3;
39const NEXT_PILOT_MOVING_DIST_AVERAGE_MIN_SIZE: usize = 2;
40const NEXT_PILOT_MOVING_DIST_TRACKER_THRESHOLD: usize = 5;
41const NEXT_PILOT_VELOCITY_RATIO_MIN: f32 = 1.05;
42const NEXT_PILOT_SPACING_THRESHOLD_SQR: f32 = 0.6 * 0.6; const CLOSE_AIRSHIP_SPEED_FACTOR: f32 = 0.9;
44
45#[derive(Debug, Copy, Clone, Default, PartialEq)]
49enum AirshipAvoidanceMode {
50 #[default]
51 None,
52 Slow(f32),
53 Hold(Vec3<f32>, Dir),
54 Stuck(Vec3<f32>),
55}
56
57#[derive(Debug, Clone)]
59struct AirshipRouteContext {
60 route_index: usize,
62 simulated_airship_speed: f32,
64 next_leg: usize,
66 next_pilot_id: NpcId,
68 current_leg_approach: Option<AirshipDockingApproach>,
70 next_leg_approach: Option<AirshipDockingApproach>,
72 next_leg_cruise_checkpoint_pos: Vec3<f32>,
75 speed_factor_conversion_factor: f32,
77
78 my_stuck_tracker: Option<StuckAirshipTracker>,
82 my_rate_tracker: Option<RateTracker>,
84 next_pilot_rate_tracker_: Option<RateTracker>,
87 avoidance_timer: Duration,
89 hold_timer: f32,
91 hold_announced: bool,
93 next_pilot_average_velocity: MovingAverage<
95 i64,
96 NEXT_PILOT_MOVING_VELOCITY_AVERAGE_CAPACITY,
97 NEXT_PILOT_MOVING_VELOCITY_AVERAGE_MIN_SIZE,
98 >,
99 next_pilot_dist_to_my_docking_pos_tracker:
102 DistanceTrendTracker<NEXT_PILOT_MOVING_DIST_TRACKER_THRESHOLD>,
103 avoid_mode: AirshipAvoidanceMode,
105 did_hold: bool,
107 extra_hold_dock_time: f32,
109}
110
111impl Default for AirshipRouteContext {
112 fn default() -> Self {
113 Self {
114 route_index: usize::MAX,
115 simulated_airship_speed: 0.0,
116 next_leg: 0,
117 next_pilot_id: NpcId::default(),
118 current_leg_approach: None,
119 next_leg_approach: None,
120 next_leg_cruise_checkpoint_pos: Vec3::default(),
121 speed_factor_conversion_factor: 0.0,
122 my_stuck_tracker: None,
123 my_rate_tracker: None,
124 next_pilot_rate_tracker_: None,
125 avoidance_timer: Duration::default(),
126 hold_timer: 0.0,
127 hold_announced: false,
128 next_pilot_average_velocity: MovingAverage::default(),
129 next_pilot_dist_to_my_docking_pos_tracker: DistanceTrendTracker::default(),
130 avoid_mode: AirshipAvoidanceMode::default(),
131 did_hold: false,
132 extra_hold_dock_time: 0.0,
133 }
134 }
135}
136
137#[derive(Debug, Default, Clone)]
140struct StuckAirshipTracker {
141 pos_history: Vec<Vec3<f32>>,
144 backout_route: Vec<Vec3<f32>>,
146}
147
148impl StuckAirshipTracker {
149 const BACKOUT_DIST: f32 = 100.0;
151 const BACKOUT_TARGET_DIST: f32 = 50.0;
154 const MAX_POS_HISTORY_SIZE: usize = 5;
156 const NEAR_GROUND_HEIGHT: f32 = 10.0;
158
159 fn add_position(&mut self, new_pos: Vec3<f32>) {
161 if self.pos_history.len() >= StuckAirshipTracker::MAX_POS_HISTORY_SIZE {
162 self.pos_history.remove(0);
163 }
164 self.pos_history.push(new_pos);
165 }
166
167 fn current_backout_pos(&mut self, ctx: &mut NpcCtx) -> Option<Vec3<f32>> {
174 if !self.backout_route.is_empty()
175 && let Some(pos) = self.backout_route.first().cloned()
176 {
177 if ctx.npc.wpos.distance_squared(pos) < StuckAirshipTracker::BACKOUT_TARGET_DIST.powi(2)
178 {
179 self.backout_route.remove(0);
180 }
181 Some(pos)
182 } else {
183 self.pos_history.clear();
184 None
185 }
186 }
187
188 fn is_stuck(
202 &mut self,
203 ctx: &mut NpcCtx,
204 current_pos: &Vec3<f32>,
205 target_pos: &Vec2<f32>,
206 ) -> bool {
207 self.add_position(*current_pos);
208 if self.pos_history.len() == StuckAirshipTracker::MAX_POS_HISTORY_SIZE
210 && self.backout_route.is_empty()
211 {
212 if let Some(last_pos) = self.pos_history.last() {
213 if self
215 .pos_history
216 .iter()
217 .all(|pos| pos.distance_squared(*last_pos) < 10.0)
218 {
219 if let Some(backout_dir) = (ctx.npc.wpos.xy() - target_pos)
224 .with_z(0.0)
225 .try_normalized()
226 {
227 let ground = ctx
228 .world
229 .sim()
230 .get_surface_alt_approx(last_pos.xy().map(|e| e as i32));
231 let mut backout_pos =
234 ctx.npc.wpos + backout_dir * StuckAirshipTracker::BACKOUT_DIST;
235 if (ctx.npc.wpos.z - ground).abs() < StuckAirshipTracker::NEAR_GROUND_HEIGHT
237 {
238 backout_pos.z += 50.0;
239 }
240 self.backout_route =
241 vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
242 #[cfg(debug_assertions)]
244 debug!(
245 "Airship {} Stuck! at {} {} {}, backout_dir:{:?}, backout_pos:{:?}",
246 format!("{:?}", ctx.npc_id),
247 ctx.npc.wpos.x,
248 ctx.npc.wpos.y,
249 ctx.npc.wpos.z,
250 backout_dir,
251 backout_pos
252 );
253 }
254 }
255 }
256 }
257 !self.backout_route.is_empty()
258 }
259}
260
261#[derive(Debug, Clone, Default)]
262struct RateTracker {
265 last_pos: Option<Vec2<f32>>,
267 last_time: f32,
269}
270
271impl RateTracker {
272 fn update(&mut self, current_pos: Vec2<f32>, time: f32) -> f32 {
275 let rate = if let Some(last_pos) = self.last_pos {
276 let dt = time - self.last_time;
277 if dt > 0.0 {
278 current_pos.distance(last_pos) / dt } else {
280 0.0
281 }
282 } else {
283 0.0
284 };
285 self.last_pos = Some(current_pos);
286 self.last_time = time;
287 rate
288 }
289}
290
291#[derive(Debug, PartialEq)]
292enum DistanceTrend {
295 Towards,
297 Away,
299 Neutral,
301}
302
303#[derive(Debug, Default, Clone)]
307struct DistanceTrendTracker<const C: usize> {
308 fixed_pos: Vec2<f32>,
310 avg_rate: MovingAverage<
312 f64,
313 NEXT_PILOT_MOVING_DIST_AVERAGE_CAPACITY,
314 NEXT_PILOT_MOVING_DIST_AVERAGE_MIN_SIZE,
315 >,
316 prev_dist: Option<f32>,
318 prev_time: f64,
320}
321
322impl<const C: usize> DistanceTrendTracker<C> {
323 fn update(&mut self, pos: Vec2<f32>, time: f64) -> Option<DistanceTrend> {
326 let current_dist = pos.distance(self.fixed_pos);
327 if let Some(prev) = self.prev_dist {
328 if time - self.prev_time > f64::EPSILON {
333 let rate = (current_dist - prev) as f64 / (time - self.prev_time);
334 self.prev_dist = Some(current_dist);
335 self.prev_time = time;
336
337 self.avg_rate.add(rate);
338 if let Some(avg) = self.avg_rate.average() {
339 if avg > C as f64 {
340 Some(DistanceTrend::Away)
341 } else if avg < -(C as f64) {
342 Some(DistanceTrend::Towards)
343 } else {
344 Some(DistanceTrend::Neutral)
345 }
346 } else {
347 None
348 }
349 } else {
350 None
352 }
353 } else {
354 self.prev_dist = Some(current_dist);
355 self.prev_time = time;
356 None
357 }
358 }
359
360 fn reset(&mut self, pos: Vec2<f32>) {
361 self.fixed_pos = pos;
362 self.avg_rate.reset();
363 self.prev_dist = None;
364 }
365}
366
367#[derive(Clone, Debug)]
369struct MovingAverage<T, const S: usize, const N: usize>
370where
371 T: Default
372 + FromPrimitive
373 + std::ops::AddAssign
374 + std::ops::Sub<Output = T>
375 + std::ops::Div<Output = T>
376 + Copy,
377{
378 values: VecDeque<T>,
379 sum: T,
380}
381
382impl<T, const S: usize, const N: usize> MovingAverage<T, S, N>
383where
384 T: Default
385 + FromPrimitive
386 + std::ops::AddAssign
387 + std::ops::Sub<Output = T>
388 + std::ops::Div<Output = T>
389 + Copy,
390{
391 fn add(&mut self, value: T) {
394 if self.values.len() == S {
395 if let Some(old_value) = self.values.pop_front() {
396 self.sum = self.sum - old_value;
397 }
398 }
399 self.values.push_back(value);
400 self.sum += value;
401 }
402
403 fn average(&self) -> Option<T> {
405 if self.values.len() < N {
406 None
407 } else {
408 Some(self.sum / T::from_u32(self.values.len() as u32).unwrap())
409 }
410 }
411
412 fn reset(&mut self) {
413 self.values.clear();
414 self.sum = T::from_u32(0).unwrap();
415 }
416}
417
418impl<T, const S: usize, const N: usize> Default for MovingAverage<T, S, N>
419where
420 T: Default
421 + FromPrimitive
422 + std::ops::AddAssign
423 + std::ops::Sub<Output = T>
424 + std::ops::Div<Output = T>
425 + Copy,
426{
427 fn default() -> Self {
428 Self {
429 values: VecDeque::with_capacity(S),
430 sum: T::from_u32(0).unwrap(),
431 }
432 }
433}
434
435#[derive(Debug, Copy, Clone, PartialEq, Default)]
437pub enum AirshipFlightPhase {
438 Ascent,
439 DepartureCruise,
440 ApproachCruise,
441 Transition,
442 #[default]
443 Docked,
444}
445
446impl fmt::Display for AirshipFlightPhase {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 match self {
449 AirshipFlightPhase::Ascent => write!(f, "Ascent"),
450 AirshipFlightPhase::DepartureCruise => write!(f, "DepartureCruise"),
451 AirshipFlightPhase::ApproachCruise => write!(f, "ApproachCruise"),
452 AirshipFlightPhase::Transition => write!(f, "Transition"),
453 AirshipFlightPhase::Docked => write!(f, "Docked"),
454 }
455 }
456}
457
458fn fly_airship(
461 phase: AirshipFlightPhase,
462 wpos: Vec3<f32>,
463 goal_dist: f32,
464 initial_speed_factor: f32,
465 height_offset: f32,
466 with_terrain_following: bool,
467 direction_override: Option<Dir>,
468 flight_mode: FlightMode,
469 with_collision_avoidance: bool,
470 radar_interval: Duration,
471) -> impl Action<AirshipRouteContext> {
472 now(move |_, airship_context: &mut AirshipRouteContext| {
473 airship_context.avoidance_timer = radar_interval;
474 airship_context.avoid_mode = AirshipAvoidanceMode::None;
475 if matches!(phase, AirshipFlightPhase::DepartureCruise) {
476 airship_context.my_stuck_tracker = Some(StuckAirshipTracker::default());
478 }
479 fly_airship_inner(
480 phase,
481 wpos,
482 goal_dist,
483 initial_speed_factor,
484 height_offset,
485 with_terrain_following,
486 direction_override,
487 flight_mode,
488 with_collision_avoidance,
489 radar_interval,
490 )
491 })
492}
493
494fn should_hold(
498 my_pilot_distance_to_next_pilot: f32,
499 next_pilot_dist_to_docking_target: f32,
500 next_pilot_to_docking_target_trend: &DistanceTrend,
501) -> bool {
502 *next_pilot_to_docking_target_trend == DistanceTrend::Towards
503 && next_pilot_dist_to_docking_target < CLOSE_TO_DOCKING_SITE_DISTANCE_SQR
504 && my_pilot_distance_to_next_pilot < VERY_CLOSE_AIRSHIP_DISTANCE_SQR
505}
506
507fn should_slow_down(
516 my_pilot_pos: &Vec2<f32>,
517 my_pilot_velocity: f32,
518 my_pilot_dist_to_docking_target: f32,
519 next_pilot_pos: &Vec2<f32>,
520 next_pilot_velocity: f32,
521 next_pilot_dist_to_docking_target: f32,
522 next_pilot_to_docking_target_trend: &DistanceTrend,
523) -> bool {
524 *next_pilot_to_docking_target_trend == DistanceTrend::Towards
525 && next_pilot_velocity / my_pilot_velocity < NEXT_PILOT_VELOCITY_RATIO_MIN
526 && my_pilot_pos.distance_squared(*next_pilot_pos)
527 < Airships::AIRSHIP_SPACING.powi(2) * NEXT_PILOT_SPACING_THRESHOLD_SQR
528 && my_pilot_dist_to_docking_target > next_pilot_dist_to_docking_target
529}
530
531fn fly_inner_default_goto(
535 ctx: &mut NpcCtx,
536 wpos: Vec3<f32>,
537 speed_factor: f32,
538 height_offset: f32,
539 with_terrain_following: bool,
540 direction_override: Option<Dir>,
541 flight_mode: FlightMode,
542) {
543 let height_offset_opt = if with_terrain_following {
544 Some(height_offset)
545 } else {
546 None
547 };
548 ctx.controller.do_goto_with_height_and_dir(
549 wpos,
550 speed_factor,
551 height_offset_opt,
552 direction_override,
553 flight_mode,
554 );
555}
556
557fn fly_airship_inner(
559 phase: AirshipFlightPhase,
560 wpos: Vec3<f32>,
561 goal_dist: f32,
562 initial_speed_factor: f32,
563 height_offset: f32,
564 with_terrain_following: bool,
565 direction_override: Option<Dir>,
566 flight_mode: FlightMode,
567 with_collision_avoidance: bool,
568 radar_interval: Duration,
569) -> impl Action<AirshipRouteContext> {
570 just(
571 move |ctx, airship_context: &mut AirshipRouteContext| {
572
573 let (current_approach, pos_tracker_target_loc) =
578 match phase {
579 AirshipFlightPhase::DepartureCruise => (&airship_context.next_leg_approach.unwrap(), airship_context.next_leg_cruise_checkpoint_pos.xy()),
580 _ => (&airship_context.current_leg_approach.unwrap(), airship_context.current_leg_approach.unwrap().approach_transition_pos.xy()),
581 };
582
583 let remaining = airship_context
587 .avoidance_timer
588 .checked_sub(Duration::from_secs_f32(ctx.dt));
589 if remaining.is_none() {
591 airship_context.avoidance_timer = radar_interval;
593
594 #[cfg(feature = "airship_log")]
595 log_airship_position(ctx, &phase);
597
598 if with_collision_avoidance
600 && matches!(phase, AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise)
601 {
602 let mypos = ctx.npc.wpos;
612 let my_velocity = if let Some(my_rate_tracker) = &mut airship_context.my_rate_tracker {
613 my_rate_tracker.update(mypos.xy(), ctx.time.0 as f32)
614 } else {
615 0.0
616 };
617 let my_distance_to_docking_target = mypos.xy().distance_squared(current_approach.airship_pos.xy());
618
619 let (next_pilot_wpos, next_pilot_dist_trend) = if let Some(next_pilot_rate_tracker) = &mut airship_context.next_pilot_rate_tracker_
620 && let Some(next_pilot) = ctx.state.data().npcs.get(airship_context.next_pilot_id)
621 {
622 let next_rate = next_pilot_rate_tracker.update(next_pilot.wpos.xy(), ctx.time.0 as f32);
623
624 let next_dist_trend = airship_context
626 .next_pilot_dist_to_my_docking_pos_tracker.update(next_pilot.wpos.xy(), ctx.time.0)
627 .unwrap_or(DistanceTrend::Away);
628
629 if next_rate > airship_context.simulated_airship_speed - NEXT_PILOT_CRUISE_SPEED_TOLERANCE {
633 airship_context.next_pilot_average_velocity.add((next_rate as f64 * MOVING_AVERAGE_SCALE_FACTOR) as i64);
635 }
636
637 (next_pilot.wpos, next_dist_trend)
638 } else {
639 (Vec3::zero(), DistanceTrend::Away)
640 };
641
642 let my_pilot_distance_to_next_pilot = mypos.xy().distance_squared(next_pilot_wpos.xy());
643 let next_pilot_distance_to_docking_target = next_pilot_wpos.xy().distance_squared(current_approach.airship_pos.xy());
644
645 let avoidance = match airship_context.avoid_mode {
647 AirshipAvoidanceMode::Stuck(..) => {
648 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
652 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
653 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
654 {
655 AirshipAvoidanceMode::Stuck(backout_pos)
656 } else {
657 #[cfg(debug_assertions)]
658 debug!("{:?} unstuck at pos: {} {}",
659 ctx.npc_id,
660 mypos.x as i32, mypos.y as i32,
661 );
662
663 AirshipAvoidanceMode::None
664 }
665 }
666 AirshipAvoidanceMode::Hold(..) => {
667 if next_pilot_dist_trend != DistanceTrend::Away {
671 airship_context.avoid_mode
672 } else {
673 AirshipAvoidanceMode::None
674 }
675 }
676 AirshipAvoidanceMode::Slow(..) => {
677 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
680 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
681 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
682 {
683 AirshipAvoidanceMode::Stuck(backout_pos)
684 } else if should_hold(
685 my_pilot_distance_to_next_pilot,
686 next_pilot_distance_to_docking_target,
687 &next_pilot_dist_trend,
688 ) {
689 airship_context.did_hold = true;
690 AirshipAvoidanceMode::Hold(
691 mypos,
692 Dir::from_unnormalized((current_approach.dock_center - mypos.xy()).with_z(0.0))
694 .unwrap_or_default(),
695 )
696 } else {
697 if next_pilot_dist_trend == DistanceTrend::Away || my_pilot_distance_to_next_pilot > Airships::AIRSHIP_SPACING.powi(2) {
707 AirshipAvoidanceMode::None
708 } else {
709 airship_context.avoid_mode
711 }
712 }
713 }
714 AirshipAvoidanceMode::None => {
715 let next_pilot_avg_velocity = (airship_context.next_pilot_average_velocity.average().unwrap_or(0) as f64 / MOVING_AVERAGE_SCALE_FACTOR) as f32;
717 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
719 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
720 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
721 {
722 #[cfg(debug_assertions)]
723 debug!("{:?} stuck at pos: {} {}",
724 ctx.npc_id,
725 mypos.x as i32, mypos.y as i32,
726 );
727 AirshipAvoidanceMode::Stuck(backout_pos)
728 } else if should_hold(
730 my_pilot_distance_to_next_pilot,
731 next_pilot_distance_to_docking_target,
732 &next_pilot_dist_trend,
733 ) {
734 airship_context.did_hold = true;
735 AirshipAvoidanceMode::Hold(
736 mypos,
737 Dir::from_unnormalized((current_approach.dock_center - mypos.xy()).with_z(0.0))
738 .unwrap_or_default(),
739 )
740 } else if should_slow_down(
742 &mypos.xy(),
743 my_velocity,
744 my_distance_to_docking_target,
745 &next_pilot_wpos.xy(),
746 next_pilot_avg_velocity,
747 next_pilot_distance_to_docking_target,
748 &next_pilot_dist_trend
749 ) {
750 let mut new_speed_factor = if matches!(ctx.npc.mode, SimulationMode::Simulated) {
754 CLOSE_AIRSHIP_SPEED_FACTOR
757 } else {
758 let target_velocity: f32 = next_pilot_avg_velocity * CLOSE_AIRSHIP_SPEED_FACTOR;
761
762 let new_sf = airship_context.speed_factor_conversion_factor * target_velocity.powi(2);
775 if new_sf > 0.0 {
776 #[cfg(debug_assertions)]
777 debug!(
778 "Pilot {:?}: Adjusting speed factor to {}, next pilot avg velocity: {}, * speed_factor = {}",
779 ctx.npc_id,
780 new_sf,
781 next_pilot_avg_velocity,
782 target_velocity,
783 );
784 }
785 new_sf
786 };
787
788 if new_speed_factor < 0.05 {
789 warn!("Pilot {:?} calculated slow down speed factor is too low, clamping to 0.05", ctx.npc_id);
790 new_speed_factor = 0.05;
791 }
792 AirshipAvoidanceMode::Slow(
793 new_speed_factor
794 )
795 } else {
796 AirshipAvoidanceMode::None
797 }
798 }
799 };
800
801 if avoidance != airship_context.avoid_mode {
803 airship_context.avoid_mode = avoidance;
804 }
805 }
806 } else {
807 airship_context.avoidance_timer = remaining.unwrap_or(radar_interval);
809 }
810
811 match airship_context.avoid_mode {
813 AirshipAvoidanceMode::Stuck(backout_pos) => {
814 ctx.controller.do_goto_with_height_and_dir(
816 backout_pos,
817 1.5,
818 None,
819 None,
820 FlightMode::Braking(BrakingMode::Normal),
821 );
822 }
823 AirshipAvoidanceMode::Hold(hold_pos, hold_dir) => {
824 airship_context.hold_timer -= ctx.dt;
826 if airship_context.hold_timer <= 0.0 {
827 if !airship_context.hold_announced {
828 airship_context.hold_announced = true;
829 ctx.controller
830 .say(None, Content::localized("npc-speech-pilot-announce_hold"));
831 } else {
832 ctx.controller
833 .say(None, Content::localized("npc-speech-pilot-continue_hold"));
834 }
835 airship_context.hold_timer = ctx.rng.gen_range(10.0..20.0);
836 }
837 let hold_pos = if matches!(ctx.npc.mode, SimulationMode::Simulated) {
840 hold_pos
841 } else {
842 hold_pos
846 + (Vec3::new(0.7, 0.8, 0.9).map(|e| (e * ctx.time.0).sin())
847 * Vec3::new(5.0, 5.0, 10.0))
848 .map(|e| e as f32)
849 };
850
851 ctx.controller.do_goto_with_height_and_dir(
853 hold_pos,
854 0.25,
855 None,
856 Some(hold_dir),
857 FlightMode::Braking(BrakingMode::Normal),
858 );
859 }
860 AirshipAvoidanceMode::Slow(speed_factor) => {
861 fly_inner_default_goto(ctx, wpos, speed_factor, height_offset,
863 with_terrain_following, direction_override, flight_mode,
864 );
865 }
866 AirshipAvoidanceMode::None => {
867 fly_inner_default_goto(ctx, wpos, initial_speed_factor, height_offset,
869 with_terrain_following, direction_override, flight_mode,
870 );
871 }
872 }
873 },
874 )
875 .repeat()
876 .boxed()
877 .stop_if(move |ctx: &mut NpcCtx| {
878 if flight_mode == FlightMode::FlyThrough {
879 ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
881 } else {
882 ctx.npc.wpos.distance_squared(wpos) < goal_dist.powi(2)
884 }
885 })
886 .debug(move || {
887 format!(
888 "fly airship, phase:{:?}, tgt pos:({}, {}, {}), goal dist:{}, speed:{}, height:{}, \
889 terrain following:{}, FlightMode:{:?}, collision avoidance:{}, radar interval:{}",
890 phase,
891 wpos.x,
892 wpos.y,
893 wpos.z,
894 goal_dist,
895 initial_speed_factor,
896 height_offset,
897 with_terrain_following,
898 flight_mode,
899 with_collision_avoidance,
900 radar_interval.as_secs_f32()
901 )
902 })
903 .map(|_, _| ())
904}
905
906fn approach_target_pos(
914 ctx: &mut NpcCtx,
915 approach_pos: Vec2<f32>,
916 default_alt: f32,
917 height: f32,
918) -> Vec3<f32> {
919 let max_alt = CARDINALS
921 .iter()
922 .map(|rpos| {
923 ctx.world
924 .sim()
925 .get_surface_alt_approx(approach_pos.as_() + rpos * 200)
926 })
927 .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
928 .unwrap_or(default_alt);
929 approach_pos.with_z(max_alt + height)
930}
931
932pub fn pilot_airship<S: State>() -> impl Action<S> {
937 now(move |ctx, route_context: &mut AirshipRouteContext| {
938 if let Some((route_index, start_leg_index)) = ctx.state.data().airship_sim.assigned_routes.get(&ctx.npc_id)
940 {
941 if route_context.route_index == usize::MAX {
945 route_context.route_index = *route_index;
948 route_context.next_leg = *start_leg_index;
949 if let Some (next_pilot) = ctx.state.data().airship_sim.next_pilot(*route_index, ctx.npc_id) {
950 route_context.next_pilot_id = next_pilot;
951 } else {
952 route_context.next_pilot_id = NpcId::default();
953 }
954 let (max_speed, reference_area, thrust) =
955 ctx.state.data().npcs.mounts.get_mount_link(ctx.npc_id)
956 .map(|mount_link|
957 ctx.state.data().npcs.get(mount_link.mount)
958 .map(|airship| {
959 (airship.body.max_speed_approx(),
960 airship.body.parasite_drag(1.0),
961 airship.body.fly_thrust().unwrap_or_default())
962 })
963 .unwrap_or_default()
964 ).or({
965 Some((0.0, 1.0, 1.0))
967 })
968 .unwrap_or((0.0, 1.0, 1.0));
969 route_context.simulated_airship_speed = max_speed;
970 route_context.speed_factor_conversion_factor = 0.45 * AIR_DENSITY * reference_area / thrust;
971 route_context.my_rate_tracker = Some(RateTracker::default());
972 route_context.next_pilot_rate_tracker_ = Some(RateTracker::default());
973
974 #[cfg(debug_assertions)]
975 {
976 let current_approach = ctx.world.civs().airships.approach_for_route_and_leg(
977 route_context.route_index,
978 route_context.next_leg,
979 );
980 debug!(
981 "Server startup, airship pilot {:?} starting on route {} and leg {}, target dock: {} {}, following pilot {:?}",
982 ctx.npc_id,
983 route_context.route_index,
984 route_context.next_leg,
985 current_approach.airship_pos.x as i32, current_approach.airship_pos.y as i32,
986 route_context.next_pilot_id
987 );
988 }
989 if route_context.next_pilot_id == NpcId::default() {
990 tracing::error!("Pilot {:?} has no next pilot to follow.", ctx.npc_id);
991 }
992 }
993
994 route_context.current_leg_approach = Some(ctx.world.civs().airships.approach_for_route_and_leg(
999 route_context.route_index,
1000 route_context.next_leg,
1001 ));
1002 route_context.next_leg = ctx.world.civs().airships.increment_route_leg(
1004 route_context.route_index,
1005 route_context.next_leg,
1006 );
1007 route_context.next_leg_approach = Some(ctx.world.civs().airships.approach_for_route_and_leg(
1010 route_context.route_index,
1011 route_context.next_leg,
1012 ));
1013
1014 if route_context.current_leg_approach.is_none() ||
1015 route_context.next_leg_approach.is_none()
1016 {
1017 tracing::error!(
1018 "Airship pilot {:?} approaches not found for route {} leg {}, stopping pilot_airship loop.",
1019 ctx.npc_id,
1020 route_context.route_index,
1021 route_context.next_leg
1022 );
1023 return finish().map(|_, _| ()).boxed();
1024 }
1025
1026 let current_approach = route_context.current_leg_approach.unwrap();
1028 let next_approach = route_context.next_leg_approach.unwrap();
1029
1030 let next_leg_cruise_dir = (next_approach.approach_transition_pos.xy() - current_approach.airship_pos.xy()).normalized();
1031 route_context.next_leg_cruise_checkpoint_pos = (next_approach.approach_transition_pos - next_leg_cruise_dir * CRUISE_CHECKPOINT_DISTANCE).with_z(next_approach.approach_transition_pos.z);
1032 route_context.next_leg_cruise_checkpoint_pos = approach_target_pos(ctx, route_context.next_leg_cruise_checkpoint_pos.xy(), route_context.next_leg_cruise_checkpoint_pos.z, next_approach.height);
1036
1037 route_context.next_pilot_dist_to_my_docking_pos_tracker.reset(current_approach.airship_pos.xy());
1039
1040 let speed_factor_fn = |sim_mode: SimulationMode, speed_factor: f32| {
1043 if matches!(sim_mode, SimulationMode::Simulated) {
1045 speed_factor.powf(0.3)
1046 } else {
1047 speed_factor
1048 }
1049 };
1050
1051 fly_airship(
1060 AirshipFlightPhase::ApproachCruise,
1061 current_approach.approach_transition_pos,
1062 50.0,
1063 speed_factor_fn(ctx.npc.mode, 1.0),
1064 current_approach.height,
1065 true,
1066 None,
1067 FlightMode::FlyThrough,
1068 true,
1069 Duration::from_secs_f32(1.0),
1070 )
1071 .then(
1072 fly_airship(
1074 AirshipFlightPhase::Transition,
1075 current_approach.airship_pos + Vec3::unit_z() * current_approach.height,
1076 20.0,
1077 speed_factor_fn(ctx.npc.mode, 0.4),
1078 current_approach.height,
1079 true,
1080 Some(current_approach.airship_direction),
1081 FlightMode::Braking(BrakingMode::Normal),
1082 false,
1083 Duration::from_secs_f32(2.0),
1084 ))
1085 .then(
1095 just(|ctx: &mut NpcCtx, _| {
1096 log_airship_position(ctx, &AirshipFlightPhase::Transition);
1097 }))
1098
1099 .then(
1100 just(move |ctx, _| {
1102 ctx.controller
1103 .do_goto_with_height_and_dir(
1104 current_approach.airship_pos + Vec3::unit_z() * 125.0,
1105 speed_factor_fn(ctx.npc.mode, 0.9),
1106 None,
1107 Some(current_approach.airship_direction),
1108 FlightMode::Braking(BrakingMode::Normal),
1109 );
1110 })
1111 .repeat()
1112 .stop_if(timeout(ctx.rng.gen_range(12.5..16.0) * (current_approach.height as f64 / Airships::CRUISE_HEIGHTS[0] as f64) * 1.3)))
1113 .then(
1114 just(|ctx: &mut NpcCtx, _| {
1115 log_airship_position(ctx, &AirshipFlightPhase::Transition);
1116 }))
1117 .then(
1118 just(move |ctx: &mut NpcCtx, _| {
1120 ctx.controller
1121 .do_goto_with_height_and_dir(
1122 current_approach.airship_pos + Vec3::unit_z() * 20.0,
1123 speed_factor_fn(ctx.npc.mode, 0.9),
1124 None,
1125 Some(current_approach.airship_direction),
1126 FlightMode::Braking(BrakingMode::Precise),
1127 );
1128 })
1129 .repeat()
1130 .stop_if(timeout(ctx.rng.gen_range(6.5..8.0))))
1131 .then(just(|ctx: &mut NpcCtx, _| {
1133 log_airship_position(ctx, &AirshipFlightPhase::Docked);
1134 ctx.controller
1135 .say(None, Content::localized("npc-speech-pilot-landed"));
1136 }))
1137
1138 .then(
1140 now(move |ctx, route_context:&mut AirshipRouteContext| {
1141 if route_context.did_hold {
1149 route_context.extra_hold_dock_time += 30.0;
1150 } else if route_context.extra_hold_dock_time > 45.0 {
1151 route_context.extra_hold_dock_time -= 45.0;
1152 } else {
1153 route_context.extra_hold_dock_time = 0.0;
1154 }
1155
1156 let docking_time = route_context.extra_hold_dock_time + Airships::docking_duration();
1157 #[cfg(debug_assertions)]
1158 {
1159 if route_context.did_hold || docking_time > Airships::docking_duration() {
1160 let docked_site_name = ctx.index.sites.get(current_approach.site_id).name().to_string();
1161 debug!("{}, Docked at {}, did_hold:{}, extra_hold_dock_time:{}, docking_time:{}", format!("{:?}", ctx.npc_id), docked_site_name, route_context.did_hold, route_context.extra_hold_dock_time, docking_time);
1162 }
1163 }
1164 route_context.did_hold = false;
1165
1166 just(move |ctx, _| {
1167 ctx.controller
1168 .do_goto_with_height_and_dir(
1169 current_approach.airship_pos,
1170 speed_factor_fn(ctx.npc.mode, 0.75),
1171 None,
1172 Some(current_approach.airship_direction),
1173 FlightMode::Braking(BrakingMode::Precise),
1174 );
1175 })
1176 .repeat()
1177 .stop_if(timeout(ctx.rng.gen_range(10.0..16.0)))
1178 .then(
1180 just(move |ctx, _| {
1181 let next_site_name = ctx.index.sites.get(next_approach.site_id).name().to_string();
1185 ctx.controller.say(
1186 None,
1187 Content::localized_with_args("npc-speech-pilot-announce_next", [
1188 (
1189 "dir",
1190 Direction::from_dir((next_approach.approach_transition_pos - ctx.npc.wpos).xy()).localize_npc(),
1191 ),
1192 ("dst", Content::Plain(next_site_name.to_string())),
1193 ]),
1194 );
1195 })
1196 )
1197 .repeat()
1198 .stop_if(timeout(docking_time as f64))
1199 })
1200 ).then(
1201 just(move |ctx, route_context:&mut AirshipRouteContext| {
1203 ctx.controller.say(
1204 None,
1205 Content::localized_with_args("npc-speech-pilot-takeoff", [
1206 ("src", Content::Plain(ctx.index.sites.get(current_approach.site_id).name().to_string())),
1207 ("dst", Content::Plain(ctx.index.sites.get(next_approach.site_id).name().to_string())),
1208 ]),
1209 );
1210 route_context.next_pilot_dist_to_my_docking_pos_tracker.reset(next_approach.airship_pos.xy());
1213 log_airship_position(ctx, &AirshipFlightPhase::Ascent);
1214 })
1215 ).then(
1216 fly_airship(
1218 AirshipFlightPhase::Ascent,
1219 current_approach.airship_pos + Vec3::unit_z() * 100.0,
1220 20.0,
1221 speed_factor_fn(ctx.npc.mode, 0.2),
1222 0.0,
1223 false,
1224 Some(current_approach.airship_direction),
1225 FlightMode::Braking(BrakingMode::Normal),
1226 false,
1227 Duration::from_secs_f32(120.0),
1228 )
1229 ).then(
1230 fly_airship(
1232 AirshipFlightPhase::Ascent,
1233 current_approach.airship_pos + Vec3::unit_z() * current_approach.height,
1234 20.0,
1235 speed_factor_fn(ctx.npc.mode, 0.4),
1236 0.0,
1237 false,
1238 Some(Dir::from_unnormalized((next_approach.approach_transition_pos - ctx.npc.wpos).xy().with_z(0.0)).unwrap_or_default()),
1239 FlightMode::Braking(BrakingMode::Normal),
1240 false,
1241 Duration::from_secs_f32(120.0),
1242 )
1243 ).then(
1244 fly_airship(
1246 AirshipFlightPhase::DepartureCruise,
1247 route_context.next_leg_cruise_checkpoint_pos,
1248 50.0,
1249 speed_factor_fn(ctx.npc.mode, 1.0),
1250 next_approach.height,
1251 true,
1252 None,
1253 FlightMode::FlyThrough,
1254 true,
1255 Duration::from_secs_f32(5.0),
1256 )
1257 )
1258 .map(|_, _| ()).boxed()
1259 } else {
1260 finish().map(|_, _| ()).boxed()
1263 }
1264 })
1265 .repeat()
1266 .with_state(AirshipRouteContext::default())
1267 .map(|_, _| ())
1268}
1269
1270#[cfg(feature = "airship_log")]
1271fn log_airship_position(ctx: &NpcCtx, phase: &AirshipFlightPhase) {
1273 if let Ok(mut logger) = airship_logger() {
1274 logger.log_position(
1275 ctx.npc_id,
1276 ctx.index.seed,
1277 phase,
1278 ctx.time.0,
1279 ctx.npc.wpos,
1280 matches!(ctx.npc.mode, SimulationMode::Loaded),
1281 );
1282 } else {
1283 warn!("Failed to log airship position for {:?}", ctx.npc_id);
1284 }
1285}
1286
1287#[cfg(not(feature = "airship_log"))]
1288fn log_airship_position(_: &NpcCtx, _: &AirshipFlightPhase) {}
1290
1291#[cfg(test)]
1292mod tests {
1293 use super::{DistanceTrend, DistanceTrendTracker, MovingAverage};
1294 use vek::*;
1295
1296 #[test]
1297 fn moving_average_test() {
1298 let mut ma: MovingAverage<f32, 5, 3> = MovingAverage::default();
1299 ma.add(1.0);
1300 ma.add(2.0);
1301 ma.add(3.0);
1302 ma.add(4.0);
1303 ma.add(5.0);
1304 assert_eq!(ma.average().unwrap(), 3.0);
1305
1306 ma.add(6.0); assert_eq!(ma.average().unwrap(), 4.0);
1308
1309 ma.add(7.0); assert_eq!(ma.average().unwrap(), 5.0);
1311
1312 ma.add(8.0); assert_eq!(ma.average().unwrap(), 6.0);
1314
1315 ma.add(9.0); assert_eq!(ma.average().unwrap(), 7.0);
1317
1318 ma.add(10.0); assert_eq!(ma.average().unwrap(), 8.0);
1320
1321 let mut ma2: MovingAverage<i64, 5, 3> = MovingAverage::default();
1322 ma2.add((1000.0f32 / 1000.0) as i64);
1323 ma2.add((2000.0f32 / 1000.0) as i64);
1324 ma2.add((3000.0f32 / 1000.0) as i64);
1325 ma2.add((4000.0f32 / 1000.0) as i64);
1326 ma2.add((5000.0f32 / 1000.0) as i64);
1327 assert_eq!(ma2.average().unwrap(), 3);
1328
1329 ma2.add((6000.0f32 / 1000.0) as i64);
1330 assert_eq!(ma2.average().unwrap(), 4);
1331
1332 ma2.add((7000.0f32 / 1000.0) as i64);
1333 assert_eq!(ma2.average().unwrap(), 5);
1334
1335 ma2.add((8000.0f32 / 1000.0) as i64);
1336 assert_eq!(ma2.average().unwrap(), 6);
1337
1338 ma2.add((9000.0f32 / 1000.0) as i64);
1339 assert_eq!(ma2.average().unwrap(), 7);
1340
1341 ma2.add((10000.0f32 / 1000.0) as i64);
1342 assert_eq!(ma2.average().unwrap(), 8);
1343
1344 let mut ma3: MovingAverage<i64, 5, 3> = MovingAverage::default();
1345 ma3.add((20.99467f32 * 10000.0) as i64);
1346 ma3.add((20.987871f32 * 10000.0) as i64);
1347 ma3.add((20.69861f32 * 10000.0) as i64);
1348 ma3.add((20.268217f32 * 10000.0) as i64);
1349 ma3.add((20.230164f32 * 10000.0) as i64);
1350 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.6358).abs() < 0.0001);
1351
1352 ma3.add((20.48151f32 * 10000.0) as i64);
1353 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.5332).abs() < 0.0001);
1354
1355 ma3.add((20.568598f32 * 10000.0) as i64);
1356 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.4493).abs() < 0.0001);
1357
1358 ma3.add((20.909971f32 * 10000.0) as i64);
1359 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.4916).abs() < 0.0001);
1360
1361 ma3.add((21.014437f32 * 10000.0) as i64);
1362 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.6408).abs() < 0.0001);
1363
1364 ma3.add((20.62308f32 * 10000.0) as i64);
1365 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.7194).abs() < 0.0001);
1366 }
1367
1368 #[test]
1369 fn distance_trend_tracker_test() {
1370 let mut tracker: DistanceTrendTracker<0> = DistanceTrendTracker::default();
1371 tracker.reset(Vec2::new(0.0, 0.0));
1372 assert!(tracker.update(Vec2::new(1.0, 0.0), 1.0).is_none());
1373 assert!(tracker.update(Vec2::new(2.0, 0.0), 2.0).is_none());
1374 assert!(matches!(
1375 tracker.update(Vec2::new(3.0, 0.0), 3.0).unwrap(),
1376 DistanceTrend::Away
1377 ));
1378 assert!(matches!(
1379 tracker.update(Vec2::new(4.0, 0.0), 4.0).unwrap(),
1380 DistanceTrend::Away
1381 ));
1382 assert!(matches!(
1383 tracker.update(Vec2::new(5.0, 0.0), 5.0).unwrap(),
1384 DistanceTrend::Away
1385 ));
1386
1387 tracker.reset(Vec2::new(0.0, 0.0));
1388 assert!(tracker.update(Vec2::new(5.0, 0.0), 1.0).is_none());
1389 assert!(tracker.update(Vec2::new(4.0, 0.0), 2.0).is_none());
1390 assert!(matches!(
1391 tracker.update(Vec2::new(3.0, 0.0), 3.0).unwrap(),
1392 DistanceTrend::Towards
1393 ));
1394 assert!(matches!(
1395 tracker.update(Vec2::new(2.0, 0.0), 4.0).unwrap(),
1396 DistanceTrend::Towards
1397 ));
1398 assert!(matches!(
1399 tracker.update(Vec2::new(1.0, 0.0), 5.0).unwrap(),
1400 DistanceTrend::Towards
1401 ));
1402 assert!(matches!(
1403 tracker.update(Vec2::new(0.0, 0.0), 6.0).unwrap(),
1404 DistanceTrend::Towards
1405 ));
1406 assert!(matches!(
1407 tracker.update(Vec2::new(-1.0, 0.0), 7.0).unwrap(),
1408 DistanceTrend::Towards
1409 ));
1410 assert!(matches!(
1411 tracker.update(Vec2::new(-2.0, 0.0), 8.0).unwrap(),
1412 DistanceTrend::Away
1413 ));
1414 assert!(matches!(
1415 tracker.update(Vec2::new(-3.0, 0.0), 9.0).unwrap(),
1416 DistanceTrend::Away
1417 ));
1418
1419 let mut tracker2: DistanceTrendTracker<5> = DistanceTrendTracker::default();
1420 assert!(tracker2.update(Vec2::new(100.0, 100.0), 10.0).is_none());
1421 assert!(tracker2.update(Vec2::new(100.0, 200.0), 20.0).is_none());
1422 assert!(matches!(
1423 tracker2.update(Vec2::new(100.0, 300.0), 30.0).unwrap(),
1424 DistanceTrend::Away
1425 ));
1426 assert!(matches!(
1427 tracker2.update(Vec2::new(100.0, 400.0), 40.0).unwrap(),
1428 DistanceTrend::Away
1429 ));
1430 assert!(matches!(
1431 tracker2.update(Vec2::new(100.0, 500.0), 50.0).unwrap(),
1432 DistanceTrend::Away
1433 ));
1434 assert!(matches!(
1435 tracker2.update(Vec2::new(100.0, 490.0), 60.0).unwrap(),
1436 DistanceTrend::Away
1437 ));
1438 assert!(matches!(
1439 tracker2.update(Vec2::new(100.0, 505.0), 70.0).unwrap(),
1440 DistanceTrend::Neutral
1441 ));
1442 assert!(matches!(
1443 tracker2.update(Vec2::new(100.0, 500.0), 80.0).unwrap(),
1444 DistanceTrend::Neutral
1445 ));
1446 assert!(matches!(
1447 tracker2.update(Vec2::new(100.0, 495.0), 90.0).unwrap(),
1448 DistanceTrend::Neutral
1449 ));
1450 assert!(matches!(
1451 tracker2.update(Vec2::new(100.0, 500.0), 100.0).unwrap(),
1452 DistanceTrend::Neutral
1453 ));
1454 assert!(matches!(
1455 tracker2.update(Vec2::new(100.0, 495.0), 110.0).unwrap(),
1456 DistanceTrend::Neutral
1457 ));
1458 assert!(matches!(
1459 tracker2.update(Vec2::new(100.0, 500.0), 120.0).unwrap(),
1460 DistanceTrend::Neutral
1461 ));
1462 assert!(matches!(
1463 tracker2.update(Vec2::new(100.0, 505.0), 130.0).unwrap(),
1464 DistanceTrend::Neutral
1465 ));
1466 assert!(matches!(
1467 tracker2.update(Vec2::new(100.0, 500.0), 140.0).unwrap(),
1468 DistanceTrend::Neutral
1469 ));
1470 assert!(matches!(
1471 tracker2.update(Vec2::new(100.0, 505.0), 150.0).unwrap(),
1472 DistanceTrend::Neutral
1473 ));
1474 }
1475}