1#[cfg(feature = "airship_log")]
2use crate::rule::npc_ai::airship_logger::airship_logger;
3
4use crate::{
5 ai::{Action, NpcCtx, State, finish, just, now, seq},
6 data::actor::SimulationMode,
7};
8use common::{
9 comp::{
10 Content,
11 agent::{BrakingMode, FlightMode},
12 compass::Direction,
13 },
14 util::Dir,
15};
16use rand::prelude::*;
17use std::{cmp::Ordering, collections::VecDeque, time::Duration};
18use vek::*;
19use world::civ::airship_travel::{AirshipDockingApproach, AirshipFlightPhase};
20
21#[cfg(debug_assertions)]
22macro_rules! debug_airships {
23 ($level:expr, $($arg:tt)*) => {
24 match $level {
25 0 => tracing::error!($($arg)*),
26 1 => tracing::warn!($($arg)*),
27 2 => tracing::info!($($arg)*),
28 3 => tracing::debug!($($arg)*),
29 4 => tracing::trace!($($arg)*),
30 _ => tracing::trace!($($arg)*),
31 }
32 }
33}
34
35#[cfg(not(debug_assertions))]
36macro_rules! debug_airships {
37 ($($arg:tt)*) => {};
38}
39
40const AIRSHIP_PROGRESS_UPDATE_INTERVAL: f32 = 5.0; #[derive(Debug, Clone)]
44struct AirshipRouteContext {
45 route_index: usize,
47 current_leg: usize,
49 first_leg: bool,
51 current_leg_approach: Option<AirshipDockingApproach>,
53 cruise_direction: Option<Dir>,
55 next_leg_approach: Option<AirshipDockingApproach>,
57
58 route_time_zero: f64,
62 route_timer: Duration,
64 leg_ctx_time_begin: f64,
66
67 announcements: VecDeque<f32>,
70
71 my_stuck_tracker: Option<StuckAirshipTracker>,
74 stuck_timer: Duration,
76 stuck_backout_pos: Option<Vec3<f32>>,
78}
79
80impl Default for AirshipRouteContext {
81 fn default() -> Self {
82 Self {
83 route_index: usize::MAX,
84 current_leg: 0,
85 first_leg: true,
86 current_leg_approach: None,
87 cruise_direction: None,
88 next_leg_approach: None,
89 route_time_zero: 0.0,
90 route_timer: Duration::default(),
91 leg_ctx_time_begin: 0.0,
92 announcements: VecDeque::new(),
93 my_stuck_tracker: None,
94 stuck_timer: Duration::default(),
95 stuck_backout_pos: None,
96 }
97 }
98}
99
100#[derive(Debug, Default, Clone)]
103struct StuckAirshipTracker {
104 pos_history: Vec<Vec3<f32>>,
107 backout_route: Vec<Vec3<f32>>,
109}
110
111impl StuckAirshipTracker {
112 const BACKOUT_DIST: f32 = 100.0;
114 const BACKOUT_TARGET_DIST: f64 = 50.0;
117 const MAX_POS_HISTORY_SIZE: usize = 5;
119 const NEAR_GROUND_HEIGHT: f32 = 10.0;
121
122 fn add_position(&mut self, new_pos: Vec3<f32>) {
124 if self.pos_history.len() >= StuckAirshipTracker::MAX_POS_HISTORY_SIZE {
125 self.pos_history.remove(0);
126 }
127 self.pos_history.push(new_pos);
128 }
129
130 fn current_backout_pos(&mut self, ctx: &mut NpcCtx) -> Option<Vec3<f32>> {
137 if !self.backout_route.is_empty()
138 && let Some(pos) = self.backout_route.first().cloned()
139 {
140 if ctx
141 .actor
142 .wpos
143 .as_::<f64>()
144 .distance_squared(pos.as_::<f64>())
145 < StuckAirshipTracker::BACKOUT_TARGET_DIST.powi(2)
146 {
147 self.backout_route.remove(0);
148 }
149 Some(pos)
150 } else {
151 self.pos_history.clear();
152 None
153 }
154 }
155
156 fn is_stuck(
170 &mut self,
171 ctx: &mut NpcCtx,
172 current_pos: &Vec3<f32>,
173 target_pos: &Vec2<f32>,
174 ) -> bool {
175 self.add_position(*current_pos);
176 if self.pos_history.len() == StuckAirshipTracker::MAX_POS_HISTORY_SIZE
178 && self.backout_route.is_empty()
179 && let Some(last_pos) = self.pos_history.last()
180 {
181 if self
183 .pos_history
184 .iter()
185 .all(|pos| pos.as_::<f64>().distance_squared(last_pos.as_::<f64>()) < 10.0)
186 {
187 if let Some(backout_dir) = (ctx.actor.wpos.xy() - target_pos)
192 .with_z(0.0)
193 .try_normalized()
194 {
195 let ground = ctx
196 .world
197 .sim()
198 .get_surface_alt_approx(last_pos.xy().map(|e| e as i32));
199 let mut backout_pos =
202 ctx.actor.wpos + backout_dir * StuckAirshipTracker::BACKOUT_DIST;
203 if (ctx.actor.wpos.z - ground).abs() < StuckAirshipTracker::NEAR_GROUND_HEIGHT {
205 backout_pos.z += 50.0;
206 }
207 self.backout_route = vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
208 #[cfg(debug_assertions)]
210 debug_airships!(
211 2,
212 "Airship {} Stuck! at {} {} {}, backout_dir:{:?}, backout_pos:{:?}",
213 format!("{:?}", ctx.actor_id),
214 ctx.actor.wpos.x,
215 ctx.actor.wpos.y,
216 ctx.actor.wpos.z,
217 backout_dir,
218 backout_pos
219 );
220 self.backout_route = vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
221 }
222 }
223 }
224 !self.backout_route.is_empty()
225 }
226}
227
228#[cfg(debug_assertions)]
229fn check_phase_completion_time(
230 ctx: &mut NpcCtx,
231 airship_context: &mut AirshipRouteContext,
232 leg_index: usize,
233 phase: AirshipFlightPhase,
234) {
235 let route_leg = &ctx.world.civs().airships.routes[airship_context.route_index].legs
236 [airship_context.current_leg];
237 let completion_route_time = ctx.time.0 - airship_context.route_time_zero;
239 let time_delta = completion_route_time - route_leg.segments[leg_index].route_time;
240 if time_delta > 10.0 {
241 debug_airships!(
242 4,
243 "Airship {} route {} leg {} completed phase {:?} late by {:.1} seconds, crt {}, \
244 scheduled end time {}",
245 format!("{:?}", ctx.actor_id),
246 airship_context.route_index,
247 airship_context.current_leg,
248 phase,
249 time_delta,
250 completion_route_time,
251 route_leg.segments[leg_index].route_time
252 );
253 } else if time_delta < -10.0 {
254 debug_airships!(
255 4,
256 "Airship {} route {} leg {} completed phase {:?} early by {:.1} seconds, crt {}, \
257 scheduled end time {}",
258 format!("{:?}", ctx.actor_id),
259 airship_context.route_index,
260 airship_context.current_leg,
261 phase,
262 time_delta,
263 completion_route_time,
264 route_leg.segments[leg_index].route_time
265 );
266 } else {
267 debug_airships!(
268 4,
269 "Airship {} route {} leg {} completed phase {:?} on time, crt {}, scheduled end time \
270 {}",
271 format!("{:?}", ctx.actor_id),
272 airship_context.route_index,
273 airship_context.current_leg,
274 phase,
275 completion_route_time,
276 route_leg.segments[leg_index].route_time
277 );
278 }
279}
280
281fn fly_airship(
282 phase: AirshipFlightPhase,
283 approach: AirshipDockingApproach,
284) -> impl Action<AirshipRouteContext> {
285 now(move |ctx, airship_context: &mut AirshipRouteContext| {
286 airship_context.stuck_timer = Duration::from_secs_f32(5.0);
287 airship_context.stuck_backout_pos = None;
288 let route_leg = &ctx.world.civs().airships.routes[airship_context.route_index].legs
289 [airship_context.current_leg];
290
291 ctx.controller.current_airship_pilot_leg = Some((airship_context.current_leg, phase));
292
293 let nominal_speed = ctx.world.civs().airships.nominal_speed;
294 let leg_segment = &route_leg.segments[phase as usize];
295 let leg_ctx_time_begin = airship_context.leg_ctx_time_begin;
297
298 let fly_duration = if airship_context.first_leg {
311 debug_airships!(
323 4,
324 "Airship {} route {} leg {} first leg phase {} from {},{} dur {:.1}s",
325 format!("{:?}", ctx.actor_id),
326 airship_context.route_index,
327 airship_context.current_leg,
328 phase,
329 ctx.actor.wpos.x,
330 ctx.actor.wpos.y,
331 leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)
332 );
333 ((leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)) as f32)
334 .max(1.0)
335 } else {
336 let leg_start_route_time =
340 airship_context.leg_ctx_time_begin - airship_context.route_time_zero;
341 let expected_leg_route_start_time =
344 leg_segment.route_time - leg_segment.duration as f64;
345 let route_time_err = (leg_start_route_time - expected_leg_route_start_time) as f32;
346 if route_time_err > 10.0 {
347 debug_airships!(
348 4,
349 "Airship {} route {} leg {} starting {} late by {:.1}s, rt {:.2}, expected rt \
350 {:.2}, leg dur {:.1}",
351 format!("{:?}", ctx.actor_id),
352 airship_context.route_index,
353 airship_context.current_leg,
354 phase,
355 route_time_err,
356 leg_start_route_time,
357 expected_leg_route_start_time,
358 leg_segment.duration - route_time_err
359 );
360 leg_segment.duration - route_time_err
361 } else if route_time_err < -10.0 {
362 debug_airships!(
363 4,
364 "Airship {} route {} leg {} starting {} early by {:.1}s, rt {:.2}, expected \
365 rt {:.2}, leg dur {:.1}",
366 format!("{:?}", ctx.actor_id),
367 airship_context.route_index,
368 airship_context.current_leg,
369 phase,
370 -route_time_err,
371 leg_start_route_time,
372 expected_leg_route_start_time,
373 leg_segment.duration - route_time_err
374 );
375 leg_segment.duration - route_time_err
376 } else {
377 debug_airships!(
378 4,
379 "Airship {} route {} leg {} starting {} on time, leg dur {:.1}",
380 format!("{:?}", ctx.actor_id),
381 airship_context.route_index,
382 airship_context.current_leg,
383 phase,
384 leg_segment.duration
385 );
386 leg_segment.duration
387 }
388 .max(1.0)
389 };
390
391 let fly_distance = match phase {
392 AirshipFlightPhase::DepartureCruise
393 | AirshipFlightPhase::ApproachCruise
394 | AirshipFlightPhase::Transition => {
395 ctx.actor.wpos.xy().distance(leg_segment.to_world_pos)
396 },
397 AirshipFlightPhase::Descent => ctx.actor.wpos.z - approach.airship_pos.z,
398 AirshipFlightPhase::Ascent => {
399 approach.airship_pos.z + approach.height - ctx.actor.wpos.z
400 },
401 AirshipFlightPhase::Docked => 0.0,
402 };
403
404 let context_end_time = ctx.time.0 + fly_duration as f64;
405
406 match phase {
407 AirshipFlightPhase::DepartureCruise => {
408 airship_context.my_stuck_tracker = Some(StuckAirshipTracker::default());
409 airship_context.route_timer = Duration::from_secs_f32(1.0);
410 airship_context.cruise_direction =
411 Dir::from_unnormalized((approach.midpoint - ctx.actor.wpos.xy()).with_z(0.0));
412 let speed = (fly_distance / fly_duration) / nominal_speed;
415 debug_airships!(
416 4,
417 "Airship {} route {} leg {} DepartureCruise, fly_distance {:.1}, fly_duration \
418 {:.1}s, speed factor {:.3}",
419 format!("{:?}", ctx.actor_id),
420 airship_context.route_index,
421 airship_context.current_leg,
422 fly_distance,
423 fly_duration,
424 speed
425 );
426 fly_airship_inner(
428 AirshipFlightPhase::DepartureCruise,
429 ctx.actor.wpos,
430 leg_segment.to_world_pos.with_z(0.0),
431 50.0,
432 leg_ctx_time_begin,
433 fly_duration,
434 context_end_time,
435 speed,
436 approach.height,
437 true,
438 None,
439 FlightMode::FlyThrough,
440 )
441 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
442 airship_context.leg_ctx_time_begin = ctx.time.0;
443 airship_context.first_leg = false;
444 #[cfg(debug_assertions)]
445 check_phase_completion_time(
446 ctx,
447 airship_context,
448 0,
449 AirshipFlightPhase::DepartureCruise,
450 );
451 }))
452 .map(|_, _| ())
453 .boxed()
454 },
455 AirshipFlightPhase::ApproachCruise => {
456 airship_context.route_timer = Duration::from_secs_f32(1.0);
457 airship_context.cruise_direction = Dir::from_unnormalized(
458 (approach.approach_transition_pos - approach.midpoint).with_z(0.0),
459 );
460 let speed = (fly_distance / fly_duration) / nominal_speed;
463 debug_airships!(
464 4,
465 "Airship {} route {} leg {} ApproachCruise, fly_distance {:.1}, fly_duration \
466 {:.1}s, speed factor {:.3}",
467 format!("{:?}", ctx.actor_id),
468 airship_context.route_index,
469 airship_context.current_leg,
470 fly_distance,
471 fly_duration,
472 speed
473 );
474 fly_airship_inner(
476 AirshipFlightPhase::ApproachCruise,
477 ctx.actor.wpos,
478 leg_segment.to_world_pos.with_z(0.0),
479 50.0,
480 leg_ctx_time_begin,
481 fly_duration,
482 context_end_time,
483 speed,
484 approach.height,
485 true,
486 None,
487 FlightMode::FlyThrough,
488 )
489 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
490 airship_context.leg_ctx_time_begin = ctx.time.0;
491 airship_context.first_leg = false;
492 #[cfg(debug_assertions)]
493 check_phase_completion_time(
494 ctx,
495 airship_context,
496 1,
497 AirshipFlightPhase::ApproachCruise,
498 );
499 }))
500 .map(|_, _| ())
501 .boxed()
502 },
503 AirshipFlightPhase::Transition => {
504 airship_context.route_timer = Duration::from_secs_f32(1.0);
507 let speed = (fly_distance / fly_duration) / nominal_speed;
510 debug_airships!(
511 4,
512 "Airship {} route {} leg {} Transition, fly_distance {:.1}, fly_duration \
513 {:.1}s, speed factor {:.3}",
514 format!("{:?}", ctx.actor_id),
515 airship_context.route_index,
516 airship_context.current_leg,
517 fly_distance,
518 fly_duration,
519 speed
520 );
521 fly_airship_inner(
523 AirshipFlightPhase::Transition,
524 ctx.actor.wpos,
525 leg_segment
526 .to_world_pos
527 .with_z(approach.airship_pos.z + approach.height),
528 20.0,
529 leg_ctx_time_begin,
530 fly_duration,
531 context_end_time,
532 speed,
533 approach.height,
534 true,
535 Some(approach.airship_direction),
536 FlightMode::Braking(BrakingMode::Normal),
537 )
538 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
539 airship_context.leg_ctx_time_begin = ctx.time.0;
540 airship_context.first_leg = false;
541 #[cfg(debug_assertions)]
542 check_phase_completion_time(
543 ctx,
544 airship_context,
545 2,
546 AirshipFlightPhase::Transition,
547 );
548 }))
549 .map(|_, _| ())
550 .boxed()
551 },
552 AirshipFlightPhase::Descent => {
553 airship_context.route_timer = Duration::from_secs_f32(1.0);
555 let desired_offset = ctx.rng.random_range(4.0..6.0);
566 let descent_offset = if fly_distance > desired_offset {
567 desired_offset
568 } else {
569 0.0
570 };
571 let descent_dist = fly_distance - descent_offset;
572 let step1_dist = descent_dist * ctx.rng.random_range(0.7..0.85);
574 let step1_dur = fly_duration * ctx.rng.random_range(0.4..0.55);
575 let speed_mult = if matches!(ctx.actor.mode, SimulationMode::Loaded) {
577 ctx.rng.random_range(1.1..1.25)
578 } else {
579 1.0
580 };
581 let speed1 = (step1_dist / step1_dur) / nominal_speed * speed_mult;
582 let speed2 = ((descent_dist - step1_dist) / (fly_duration - step1_dur))
583 / nominal_speed
584 * speed_mult;
585 debug_airships!(
586 4,
587 "Airship {} route {} leg {} Descent, fly_distance {:.1}, fly_duration {:.1}s, \
588 desired_offset {:.1}, descent_offset {:.1}, descent_dist {:.1}, step1_dist \
589 {:.1}, step1_dur {:.3}s, speedmult {:.1}, speed1 {:.3}, speed2 {:.3}",
590 format!("{:?}", ctx.actor_id),
591 airship_context.route_index,
592 airship_context.current_leg,
593 fly_distance,
594 fly_duration,
595 desired_offset,
596 descent_offset,
597 descent_dist,
598 step1_dist,
599 step1_dur,
600 speed_mult,
601 speed1,
602 speed2
603 );
604
605 fly_airship_inner(
607 AirshipFlightPhase::Descent,
608 ctx.actor.wpos,
609 ctx.actor.wpos - Vec3::unit_z() * step1_dist,
610 20.0,
611 leg_ctx_time_begin,
612 step1_dur,
613 context_end_time - step1_dur as f64,
614 speed1,
615 0.0,
616 false,
617 Some(approach.airship_direction),
618 FlightMode::Braking(BrakingMode::Normal),
619 )
620 .then (fly_airship_inner(
621 AirshipFlightPhase::Descent,
622 ctx.actor.wpos - Vec3::unit_z() * step1_dist,
623 approach.airship_pos + Vec3::unit_z() * descent_offset,
624 20.0,
625 leg_ctx_time_begin + step1_dur as f64,
626 fly_duration - step1_dur,
627 context_end_time,
628 speed2,
629 0.0,
630 false,
631 Some(approach.airship_direction),
632 FlightMode::Braking(BrakingMode::Precise),
633 ))
634 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
636 airship_context.leg_ctx_time_begin = ctx.time.0;
637 airship_context.first_leg = false;
638 #[cfg(debug_assertions)]
639 check_phase_completion_time(ctx, airship_context, 3, AirshipFlightPhase::Descent);
640 log_airship_position(ctx, airship_context.route_index, &AirshipFlightPhase::Docked);
641 ctx.controller
642 .say(None, Content::localized("npc-speech-pilot-landed"));
643 }))
644 .map(|_, _| ()).boxed()
645 },
646 AirshipFlightPhase::Docked => {
647 debug_airships!(
648 4,
649 "Airship {} route {} leg {} Docked ctx time {:.1}, \
650 airship_context.leg_ctx_time_begin {:.1}, docking duration {:.1}s",
651 format!("{:?}", ctx.actor_id),
652 airship_context.route_index,
653 airship_context.current_leg,
654 leg_ctx_time_begin,
655 airship_context.leg_ctx_time_begin,
656 fly_duration,
657 );
658 let announcement_times = {
675 let mut times = Vec::new();
676 if fly_duration > 10.0 {
677 let first_time = ctx.rng.random_range(5.0..8.0);
678 let last_time = fly_duration - ctx.rng.random_range(10.0..12.0);
679 if first_time + 10.0 > last_time {
680 let mid_time = fly_duration / 2.0;
682 if mid_time > 5.0 && (fly_duration - mid_time) > 10.0 {
683 times.push(mid_time);
684 }
685 } else {
686 times.push(first_time);
688 let mut last_time = first_time;
689 let mut t = first_time + ctx.rng.random_range(10.0..16.0);
690 while t < fly_duration - 10.0 {
691 times.push(t);
692 last_time = t;
693 t += ctx.rng.random_range(10.0..16.0);
694 }
695 if last_time < fly_duration - 22.0 {
696 times.push(last_time + 12.0);
698 }
699 times.sort_by(|a, b| {
700 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
701 });
702 }
703 }
704 times
705 };
706 airship_context.announcements = announcement_times.into_iter().collect();
707 now(move |ctx, airship_context: &mut AirshipRouteContext| {
708 let dock_ctx_end_time = if let Some(at) = airship_context.announcements.front()
711 {
712 leg_ctx_time_begin + *at as f64
713 } else {
714 context_end_time
715 };
716 fly_airship_inner(
717 AirshipFlightPhase::Docked,
718 ctx.actor.wpos,
719 approach.airship_pos,
720 5.0,
721 leg_ctx_time_begin,
722 fly_duration,
723 dock_ctx_end_time,
724 0.75,
725 0.0,
726 false,
727 Some(approach.airship_direction),
728 FlightMode::Braking(BrakingMode::Precise),
729 )
730 .then(just(
731 |ctx, airship_context: &mut AirshipRouteContext| {
732 if airship_context.announcements.pop_front().is_some() {
736 let (dst_site_name, dst_site_dir) = if let Some(next_leg_approach) =
738 airship_context.next_leg_approach
739 {
740 (
741 ctx.index
742 .sites
743 .get(next_leg_approach.site_id)
744 .name()
745 .unwrap_or("Unknown Site")
746 .to_string(),
747 Direction::from_dir(
748 next_leg_approach.approach_transition_pos
749 - ctx.actor.wpos.xy(),
750 )
751 .localize_npc(),
752 )
753 } else {
754 ("Unknown Site".to_string(), Direction::North.localize_npc())
755 };
756 ctx.controller.say(
757 None,
758 Content::localized("npc-speech-pilot-announce_next")
759 .with_arg("dir", dst_site_dir)
760 .with_arg("dst", dst_site_name),
761 );
762 log_airship_position(
763 ctx,
764 airship_context.route_index,
765 &AirshipFlightPhase::Docked,
766 );
767 }
768 },
769 ))
770 })
771 .repeat()
772 .stop_if(move |ctx: &mut NpcCtx| ctx.time.0 >= context_end_time)
773 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
774 airship_context.leg_ctx_time_begin = ctx.time.0;
775 airship_context.first_leg = false;
776 #[cfg(debug_assertions)]
777 check_phase_completion_time(
778 ctx,
779 airship_context,
780 4,
781 AirshipFlightPhase::Docked,
782 );
783 log_airship_position(
784 ctx,
785 airship_context.route_index,
786 &AirshipFlightPhase::Docked,
787 );
788 }))
789 .map(|_, _| ())
790 .boxed()
791 },
792 AirshipFlightPhase::Ascent => {
793 log_airship_position(
794 ctx,
795 airship_context.route_index,
796 &AirshipFlightPhase::Ascent,
797 );
798 airship_context.route_timer = Duration::from_secs_f32(0.5);
799 let speed = (fly_distance / fly_duration) / nominal_speed;
801 debug_airships!(
802 4,
803 "Airship {} route {} leg {} Ascent, fly_distance {:.1}, fly_duration {:.1}s, \
804 speed factor {:.3}",
805 format!("{:?}", ctx.actor_id),
806 airship_context.route_index,
807 airship_context.current_leg,
808 fly_distance,
809 fly_duration,
810 speed
811 );
812 let src_site_name = ctx
813 .index
814 .sites
815 .get(approach.site_id)
816 .name()
817 .unwrap_or("Unknown Site")
818 .to_string();
819 let dst_site_name =
820 if let Some(next_leg_approach) = airship_context.next_leg_approach {
821 ctx.index
822 .sites
823 .get(next_leg_approach.site_id)
824 .name()
825 .unwrap_or("Unknown Site")
826 .to_string()
827 } else {
828 "Unknown Site".to_string()
829 };
830 ctx.controller.say(
831 None,
832 Content::localized("npc-speech-pilot-takeoff")
833 .with_arg("src", src_site_name)
834 .with_arg("dst", dst_site_name),
835 );
836 fly_airship_inner(
837 AirshipFlightPhase::Ascent,
838 ctx.actor.wpos,
839 approach.airship_pos + Vec3::unit_z() * approach.height,
840 20.0,
841 leg_ctx_time_begin,
842 fly_duration,
843 context_end_time,
844 speed,
845 0.0,
846 false,
847 Some(approach.airship_direction),
848 FlightMode::Braking(BrakingMode::Normal),
849 )
850 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
851 airship_context.leg_ctx_time_begin = ctx.time.0;
852 airship_context.first_leg = false;
853 #[cfg(debug_assertions)]
854 check_phase_completion_time(
855 ctx,
856 airship_context,
857 5,
858 AirshipFlightPhase::Ascent,
859 );
860 }))
861 .map(|_, _| ())
862 .boxed()
863 },
864 }
865 })
866}
867
868fn fly_airship_inner(
870 phase: AirshipFlightPhase,
871 from: Vec3<f32>,
872 to: Vec3<f32>,
873 goal_dist: f32,
874 leg_ctx_time_begin: f64,
875 leg_duration: f32,
876 context_tgt_end_time: f64,
877 speed_factor: f32,
878 height_offset: f32,
879 with_terrain_following: bool,
880 direction_override: Option<Dir>,
881 flight_mode: FlightMode,
882) -> impl Action<AirshipRouteContext> {
883 just(move |ctx, airship_context: &mut AirshipRouteContext| {
884 let stuck_tracker_target_loc = to.xy();
888
889 let nominal_pos = match phase {
891 AirshipFlightPhase::DepartureCruise
892 | AirshipFlightPhase::ApproachCruise
893 | AirshipFlightPhase::Transition => {
894 let route_interpolation_ratio =
896 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
897 (from.xy() + (to.xy() - from.xy()) * route_interpolation_ratio).with_z(to.z)
898 },
899 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
900 let route_interpolation_ratio =
904 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
905 let nominal_z = from.z + (to.z - from.z) * route_interpolation_ratio;
906 to.xy().with_z(nominal_z)
907 },
908 _ => {
909 to
911 },
912 };
913
914 let timer = airship_context
916 .route_timer
917 .checked_sub(Duration::from_secs_f32(ctx.dt));
918 airship_context.route_timer =
920 timer.unwrap_or(Duration::from_secs_f32(AIRSHIP_PROGRESS_UPDATE_INTERVAL));
921 if timer.is_none() {
922 #[cfg(feature = "airship_log")]
925 {
926 let distance_to_nominal = match phase {
928 AirshipFlightPhase::DepartureCruise
929 | AirshipFlightPhase::ApproachCruise
930 | AirshipFlightPhase::Transition => {
931 ctx.actor
933 .wpos
934 .xy()
935 .as_::<f64>()
936 .distance(nominal_pos.xy().as_())
937 },
938 _ => ctx.actor.wpos.as_::<f64>().distance(nominal_pos.as_()),
939 };
940 log_airship_position_plus(
941 ctx,
942 airship_context.route_index,
943 &phase,
944 distance_to_nominal,
945 0.0,
946 );
947 }
948
949 if matches!(
952 phase,
953 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
954 ) {
955 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
957 && stuck_tracker.is_stuck(ctx, &ctx.actor.wpos, &stuck_tracker_target_loc)
958 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
959 {
960 airship_context.stuck_backout_pos = Some(backout_pos);
961 } else if airship_context.stuck_backout_pos.is_some() {
962 #[cfg(debug_assertions)]
963 debug_airships!(
964 2,
965 "{:?} unstuck at pos: {} {}",
966 ctx.actor_id,
967 ctx.actor.wpos.x as i32,
968 ctx.actor.wpos.y as i32,
969 );
970 airship_context.stuck_backout_pos = None;
971 };
972 airship_context.cruise_direction =
974 Dir::from_unnormalized((to.xy() - ctx.actor.wpos.xy()).with_z(0.0));
975 }
976 }
977 if let Some(backout_pos) = airship_context.stuck_backout_pos {
979 ctx.controller.do_goto_with_height_and_dir(
981 backout_pos,
982 1.5,
983 None,
984 None,
985 FlightMode::Braking(BrakingMode::Normal),
986 );
987 } else {
988 let height_offset_opt = if with_terrain_following {
990 Some(height_offset)
991 } else {
992 None
993 };
994 let dir_opt = if direction_override.is_some() {
1002 direction_override
1003 } else if matches!(
1004 phase,
1005 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
1006 ) {
1007 airship_context.cruise_direction
1008 } else {
1009 None
1010 };
1011 ctx.controller.do_goto_with_height_and_dir(
1012 nominal_pos,
1013 speed_factor,
1014 height_offset_opt,
1015 dir_opt,
1016 flight_mode,
1017 );
1018 }
1019 })
1020 .repeat()
1021 .boxed()
1022 .stop_if(move |ctx: &mut NpcCtx| {
1023 match phase {
1024 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
1025 ctx.time.0 >= context_tgt_end_time
1026 || ctx.actor.wpos.as_::<f64>().distance_squared(to.as_())
1027 < (goal_dist as f64).powi(2)
1028 },
1029 AirshipFlightPhase::Docked => {
1030 if ctx.time.0 >= context_tgt_end_time {
1032 debug_airships!(
1033 4,
1034 "Airship {} docking phase complete time now {:.1} >= context_tgt_end_time \
1035 {:.1}",
1036 format!("{:?}", ctx.actor_id),
1037 ctx.time.0,
1038 context_tgt_end_time,
1039 );
1040 }
1041 ctx.time.0 >= context_tgt_end_time
1042 },
1043 _ => {
1044 if flight_mode == FlightMode::FlyThrough {
1045 ctx.actor
1047 .wpos
1048 .xy()
1049 .as_::<f64>()
1050 .distance_squared(to.xy().as_())
1051 < (goal_dist as f64).powi(2)
1052 } else {
1053 ctx.actor.wpos.as_::<f64>().distance_squared(to.as_())
1055 < (goal_dist as f64).powi(2)
1056 }
1057 },
1058 }
1059 })
1060 .debug(move || {
1061 format!(
1062 "fly airship, phase:{:?}, tgt pos:({}, {}, {}), goal dist:{}, leg dur: {}, initial \
1063 speed:{}, height:{}, terrain following:{}, FlightMode:{:?}",
1064 phase,
1065 to.x,
1066 to.y,
1067 to.z,
1068 goal_dist,
1069 leg_duration,
1070 speed_factor,
1071 height_offset,
1072 with_terrain_following,
1073 flight_mode,
1074 )
1075 })
1076 .map(|_, _| ())
1077}
1078
1079pub fn pilot_airship<S: State>() -> impl Action<S> {
1084 now(move |ctx, airship_context: &mut AirshipRouteContext| {
1085 if let Some((route_index, start_leg_index)) =
1087 ctx.data.airship_sim.assigned_routes.get(&ctx.actor_id)
1088 {
1089 let is_initial_startup = airship_context.route_index == usize::MAX;
1092 if is_initial_startup {
1093 setup_airship_route_context(ctx, airship_context, route_index, start_leg_index);
1094 } else {
1095 airship_context.current_leg = ctx
1097 .world
1098 .civs()
1099 .airships
1100 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1101 if airship_context.current_leg == 0 {
1102 airship_context.route_time_zero +=
1105 ctx.world.civs().airships.routes[airship_context.route_index].total_time;
1106 debug_airships!(
1107 4,
1108 "Airship {} route {} completed full route, route time zero now {:.1}, \
1109 ctx.time.0 - route_time_zero = {:.3}",
1110 format!("{:?}", ctx.actor_id),
1111 airship_context.route_index,
1112 airship_context.route_time_zero,
1113 ctx.time.0 - airship_context.route_time_zero
1114 );
1115 } else {
1116 debug_airships!(
1117 4,
1118 "Airship {} route {} starting next leg {}, current route time {:.1}",
1119 format!("{:?}", ctx.actor_id),
1120 airship_context.route_index,
1121 airship_context.current_leg,
1122 ctx.time.0 - airship_context.route_time_zero
1123 );
1124 }
1125 }
1126
1127 airship_context.current_leg_approach =
1130 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1131 airship_context.route_index,
1132 airship_context.current_leg,
1133 &ctx.world.sim().map_size_lg(),
1134 ));
1135
1136 if airship_context.current_leg_approach.is_none() {
1137 tracing::error!(
1138 "Airship pilot {:?} approach not found for route {} leg {}, stopping \
1139 pilot_airship loop.",
1140 ctx.actor_id,
1141 airship_context.route_index,
1142 airship_context.current_leg
1143 );
1144 return finish().map(|_, _| ()).boxed();
1145 }
1146
1147 let next_leg_index = ctx
1150 .world
1151 .civs()
1152 .airships
1153 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1154 airship_context.next_leg_approach =
1155 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1156 airship_context.route_index,
1157 next_leg_index,
1158 &ctx.world.sim().map_size_lg(),
1159 ));
1160 if airship_context.next_leg_approach.is_none() {
1161 tracing::warn!(
1162 "Airship pilot {:?} approach not found for next route {} leg {}",
1163 ctx.actor_id,
1164 airship_context.route_index,
1165 next_leg_index
1166 );
1167 }
1168
1169 if is_initial_startup {
1171 let my_route = &ctx.world.civs().airships.routes[airship_context.route_index];
1175 if let Some(my_spawn_loc) = my_route.spawning_locations.iter().min_by(|a, b| {
1176 let dist_a = ctx
1177 .actor
1178 .wpos
1179 .xy()
1180 .as_::<f64>()
1181 .distance_squared(a.pos.as_());
1182 let dist_b = ctx
1183 .actor
1184 .wpos
1185 .xy()
1186 .as_::<f64>()
1187 .distance_squared(b.pos.as_());
1188 dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal)
1189 }) {
1190 airship_context.route_time_zero = ctx.time.0 - my_spawn_loc.spawn_route_time;
1195 airship_context.leg_ctx_time_begin = ctx.time.0;
1196 airship_context.first_leg = true;
1197 debug_airships!(
1198 4,
1199 "Airship {} route {} leg {}, initial start up on phase {:?}, setting \
1200 route_time_zero to {:.1} (ctx.time.0 {} - my_spawn_loc.spawn_route_time \
1201 {})",
1202 format!("{:?}", ctx.actor_id),
1203 airship_context.route_index,
1204 airship_context.current_leg,
1205 my_spawn_loc.flight_phase,
1206 airship_context.route_time_zero,
1207 ctx.time.0,
1208 my_spawn_loc.spawn_route_time,
1209 );
1210 initial_flight_sequence(my_spawn_loc.flight_phase)
1211 .map(|_, _| ())
1212 .boxed()
1213 } else {
1214 tracing::error!(
1216 "Airship pilot {:?} spawning location not found for route {} leg {}",
1217 ctx.actor_id,
1218 airship_context.route_index,
1219 next_leg_index
1220 );
1221 finish().map(|_, _| ()).boxed()
1222 }
1223 } else {
1224 nominal_flight_sequence().map(|_, _| ()).boxed()
1225 }
1226 } else {
1227 finish().map(|_, _| ()).boxed()
1231 }
1232 })
1233 .repeat()
1234 .with_state(AirshipRouteContext::default())
1235 .map(|_, _| ())
1236}
1237
1238fn setup_airship_route_context(
1239 _ctx: &mut NpcCtx,
1240 route_context: &mut AirshipRouteContext,
1241 route_index: &usize,
1242 leg_index: &usize,
1243) {
1244 route_context.route_index = *route_index;
1245 route_context.current_leg = *leg_index;
1246
1247 #[cfg(debug_assertions)]
1248 {
1249 let current_approach = _ctx.world.civs().airships.approach_for_route_and_leg(
1250 route_context.route_index,
1251 route_context.current_leg,
1252 &_ctx.world.sim().map_size_lg(),
1253 );
1254 debug_airships!(
1255 4,
1256 "Server startup, airship pilot {:?} starting on route {} leg {}, target dock: {} {}",
1257 _ctx.actor_id,
1258 route_context.route_index,
1259 route_context.current_leg,
1260 current_approach.airship_pos.x as i32,
1261 current_approach.airship_pos.y as i32,
1262 );
1263 }
1264}
1265
1266fn initial_flight_sequence(start_phase: AirshipFlightPhase) -> impl Action<AirshipRouteContext> {
1267 now(move |_, airship_context: &mut AirshipRouteContext| {
1268 let approach = airship_context.current_leg_approach.unwrap();
1269 let phases = match start_phase {
1270 AirshipFlightPhase::DepartureCruise => vec![
1271 (AirshipFlightPhase::DepartureCruise, approach),
1272 (AirshipFlightPhase::ApproachCruise, approach),
1273 (AirshipFlightPhase::Transition, approach),
1274 (AirshipFlightPhase::Descent, approach),
1275 (AirshipFlightPhase::Docked, approach),
1276 (AirshipFlightPhase::Ascent, approach),
1277 ],
1278 AirshipFlightPhase::ApproachCruise => vec![
1279 (AirshipFlightPhase::ApproachCruise, approach),
1280 (AirshipFlightPhase::Transition, approach),
1281 (AirshipFlightPhase::Descent, approach),
1282 (AirshipFlightPhase::Docked, approach),
1283 (AirshipFlightPhase::Ascent, approach),
1284 ],
1285 AirshipFlightPhase::Transition => vec![
1286 (AirshipFlightPhase::Transition, approach),
1287 (AirshipFlightPhase::Descent, approach),
1288 (AirshipFlightPhase::Docked, approach),
1289 (AirshipFlightPhase::Ascent, approach),
1290 ],
1291 AirshipFlightPhase::Descent => vec![
1292 (AirshipFlightPhase::Descent, approach),
1293 (AirshipFlightPhase::Docked, approach),
1294 (AirshipFlightPhase::Ascent, approach),
1295 ],
1296 AirshipFlightPhase::Docked => {
1297 vec![
1299 (AirshipFlightPhase::Docked, approach),
1300 (AirshipFlightPhase::Ascent, approach),
1301 ]
1302 },
1303 AirshipFlightPhase::Ascent => vec![(AirshipFlightPhase::Ascent, approach)],
1304 };
1305 seq(phases
1306 .into_iter()
1307 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1308 })
1309}
1310
1311fn nominal_flight_sequence() -> impl Action<AirshipRouteContext> {
1312 now(move |_, airship_context: &mut AirshipRouteContext| {
1313 let approach = airship_context.current_leg_approach.unwrap();
1314 let phases = vec![
1315 (AirshipFlightPhase::DepartureCruise, approach),
1316 (AirshipFlightPhase::ApproachCruise, approach),
1317 (AirshipFlightPhase::Transition, approach),
1318 (AirshipFlightPhase::Descent, approach),
1319 (AirshipFlightPhase::Docked, approach),
1320 (AirshipFlightPhase::Ascent, approach),
1321 ];
1322 seq(phases
1323 .into_iter()
1324 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1325 })
1326}
1327
1328#[cfg(feature = "airship_log")]
1329fn log_airship_position(ctx: &NpcCtx, route_index: usize, phase: &AirshipFlightPhase) {
1331 log_airship_position_plus(ctx, route_index, phase, 0.0, 0.0);
1332}
1333
1334#[cfg(feature = "airship_log")]
1335fn log_airship_position_plus(
1336 ctx: &NpcCtx,
1337 route_index: usize,
1338 phase: &AirshipFlightPhase,
1339 value1: f64,
1340 value2: f64,
1341) {
1342 if let Ok(mut logger) = airship_logger() {
1343 logger.log_position(
1344 ctx.actor_id,
1345 ctx.index.seed,
1346 route_index,
1347 phase,
1348 ctx.time.0,
1349 ctx.actor.wpos,
1350 matches!(ctx.actor.mode, SimulationMode::Loaded),
1351 value1,
1352 value2,
1353 );
1354 } else {
1355 tracing::warn!("Failed to log airship position for {:?}", ctx.actor_id);
1356 }
1357}
1358
1359#[cfg(not(feature = "airship_log"))]
1360fn log_airship_position(_: &NpcCtx, _: usize, _: &AirshipFlightPhase) {}