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_SQR: usize = 5 * 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_SQR>,
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 && let Some(last_pos) = self.pos_history.last()
212 {
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 backout_pos.z += 50.0;
238 }
239 self.backout_route = vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
240 #[cfg(debug_assertions)]
242 debug!(
243 "Airship {} Stuck! at {} {} {}, backout_dir:{:?}, backout_pos:{:?}",
244 format!("{:?}", ctx.npc_id),
245 ctx.npc.wpos.x,
246 ctx.npc.wpos.y,
247 ctx.npc.wpos.z,
248 backout_dir,
249 backout_pos
250 );
251 }
252 }
253 }
254 !self.backout_route.is_empty()
255 }
256}
257
258#[derive(Debug, Clone, Default)]
259struct RateTracker {
262 last_pos: Option<Vec2<f32>>,
264 last_time: f32,
266}
267
268impl RateTracker {
269 fn update(&mut self, current_pos: Vec2<f32>, time: f32) -> f32 {
272 let rate = if let Some(last_pos) = self.last_pos {
273 let dt = time - self.last_time;
274 if dt > 0.0 {
275 current_pos.distance(last_pos) / dt } else {
277 0.0
278 }
279 } else {
280 0.0
281 };
282 self.last_pos = Some(current_pos);
283 self.last_time = time;
284 rate
285 }
286}
287
288#[derive(Debug, PartialEq)]
289enum DistanceTrend {
292 Towards,
294 Away,
296 Neutral,
298}
299
300#[derive(Debug, Default, Clone)]
304struct DistanceTrendTracker<const C: usize> {
305 fixed_pos: Vec2<f32>,
307 avg_rate: MovingAverage<
309 f64,
310 NEXT_PILOT_MOVING_DIST_AVERAGE_CAPACITY,
311 NEXT_PILOT_MOVING_DIST_AVERAGE_MIN_SIZE,
312 >,
313 prev_dist: Option<f32>,
315 prev_time: f64,
317}
318
319impl<const C: usize> DistanceTrendTracker<C> {
320 fn update(&mut self, pos: Vec2<f32>, time: f64) -> Option<DistanceTrend> {
323 let current_dist = pos.distance(self.fixed_pos);
324 if let Some(prev) = self.prev_dist {
325 if time - self.prev_time > f64::EPSILON {
330 let rate = (current_dist - prev) as f64 / (time - self.prev_time);
331 self.prev_dist = Some(current_dist);
332 self.prev_time = time;
333
334 self.avg_rate.add(rate);
335 if let Some(avg) = self.avg_rate.average() {
336 if avg > C as f64 {
337 Some(DistanceTrend::Away)
338 } else if avg < -(C as f64) {
339 Some(DistanceTrend::Towards)
340 } else {
341 Some(DistanceTrend::Neutral)
342 }
343 } else {
344 None
345 }
346 } else {
347 None
349 }
350 } else {
351 self.prev_dist = Some(current_dist);
352 self.prev_time = time;
353 None
354 }
355 }
356
357 fn reset(&mut self, pos: Vec2<f32>) {
358 self.fixed_pos = pos;
359 self.avg_rate.reset();
360 self.prev_dist = None;
361 }
362}
363
364#[derive(Clone, Debug)]
366struct MovingAverage<T, const S: usize, const N: usize>
367where
368 T: Default
369 + FromPrimitive
370 + std::ops::AddAssign
371 + std::ops::Sub<Output = T>
372 + std::ops::Div<Output = T>
373 + Copy,
374{
375 values: VecDeque<T>,
376 sum: T,
377}
378
379impl<T, const S: usize, const N: usize> MovingAverage<T, S, N>
380where
381 T: Default
382 + FromPrimitive
383 + std::ops::AddAssign
384 + std::ops::Sub<Output = T>
385 + std::ops::Div<Output = T>
386 + Copy,
387{
388 fn add(&mut self, value: T) {
391 if self.values.len() == S
392 && let Some(old_value) = self.values.pop_front()
393 {
394 self.sum = self.sum - old_value;
395 }
396 self.values.push_back(value);
397 self.sum += value;
398 }
399
400 fn average(&self) -> Option<T> {
402 if self.values.len() < N {
403 None
404 } else {
405 Some(self.sum / T::from_u32(self.values.len() as u32).unwrap())
406 }
407 }
408
409 fn reset(&mut self) {
410 self.values.clear();
411 self.sum = T::from_u32(0).unwrap();
412 }
413}
414
415impl<T, const S: usize, const N: usize> Default for MovingAverage<T, S, N>
416where
417 T: Default
418 + FromPrimitive
419 + std::ops::AddAssign
420 + std::ops::Sub<Output = T>
421 + std::ops::Div<Output = T>
422 + Copy,
423{
424 fn default() -> Self {
425 Self {
426 values: VecDeque::with_capacity(S),
427 sum: T::from_u32(0).unwrap(),
428 }
429 }
430}
431
432#[derive(Debug, Copy, Clone, PartialEq, Default)]
434pub enum AirshipFlightPhase {
435 Ascent,
436 DepartureCruise,
437 ApproachCruise,
438 Transition,
439 #[default]
440 Docked,
441}
442
443impl fmt::Display for AirshipFlightPhase {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 match self {
446 AirshipFlightPhase::Ascent => write!(f, "Ascent"),
447 AirshipFlightPhase::DepartureCruise => write!(f, "DepartureCruise"),
448 AirshipFlightPhase::ApproachCruise => write!(f, "ApproachCruise"),
449 AirshipFlightPhase::Transition => write!(f, "Transition"),
450 AirshipFlightPhase::Docked => write!(f, "Docked"),
451 }
452 }
453}
454
455fn fly_airship(
458 phase: AirshipFlightPhase,
459 wpos: Vec3<f32>,
460 goal_dist: f32,
461 initial_speed_factor: f32,
462 height_offset: f32,
463 with_terrain_following: bool,
464 direction_override: Option<Dir>,
465 flight_mode: FlightMode,
466 with_collision_avoidance: bool,
467 radar_interval: Duration,
468) -> impl Action<AirshipRouteContext> {
469 now(move |_, airship_context: &mut AirshipRouteContext| {
470 airship_context.avoidance_timer = radar_interval;
471 airship_context.avoid_mode = AirshipAvoidanceMode::None;
472 if matches!(phase, AirshipFlightPhase::DepartureCruise) {
473 airship_context.my_stuck_tracker = Some(StuckAirshipTracker::default());
475 }
476 fly_airship_inner(
477 phase,
478 wpos,
479 goal_dist,
480 initial_speed_factor,
481 height_offset,
482 with_terrain_following,
483 direction_override,
484 flight_mode,
485 with_collision_avoidance,
486 radar_interval,
487 )
488 })
489}
490
491fn should_hold(
495 my_pilot_distance_to_next_pilot: f32,
496 next_pilot_dist_to_docking_target: f32,
497 next_pilot_to_docking_target_trend: &DistanceTrend,
498) -> bool {
499 *next_pilot_to_docking_target_trend == DistanceTrend::Towards
500 && next_pilot_dist_to_docking_target < CLOSE_TO_DOCKING_SITE_DISTANCE_SQR
501 && my_pilot_distance_to_next_pilot < VERY_CLOSE_AIRSHIP_DISTANCE_SQR
502}
503
504fn should_slow_down(
513 my_pilot_pos: &Vec2<f32>,
514 my_pilot_velocity: f32,
515 my_pilot_dist_to_docking_target: f32,
516 next_pilot_pos: &Vec2<f32>,
517 next_pilot_velocity: f32,
518 next_pilot_dist_to_docking_target: f32,
519 next_pilot_to_docking_target_trend: &DistanceTrend,
520) -> bool {
521 *next_pilot_to_docking_target_trend == DistanceTrend::Towards
522 && next_pilot_velocity / my_pilot_velocity < NEXT_PILOT_VELOCITY_RATIO_MIN
523 && my_pilot_pos.distance_squared(*next_pilot_pos)
524 < Airships::AIRSHIP_SPACING.powi(2) * NEXT_PILOT_SPACING_THRESHOLD_SQR
525 && my_pilot_dist_to_docking_target > next_pilot_dist_to_docking_target
526}
527
528fn fly_inner_default_goto(
532 ctx: &mut NpcCtx,
533 wpos: Vec3<f32>,
534 speed_factor: f32,
535 height_offset: f32,
536 with_terrain_following: bool,
537 direction_override: Option<Dir>,
538 flight_mode: FlightMode,
539) {
540 let height_offset_opt = if with_terrain_following {
541 Some(height_offset)
542 } else {
543 None
544 };
545 ctx.controller.do_goto_with_height_and_dir(
546 wpos,
547 speed_factor,
548 height_offset_opt,
549 direction_override,
550 flight_mode,
551 );
552}
553
554fn fly_airship_inner(
556 phase: AirshipFlightPhase,
557 wpos: Vec3<f32>,
558 goal_dist: f32,
559 initial_speed_factor: f32,
560 height_offset: f32,
561 with_terrain_following: bool,
562 direction_override: Option<Dir>,
563 flight_mode: FlightMode,
564 with_collision_avoidance: bool,
565 radar_interval: Duration,
566) -> impl Action<AirshipRouteContext> {
567 just(
568 move |ctx, airship_context: &mut AirshipRouteContext| {
569
570 let (current_approach, pos_tracker_target_loc) =
575 match phase {
576 AirshipFlightPhase::DepartureCruise => (&airship_context.next_leg_approach.unwrap(), airship_context.next_leg_cruise_checkpoint_pos.xy()),
577 _ => (&airship_context.current_leg_approach.unwrap(), airship_context.current_leg_approach.unwrap().approach_transition_pos.xy()),
578 };
579
580 let remaining = airship_context
584 .avoidance_timer
585 .checked_sub(Duration::from_secs_f32(ctx.dt));
586 if remaining.is_none() {
588 airship_context.avoidance_timer = radar_interval;
590
591 #[cfg(feature = "airship_log")]
592 log_airship_position(ctx, &phase);
594
595 if with_collision_avoidance
597 && matches!(phase, AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise)
598 {
599 let mypos = ctx.npc.wpos;
609 let my_velocity = if let Some(my_rate_tracker) = &mut airship_context.my_rate_tracker {
610 my_rate_tracker.update(mypos.xy(), ctx.time.0 as f32)
611 } else {
612 0.0
613 };
614 let my_distance_to_docking_target = mypos.xy().distance_squared(current_approach.airship_pos.xy());
615
616 let (next_pilot_wpos, next_pilot_dist_trend) = if let Some(next_pilot_rate_tracker) = &mut airship_context.next_pilot_rate_tracker_
617 && let Some(next_pilot) = ctx.state.data().npcs.get(airship_context.next_pilot_id)
618 {
619 let next_rate = next_pilot_rate_tracker.update(next_pilot.wpos.xy(), ctx.time.0 as f32);
620
621 let next_dist_trend = airship_context
623 .next_pilot_dist_to_my_docking_pos_tracker.update(next_pilot.wpos.xy(), ctx.time.0)
624 .unwrap_or(DistanceTrend::Away);
625
626 if next_rate > airship_context.simulated_airship_speed - NEXT_PILOT_CRUISE_SPEED_TOLERANCE {
630 airship_context.next_pilot_average_velocity.add((next_rate as f64 * MOVING_AVERAGE_SCALE_FACTOR) as i64);
632 }
633
634 (next_pilot.wpos, next_dist_trend)
635 } else {
636 (Vec3::zero(), DistanceTrend::Away)
637 };
638
639 let my_pilot_distance_to_next_pilot = mypos.xy().distance_squared(next_pilot_wpos.xy());
640 let next_pilot_distance_to_docking_target = next_pilot_wpos.xy().distance_squared(current_approach.airship_pos.xy());
641
642 let avoidance = match airship_context.avoid_mode {
644 AirshipAvoidanceMode::Stuck(..) => {
645 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
649 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
650 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
651 {
652 AirshipAvoidanceMode::Stuck(backout_pos)
653 } else {
654 #[cfg(debug_assertions)]
655 debug!("{:?} unstuck at pos: {} {}",
656 ctx.npc_id,
657 mypos.x as i32, mypos.y as i32,
658 );
659
660 AirshipAvoidanceMode::None
661 }
662 }
663 AirshipAvoidanceMode::Hold(..) => {
664 if next_pilot_dist_trend != DistanceTrend::Away {
668 airship_context.avoid_mode
669 } else {
670 AirshipAvoidanceMode::None
671 }
672 }
673 AirshipAvoidanceMode::Slow(..) => {
674 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
677 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
678 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
679 {
680 AirshipAvoidanceMode::Stuck(backout_pos)
681 } else if should_hold(
682 my_pilot_distance_to_next_pilot,
683 next_pilot_distance_to_docking_target,
684 &next_pilot_dist_trend,
685 ) {
686 airship_context.did_hold = true;
687 AirshipAvoidanceMode::Hold(
688 mypos,
689 Dir::from_unnormalized((current_approach.dock_center - mypos.xy()).with_z(0.0))
691 .unwrap_or_default(),
692 )
693 } else {
694 if next_pilot_dist_trend == DistanceTrend::Away || my_pilot_distance_to_next_pilot > Airships::AIRSHIP_SPACING.powi(2) {
704 AirshipAvoidanceMode::None
705 } else {
706 airship_context.avoid_mode
708 }
709 }
710 }
711 AirshipAvoidanceMode::None => {
712 let next_pilot_avg_velocity = (airship_context.next_pilot_average_velocity.average().unwrap_or(0) as f64 / MOVING_AVERAGE_SCALE_FACTOR) as f32;
714 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
716 && stuck_tracker.is_stuck(ctx, &mypos, &pos_tracker_target_loc)
717 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
718 {
719 #[cfg(debug_assertions)]
720 debug!("{:?} stuck at pos: {} {}",
721 ctx.npc_id,
722 mypos.x as i32, mypos.y as i32,
723 );
724 AirshipAvoidanceMode::Stuck(backout_pos)
725 } else if should_hold(
727 my_pilot_distance_to_next_pilot,
728 next_pilot_distance_to_docking_target,
729 &next_pilot_dist_trend,
730 ) {
731 airship_context.did_hold = true;
732 AirshipAvoidanceMode::Hold(
733 mypos,
734 Dir::from_unnormalized((current_approach.dock_center - mypos.xy()).with_z(0.0))
735 .unwrap_or_default(),
736 )
737 } else if should_slow_down(
739 &mypos.xy(),
740 my_velocity,
741 my_distance_to_docking_target,
742 &next_pilot_wpos.xy(),
743 next_pilot_avg_velocity,
744 next_pilot_distance_to_docking_target,
745 &next_pilot_dist_trend
746 ) {
747 let mut new_speed_factor = if matches!(ctx.npc.mode, SimulationMode::Simulated) {
751 CLOSE_AIRSHIP_SPEED_FACTOR
754 } else {
755 let target_velocity: f32 = next_pilot_avg_velocity * CLOSE_AIRSHIP_SPEED_FACTOR;
758
759 let new_sf = airship_context.speed_factor_conversion_factor * target_velocity.powi(2);
772 if new_sf > 0.0 {
773 #[cfg(debug_assertions)]
774 debug!(
775 "Pilot {:?}: Adjusting speed factor to {}, next pilot avg velocity: {}, * speed_factor = {}",
776 ctx.npc_id,
777 new_sf,
778 next_pilot_avg_velocity,
779 target_velocity,
780 );
781 }
782 new_sf
783 };
784
785 if new_speed_factor < 0.05 {
786 warn!("Pilot {:?} calculated slow down speed factor is too low, clamping to 0.05", ctx.npc_id);
787 new_speed_factor = 0.05;
788 }
789 AirshipAvoidanceMode::Slow(
790 new_speed_factor
791 )
792 } else {
793 AirshipAvoidanceMode::None
794 }
795 }
796 };
797
798 if avoidance != airship_context.avoid_mode {
800 airship_context.avoid_mode = avoidance;
801 }
802 }
803 } else {
804 airship_context.avoidance_timer = remaining.unwrap_or(radar_interval);
806 }
807
808 match airship_context.avoid_mode {
810 AirshipAvoidanceMode::Stuck(backout_pos) => {
811 ctx.controller.do_goto_with_height_and_dir(
813 backout_pos,
814 1.5,
815 None,
816 None,
817 FlightMode::Braking(BrakingMode::Normal),
818 );
819 }
820 AirshipAvoidanceMode::Hold(hold_pos, hold_dir) => {
821 airship_context.hold_timer -= ctx.dt;
823 if airship_context.hold_timer <= 0.0 {
824 if !airship_context.hold_announced {
825 airship_context.hold_announced = true;
826 ctx.controller
827 .say(None, Content::localized("npc-speech-pilot-announce_hold"));
828 } else {
829 ctx.controller
830 .say(None, Content::localized("npc-speech-pilot-continue_hold"));
831 }
832 airship_context.hold_timer = ctx.rng.random_range(10.0..20.0);
833 }
834 let hold_pos = if matches!(ctx.npc.mode, SimulationMode::Simulated) {
837 hold_pos
838 } else {
839 hold_pos
843 + (Vec3::new(0.7, 0.8, 0.9).map(|e| (e * ctx.time.0).sin())
844 * Vec3::new(5.0, 5.0, 10.0))
845 .map(|e| e as f32)
846 };
847
848 ctx.controller.do_goto_with_height_and_dir(
850 hold_pos,
851 0.25,
852 None,
853 Some(hold_dir),
854 FlightMode::Braking(BrakingMode::Normal),
855 );
856 }
857 AirshipAvoidanceMode::Slow(speed_factor) => {
858 fly_inner_default_goto(ctx, wpos, speed_factor, height_offset,
860 with_terrain_following, direction_override, flight_mode,
861 );
862 }
863 AirshipAvoidanceMode::None => {
864 fly_inner_default_goto(ctx, wpos, initial_speed_factor, height_offset,
866 with_terrain_following, direction_override, flight_mode,
867 );
868 }
869 }
870 },
871 )
872 .repeat()
873 .boxed()
874 .stop_if(move |ctx: &mut NpcCtx| {
875 if flight_mode == FlightMode::FlyThrough {
876 ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
878 } else {
879 ctx.npc.wpos.distance_squared(wpos) < goal_dist.powi(2)
881 }
882 })
883 .debug(move || {
884 format!(
885 "fly airship, phase:{:?}, tgt pos:({}, {}, {}), goal dist:{}, speed:{}, height:{}, \
886 terrain following:{}, FlightMode:{:?}, collision avoidance:{}, radar interval:{}",
887 phase,
888 wpos.x,
889 wpos.y,
890 wpos.z,
891 goal_dist,
892 initial_speed_factor,
893 height_offset,
894 with_terrain_following,
895 flight_mode,
896 with_collision_avoidance,
897 radar_interval.as_secs_f32()
898 )
899 })
900 .map(|_, _| ())
901}
902
903fn approach_target_pos(
911 ctx: &mut NpcCtx,
912 approach_pos: Vec2<f32>,
913 default_alt: f32,
914 height: f32,
915) -> Vec3<f32> {
916 let max_alt = CARDINALS
918 .iter()
919 .map(|rpos| {
920 ctx.world
921 .sim()
922 .get_surface_alt_approx(approach_pos.as_() + rpos * 200)
923 })
924 .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
925 .unwrap_or(default_alt);
926 approach_pos.with_z(max_alt + height)
927}
928
929pub fn pilot_airship<S: State>() -> impl Action<S> {
934 now(move |ctx, route_context: &mut AirshipRouteContext| {
935 if let Some((route_index, start_leg_index)) = ctx.state.data().airship_sim.assigned_routes.get(&ctx.npc_id)
937 {
938 if route_context.route_index == usize::MAX {
942 route_context.route_index = *route_index;
945 route_context.next_leg = *start_leg_index;
946 if let Some (next_pilot) = ctx.state.data().airship_sim.next_pilot(*route_index, ctx.npc_id) {
947 route_context.next_pilot_id = next_pilot;
948 } else {
949 route_context.next_pilot_id = NpcId::default();
950 }
951 let (max_speed, reference_area, thrust) =
952 ctx.state.data().npcs.mounts.get_mount_link(ctx.npc_id)
953 .map(|mount_link|
954 ctx.state.data().npcs.get(mount_link.mount)
955 .map(|airship| {
956 (airship.body.max_speed_approx(),
957 airship.body.parasite_drag(1.0),
958 airship.body.fly_thrust().unwrap_or_default())
959 })
960 .unwrap_or_default()
961 ).or({
962 Some((0.0, 1.0, 1.0))
964 })
965 .unwrap_or((0.0, 1.0, 1.0));
966 route_context.simulated_airship_speed = max_speed;
967 route_context.speed_factor_conversion_factor = 0.45 * AIR_DENSITY * reference_area / thrust;
968 route_context.my_rate_tracker = Some(RateTracker::default());
969 route_context.next_pilot_rate_tracker_ = Some(RateTracker::default());
970
971 #[cfg(debug_assertions)]
972 {
973 let current_approach = ctx.world.civs().airships.approach_for_route_and_leg(
974 route_context.route_index,
975 route_context.next_leg,
976 );
977 debug!(
978 "Server startup, airship pilot {:?} starting on route {} and leg {}, target dock: {} {}, following pilot {:?}",
979 ctx.npc_id,
980 route_context.route_index,
981 route_context.next_leg,
982 current_approach.airship_pos.x as i32, current_approach.airship_pos.y as i32,
983 route_context.next_pilot_id
984 );
985 }
986 if route_context.next_pilot_id == NpcId::default() {
987 tracing::error!("Pilot {:?} has no next pilot to follow.", ctx.npc_id);
988 }
989 }
990
991 route_context.current_leg_approach = Some(ctx.world.civs().airships.approach_for_route_and_leg(
996 route_context.route_index,
997 route_context.next_leg,
998 ));
999 route_context.next_leg = ctx.world.civs().airships.increment_route_leg(
1001 route_context.route_index,
1002 route_context.next_leg,
1003 );
1004 route_context.next_leg_approach = Some(ctx.world.civs().airships.approach_for_route_and_leg(
1007 route_context.route_index,
1008 route_context.next_leg,
1009 ));
1010
1011 if route_context.current_leg_approach.is_none() ||
1012 route_context.next_leg_approach.is_none()
1013 {
1014 tracing::error!(
1015 "Airship pilot {:?} approaches not found for route {} leg {}, stopping pilot_airship loop.",
1016 ctx.npc_id,
1017 route_context.route_index,
1018 route_context.next_leg
1019 );
1020 return finish().map(|_, _| ()).boxed();
1021 }
1022
1023 let current_approach = route_context.current_leg_approach.unwrap();
1025 let next_approach = route_context.next_leg_approach.unwrap();
1026
1027 let next_leg_cruise_dir = (next_approach.approach_transition_pos.xy() - current_approach.airship_pos.xy()).normalized();
1028 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);
1029 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);
1033
1034 route_context.next_pilot_dist_to_my_docking_pos_tracker.reset(current_approach.airship_pos.xy());
1036
1037 let speed_factor_fn = |sim_mode: SimulationMode, speed_factor: f32| {
1040 if matches!(sim_mode, SimulationMode::Simulated) {
1042 speed_factor.powf(0.3)
1043 } else {
1044 speed_factor
1045 }
1046 };
1047
1048 fly_airship(
1057 AirshipFlightPhase::ApproachCruise,
1058 current_approach.approach_transition_pos,
1059 50.0,
1060 speed_factor_fn(ctx.npc.mode, 1.0),
1061 current_approach.height,
1062 true,
1063 None,
1064 FlightMode::FlyThrough,
1065 true,
1066 Duration::from_secs_f32(1.0),
1067 )
1068 .then(
1069 fly_airship(
1071 AirshipFlightPhase::Transition,
1072 current_approach.airship_pos + Vec3::unit_z() * current_approach.height,
1073 20.0,
1074 speed_factor_fn(ctx.npc.mode, 0.4),
1075 current_approach.height,
1076 true,
1077 Some(current_approach.airship_direction),
1078 FlightMode::Braking(BrakingMode::Normal),
1079 false,
1080 Duration::from_secs_f32(2.0),
1081 ))
1082 .then(
1092 just(|ctx: &mut NpcCtx, _| {
1093 log_airship_position(ctx, &AirshipFlightPhase::Transition);
1094 }))
1095
1096 .then(
1097 just(move |ctx, _| {
1099 ctx.controller
1100 .do_goto_with_height_and_dir(
1101 current_approach.airship_pos + Vec3::unit_z() * 125.0,
1102 speed_factor_fn(ctx.npc.mode, 0.9),
1103 None,
1104 Some(current_approach.airship_direction),
1105 FlightMode::Braking(BrakingMode::Normal),
1106 );
1107 })
1108 .repeat()
1109 .stop_if(timeout(ctx.rng.random_range(12.5..16.0) * (current_approach.height as f64 / Airships::CRUISE_HEIGHTS[0] as f64) * 1.3)))
1110 .then(
1111 just(|ctx: &mut NpcCtx, _| {
1112 log_airship_position(ctx, &AirshipFlightPhase::Transition);
1113 }))
1114 .then(
1115 just(move |ctx: &mut NpcCtx, _| {
1117 ctx.controller
1118 .do_goto_with_height_and_dir(
1119 current_approach.airship_pos + Vec3::unit_z() * 20.0,
1120 speed_factor_fn(ctx.npc.mode, 0.9),
1121 None,
1122 Some(current_approach.airship_direction),
1123 FlightMode::Braking(BrakingMode::Precise),
1124 );
1125 })
1126 .repeat()
1127 .stop_if(timeout(ctx.rng.random_range(6.5..8.0))))
1128 .then(just(|ctx: &mut NpcCtx, _| {
1130 log_airship_position(ctx, &AirshipFlightPhase::Docked);
1131 ctx.controller
1132 .say(None, Content::localized("npc-speech-pilot-landed"));
1133 }))
1134
1135 .then(
1137 now(move |ctx, route_context:&mut AirshipRouteContext| {
1138 if route_context.did_hold {
1146 route_context.extra_hold_dock_time += 30.0;
1147 } else if route_context.extra_hold_dock_time > 45.0 {
1148 route_context.extra_hold_dock_time -= 45.0;
1149 } else {
1150 route_context.extra_hold_dock_time = 0.0;
1151 }
1152
1153 let docking_time = route_context.extra_hold_dock_time + Airships::docking_duration();
1154 #[cfg(debug_assertions)]
1155 {
1156 if route_context.did_hold || docking_time > Airships::docking_duration() {
1157 let docked_site_name = ctx.index.sites.get(current_approach.site_id).name();
1158 debug!("{:?}, Docked at {:?}, did_hold:{}, extra_hold_dock_time:{}, docking_time:{}", ctx.npc_id, docked_site_name, route_context.did_hold, route_context.extra_hold_dock_time, docking_time);
1159 }
1160 }
1161 route_context.did_hold = false;
1162
1163 just(move |ctx, _| {
1164 ctx.controller
1165 .do_goto_with_height_and_dir(
1166 current_approach.airship_pos,
1167 speed_factor_fn(ctx.npc.mode, 0.75),
1168 None,
1169 Some(current_approach.airship_direction),
1170 FlightMode::Braking(BrakingMode::Precise),
1171 );
1172 })
1173 .repeat()
1174 .stop_if(timeout(ctx.rng.random_range(10.0..16.0)))
1175 .then(
1177 just(move |ctx, _| {
1178 let next_site_name = ctx.index.sites.get(next_approach.site_id).name();
1182 ctx.controller.say(
1183 None,
1184 Content::localized_with_args("npc-speech-pilot-announce_next", [
1185 (
1186 "dir",
1187 Direction::from_dir((next_approach.approach_transition_pos - ctx.npc.wpos).xy()).localize_npc(),
1188 ),
1189 ("dst", Content::Plain(next_site_name.unwrap_or("unknown").to_string())),
1190 ]),
1191 );
1192 })
1193 )
1194 .repeat()
1195 .stop_if(timeout(docking_time as f64))
1196 })
1197 ).then(
1198 just(move |ctx, route_context:&mut AirshipRouteContext| {
1200 ctx.controller.say(
1201 None,
1202 Content::localized_with_args("npc-speech-pilot-takeoff", [
1203 ("src", Content::Plain(ctx.index.sites.get(current_approach.site_id).name().unwrap_or("unknown").to_string())),
1204 ("dst", Content::Plain(ctx.index.sites.get(next_approach.site_id).name().unwrap_or("unknown").to_string())),
1205 ]),
1206 );
1207 route_context.next_pilot_dist_to_my_docking_pos_tracker.reset(next_approach.airship_pos.xy());
1210 log_airship_position(ctx, &AirshipFlightPhase::Ascent);
1211 })
1212 ).then(
1213 fly_airship(
1215 AirshipFlightPhase::Ascent,
1216 current_approach.airship_pos + Vec3::unit_z() * 100.0,
1217 20.0,
1218 speed_factor_fn(ctx.npc.mode, 0.2),
1219 0.0,
1220 false,
1221 Some(current_approach.airship_direction),
1222 FlightMode::Braking(BrakingMode::Normal),
1223 false,
1224 Duration::from_secs_f32(120.0),
1225 )
1226 ).then(
1227 fly_airship(
1229 AirshipFlightPhase::Ascent,
1230 current_approach.airship_pos + Vec3::unit_z() * current_approach.height,
1231 20.0,
1232 speed_factor_fn(ctx.npc.mode, 0.4),
1233 0.0,
1234 false,
1235 Some(Dir::from_unnormalized((next_approach.approach_transition_pos - ctx.npc.wpos).xy().with_z(0.0)).unwrap_or_default()),
1236 FlightMode::Braking(BrakingMode::Normal),
1237 false,
1238 Duration::from_secs_f32(120.0),
1239 )
1240 ).then(
1241 fly_airship(
1243 AirshipFlightPhase::DepartureCruise,
1244 route_context.next_leg_cruise_checkpoint_pos,
1245 50.0,
1246 speed_factor_fn(ctx.npc.mode, 1.0),
1247 next_approach.height,
1248 true,
1249 None,
1250 FlightMode::FlyThrough,
1251 true,
1252 Duration::from_secs_f32(5.0),
1253 )
1254 )
1255 .map(|_, _| ()).boxed()
1256 } else {
1257 finish().map(|_, _| ()).boxed()
1260 }
1261 })
1262 .repeat()
1263 .with_state(AirshipRouteContext::default())
1264 .map(|_, _| ())
1265}
1266
1267#[cfg(feature = "airship_log")]
1268fn log_airship_position(ctx: &NpcCtx, phase: &AirshipFlightPhase) {
1270 if let Ok(mut logger) = airship_logger() {
1271 logger.log_position(
1272 ctx.npc_id,
1273 ctx.index.seed,
1274 phase,
1275 ctx.time.0,
1276 ctx.npc.wpos,
1277 matches!(ctx.npc.mode, SimulationMode::Loaded),
1278 );
1279 } else {
1280 warn!("Failed to log airship position for {:?}", ctx.npc_id);
1281 }
1282}
1283
1284#[cfg(not(feature = "airship_log"))]
1285fn log_airship_position(_: &NpcCtx, _: &AirshipFlightPhase) {}
1287
1288#[cfg(test)]
1289mod tests {
1290 use super::{DistanceTrend, DistanceTrendTracker, MovingAverage};
1291 use vek::*;
1292
1293 #[test]
1294 fn moving_average_test() {
1295 let mut ma: MovingAverage<f32, 5, 3> = MovingAverage::default();
1296 ma.add(1.0);
1297 ma.add(2.0);
1298 ma.add(3.0);
1299 ma.add(4.0);
1300 ma.add(5.0);
1301 assert_eq!(ma.average().unwrap(), 3.0);
1302
1303 ma.add(6.0); assert_eq!(ma.average().unwrap(), 4.0);
1305
1306 ma.add(7.0); assert_eq!(ma.average().unwrap(), 5.0);
1308
1309 ma.add(8.0); assert_eq!(ma.average().unwrap(), 6.0);
1311
1312 ma.add(9.0); assert_eq!(ma.average().unwrap(), 7.0);
1314
1315 ma.add(10.0); assert_eq!(ma.average().unwrap(), 8.0);
1317
1318 let mut ma2: MovingAverage<i64, 5, 3> = MovingAverage::default();
1319 ma2.add((1000.0f32 / 1000.0) as i64);
1320 ma2.add((2000.0f32 / 1000.0) as i64);
1321 ma2.add((3000.0f32 / 1000.0) as i64);
1322 ma2.add((4000.0f32 / 1000.0) as i64);
1323 ma2.add((5000.0f32 / 1000.0) as i64);
1324 assert_eq!(ma2.average().unwrap(), 3);
1325
1326 ma2.add((6000.0f32 / 1000.0) as i64);
1327 assert_eq!(ma2.average().unwrap(), 4);
1328
1329 ma2.add((7000.0f32 / 1000.0) as i64);
1330 assert_eq!(ma2.average().unwrap(), 5);
1331
1332 ma2.add((8000.0f32 / 1000.0) as i64);
1333 assert_eq!(ma2.average().unwrap(), 6);
1334
1335 ma2.add((9000.0f32 / 1000.0) as i64);
1336 assert_eq!(ma2.average().unwrap(), 7);
1337
1338 ma2.add((10000.0f32 / 1000.0) as i64);
1339 assert_eq!(ma2.average().unwrap(), 8);
1340
1341 let mut ma3: MovingAverage<i64, 5, 3> = MovingAverage::default();
1342 ma3.add((20.99467f32 * 10000.0) as i64);
1343 ma3.add((20.987871f32 * 10000.0) as i64);
1344 ma3.add((20.69861f32 * 10000.0) as i64);
1345 ma3.add((20.268217f32 * 10000.0) as i64);
1346 ma3.add((20.230164f32 * 10000.0) as i64);
1347 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.6358).abs() < 0.0001);
1348
1349 ma3.add((20.48151f32 * 10000.0) as i64);
1350 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.5332).abs() < 0.0001);
1351
1352 ma3.add((20.568598f32 * 10000.0) as i64);
1353 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.4493).abs() < 0.0001);
1354
1355 ma3.add((20.909971f32 * 10000.0) as i64);
1356 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.4916).abs() < 0.0001);
1357
1358 ma3.add((21.014437f32 * 10000.0) as i64);
1359 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.6408).abs() < 0.0001);
1360
1361 ma3.add((20.62308f32 * 10000.0) as i64);
1362 assert!((ma3.average().unwrap() as f64 / 10000.0 - 20.7194).abs() < 0.0001);
1363 }
1364
1365 #[test]
1366 fn distance_trend_tracker_test() {
1367 let mut tracker: DistanceTrendTracker<0> = DistanceTrendTracker::default();
1368 tracker.reset(Vec2::new(0.0, 0.0));
1369 assert!(tracker.update(Vec2::new(1.0, 0.0), 1.0).is_none());
1370 assert!(tracker.update(Vec2::new(2.0, 0.0), 2.0).is_none());
1371 assert!(matches!(
1372 tracker.update(Vec2::new(3.0, 0.0), 3.0).unwrap(),
1373 DistanceTrend::Away
1374 ));
1375 assert!(matches!(
1376 tracker.update(Vec2::new(4.0, 0.0), 4.0).unwrap(),
1377 DistanceTrend::Away
1378 ));
1379 assert!(matches!(
1380 tracker.update(Vec2::new(5.0, 0.0), 5.0).unwrap(),
1381 DistanceTrend::Away
1382 ));
1383
1384 tracker.reset(Vec2::new(0.0, 0.0));
1385 assert!(tracker.update(Vec2::new(5.0, 0.0), 1.0).is_none());
1386 assert!(tracker.update(Vec2::new(4.0, 0.0), 2.0).is_none());
1387 assert!(matches!(
1388 tracker.update(Vec2::new(3.0, 0.0), 3.0).unwrap(),
1389 DistanceTrend::Towards
1390 ));
1391 assert!(matches!(
1392 tracker.update(Vec2::new(2.0, 0.0), 4.0).unwrap(),
1393 DistanceTrend::Towards
1394 ));
1395 assert!(matches!(
1396 tracker.update(Vec2::new(1.0, 0.0), 5.0).unwrap(),
1397 DistanceTrend::Towards
1398 ));
1399 assert!(matches!(
1400 tracker.update(Vec2::new(0.0, 0.0), 6.0).unwrap(),
1401 DistanceTrend::Towards
1402 ));
1403 assert!(matches!(
1404 tracker.update(Vec2::new(-1.0, 0.0), 7.0).unwrap(),
1405 DistanceTrend::Towards
1406 ));
1407 assert!(matches!(
1408 tracker.update(Vec2::new(-2.0, 0.0), 8.0).unwrap(),
1409 DistanceTrend::Away
1410 ));
1411 assert!(matches!(
1412 tracker.update(Vec2::new(-3.0, 0.0), 9.0).unwrap(),
1413 DistanceTrend::Away
1414 ));
1415
1416 let mut tracker2: DistanceTrendTracker<5> = DistanceTrendTracker::default();
1417 assert!(tracker2.update(Vec2::new(100.0, 100.0), 10.0).is_none());
1418 assert!(tracker2.update(Vec2::new(100.0, 200.0), 20.0).is_none());
1419 assert!(matches!(
1420 tracker2.update(Vec2::new(100.0, 300.0), 30.0).unwrap(),
1421 DistanceTrend::Away
1422 ));
1423 assert!(matches!(
1424 tracker2.update(Vec2::new(100.0, 400.0), 40.0).unwrap(),
1425 DistanceTrend::Away
1426 ));
1427 assert!(matches!(
1428 tracker2.update(Vec2::new(100.0, 500.0), 50.0).unwrap(),
1429 DistanceTrend::Away
1430 ));
1431 assert!(matches!(
1432 tracker2.update(Vec2::new(100.0, 490.0), 60.0).unwrap(),
1433 DistanceTrend::Away
1434 ));
1435 assert!(matches!(
1436 tracker2.update(Vec2::new(100.0, 505.0), 70.0).unwrap(),
1437 DistanceTrend::Neutral
1438 ));
1439 assert!(matches!(
1440 tracker2.update(Vec2::new(100.0, 500.0), 80.0).unwrap(),
1441 DistanceTrend::Neutral
1442 ));
1443 assert!(matches!(
1444 tracker2.update(Vec2::new(100.0, 495.0), 90.0).unwrap(),
1445 DistanceTrend::Neutral
1446 ));
1447 assert!(matches!(
1448 tracker2.update(Vec2::new(100.0, 500.0), 100.0).unwrap(),
1449 DistanceTrend::Neutral
1450 ));
1451 assert!(matches!(
1452 tracker2.update(Vec2::new(100.0, 495.0), 110.0).unwrap(),
1453 DistanceTrend::Neutral
1454 ));
1455 assert!(matches!(
1456 tracker2.update(Vec2::new(100.0, 500.0), 120.0).unwrap(),
1457 DistanceTrend::Neutral
1458 ));
1459 assert!(matches!(
1460 tracker2.update(Vec2::new(100.0, 505.0), 130.0).unwrap(),
1461 DistanceTrend::Neutral
1462 ));
1463 assert!(matches!(
1464 tracker2.update(Vec2::new(100.0, 500.0), 140.0).unwrap(),
1465 DistanceTrend::Neutral
1466 ));
1467 assert!(matches!(
1468 tracker2.update(Vec2::new(100.0, 505.0), 150.0).unwrap(),
1469 DistanceTrend::Neutral
1470 ));
1471 }
1472}