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::npc::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.npc.wpos.as_::<f64>().distance_squared(pos.as_::<f64>())
141 < StuckAirshipTracker::BACKOUT_TARGET_DIST.powi(2)
142 {
143 self.backout_route.remove(0);
144 }
145 Some(pos)
146 } else {
147 self.pos_history.clear();
148 None
149 }
150 }
151
152 fn is_stuck(
166 &mut self,
167 ctx: &mut NpcCtx,
168 current_pos: &Vec3<f32>,
169 target_pos: &Vec2<f32>,
170 ) -> bool {
171 self.add_position(*current_pos);
172 if self.pos_history.len() == StuckAirshipTracker::MAX_POS_HISTORY_SIZE
174 && self.backout_route.is_empty()
175 && let Some(last_pos) = self.pos_history.last()
176 {
177 if self
179 .pos_history
180 .iter()
181 .all(|pos| pos.as_::<f64>().distance_squared(last_pos.as_::<f64>()) < 10.0)
182 {
183 if let Some(backout_dir) = (ctx.npc.wpos.xy() - target_pos)
188 .with_z(0.0)
189 .try_normalized()
190 {
191 let ground = ctx
192 .world
193 .sim()
194 .get_surface_alt_approx(last_pos.xy().map(|e| e as i32));
195 let mut backout_pos =
198 ctx.npc.wpos + backout_dir * StuckAirshipTracker::BACKOUT_DIST;
199 if (ctx.npc.wpos.z - ground).abs() < StuckAirshipTracker::NEAR_GROUND_HEIGHT {
201 backout_pos.z += 50.0;
202 }
203 self.backout_route = vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
204 #[cfg(debug_assertions)]
206 debug_airships!(
207 2,
208 "Airship {} Stuck! at {} {} {}, backout_dir:{:?}, backout_pos:{:?}",
209 format!("{:?}", ctx.npc_id),
210 ctx.npc.wpos.x,
211 ctx.npc.wpos.y,
212 ctx.npc.wpos.z,
213 backout_dir,
214 backout_pos
215 );
216 self.backout_route = vec![backout_pos, backout_pos + Vec3::unit_z() * 200.0];
217 }
218 }
219 }
220 !self.backout_route.is_empty()
221 }
222}
223
224#[cfg(debug_assertions)]
225fn check_phase_completion_time(
226 ctx: &mut NpcCtx,
227 airship_context: &mut AirshipRouteContext,
228 leg_index: usize,
229 phase: AirshipFlightPhase,
230) {
231 let route_leg = &ctx.world.civs().airships.routes[airship_context.route_index].legs
232 [airship_context.current_leg];
233 let completion_route_time = ctx.time.0 - airship_context.route_time_zero;
235 let time_delta = completion_route_time - route_leg.segments[leg_index].route_time;
236 if time_delta > 10.0 {
237 debug_airships!(
238 4,
239 "Airship {} route {} leg {} completed phase {:?} late by {:.1} seconds, crt {}, \
240 scheduled end time {}",
241 format!("{:?}", ctx.npc_id),
242 airship_context.route_index,
243 airship_context.current_leg,
244 phase,
245 time_delta,
246 completion_route_time,
247 route_leg.segments[leg_index].route_time
248 );
249 } else if time_delta < -10.0 {
250 debug_airships!(
251 4,
252 "Airship {} route {} leg {} completed phase {:?} early by {:.1} seconds, crt {}, \
253 scheduled end time {}",
254 format!("{:?}", ctx.npc_id),
255 airship_context.route_index,
256 airship_context.current_leg,
257 phase,
258 time_delta,
259 completion_route_time,
260 route_leg.segments[leg_index].route_time
261 );
262 } else {
263 debug_airships!(
264 4,
265 "Airship {} route {} leg {} completed phase {:?} on time, crt {}, scheduled end time \
266 {}",
267 format!("{:?}", ctx.npc_id),
268 airship_context.route_index,
269 airship_context.current_leg,
270 phase,
271 completion_route_time,
272 route_leg.segments[leg_index].route_time
273 );
274 }
275}
276
277fn fly_airship(
278 phase: AirshipFlightPhase,
279 approach: AirshipDockingApproach,
280) -> impl Action<AirshipRouteContext> {
281 now(move |ctx, airship_context: &mut AirshipRouteContext| {
282 airship_context.stuck_timer = Duration::from_secs_f32(5.0);
283 airship_context.stuck_backout_pos = None;
284 let route_leg = &ctx.world.civs().airships.routes[airship_context.route_index].legs
285 [airship_context.current_leg];
286
287 ctx.controller.current_airship_pilot_leg = Some((airship_context.current_leg, phase));
288
289 let nominal_speed = ctx.world.civs().airships.nominal_speed;
290 let leg_segment = &route_leg.segments[phase as usize];
291 let leg_ctx_time_begin = airship_context.leg_ctx_time_begin;
293
294 let fly_duration = if airship_context.first_leg {
307 debug_airships!(
319 4,
320 "Airship {} route {} leg {} first leg phase {} from {},{} dur {:.1}s",
321 format!("{:?}", ctx.npc_id),
322 airship_context.route_index,
323 airship_context.current_leg,
324 phase,
325 ctx.npc.wpos.x,
326 ctx.npc.wpos.y,
327 leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)
328 );
329 ((leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)) as f32)
330 .max(1.0)
331 } else {
332 let leg_start_route_time =
336 airship_context.leg_ctx_time_begin - airship_context.route_time_zero;
337 let expected_leg_route_start_time =
340 leg_segment.route_time - leg_segment.duration as f64;
341 let route_time_err = (leg_start_route_time - expected_leg_route_start_time) as f32;
342 if route_time_err > 10.0 {
343 debug_airships!(
344 4,
345 "Airship {} route {} leg {} starting {} late by {:.1}s, rt {:.2}, expected rt \
346 {:.2}, leg dur {:.1}",
347 format!("{:?}", ctx.npc_id),
348 airship_context.route_index,
349 airship_context.current_leg,
350 phase,
351 route_time_err,
352 leg_start_route_time,
353 expected_leg_route_start_time,
354 leg_segment.duration - route_time_err
355 );
356 leg_segment.duration - route_time_err
357 } else if route_time_err < -10.0 {
358 debug_airships!(
359 4,
360 "Airship {} route {} leg {} starting {} early by {:.1}s, rt {:.2}, expected \
361 rt {:.2}, leg dur {:.1}",
362 format!("{:?}", ctx.npc_id),
363 airship_context.route_index,
364 airship_context.current_leg,
365 phase,
366 -route_time_err,
367 leg_start_route_time,
368 expected_leg_route_start_time,
369 leg_segment.duration - route_time_err
370 );
371 leg_segment.duration - route_time_err
372 } else {
373 debug_airships!(
374 4,
375 "Airship {} route {} leg {} starting {} on time, leg dur {:.1}",
376 format!("{:?}", ctx.npc_id),
377 airship_context.route_index,
378 airship_context.current_leg,
379 phase,
380 leg_segment.duration
381 );
382 leg_segment.duration
383 }
384 .max(1.0)
385 };
386
387 let fly_distance = match phase {
388 AirshipFlightPhase::DepartureCruise
389 | AirshipFlightPhase::ApproachCruise
390 | AirshipFlightPhase::Transition => {
391 ctx.npc.wpos.xy().distance(leg_segment.to_world_pos)
392 },
393 AirshipFlightPhase::Descent => ctx.npc.wpos.z - approach.airship_pos.z,
394 AirshipFlightPhase::Ascent => approach.airship_pos.z + approach.height - ctx.npc.wpos.z,
395 AirshipFlightPhase::Docked => 0.0,
396 };
397
398 let context_end_time = ctx.time.0 + fly_duration as f64;
399
400 match phase {
401 AirshipFlightPhase::DepartureCruise => {
402 airship_context.my_stuck_tracker = Some(StuckAirshipTracker::default());
403 airship_context.route_timer = Duration::from_secs_f32(1.0);
404 airship_context.cruise_direction =
405 Dir::from_unnormalized((approach.midpoint - ctx.npc.wpos.xy()).with_z(0.0));
406 let speed = (fly_distance / fly_duration) / nominal_speed;
409 debug_airships!(
410 4,
411 "Airship {} route {} leg {} DepartureCruise, fly_distance {:.1}, fly_duration \
412 {:.1}s, speed factor {:.3}",
413 format!("{:?}", ctx.npc_id),
414 airship_context.route_index,
415 airship_context.current_leg,
416 fly_distance,
417 fly_duration,
418 speed
419 );
420 fly_airship_inner(
422 AirshipFlightPhase::DepartureCruise,
423 ctx.npc.wpos,
424 leg_segment.to_world_pos.with_z(0.0),
425 50.0,
426 leg_ctx_time_begin,
427 fly_duration,
428 context_end_time,
429 speed,
430 approach.height,
431 true,
432 None,
433 FlightMode::FlyThrough,
434 )
435 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
436 airship_context.leg_ctx_time_begin = ctx.time.0;
437 airship_context.first_leg = false;
438 #[cfg(debug_assertions)]
439 check_phase_completion_time(
440 ctx,
441 airship_context,
442 0,
443 AirshipFlightPhase::DepartureCruise,
444 );
445 }))
446 .map(|_, _| ())
447 .boxed()
448 },
449 AirshipFlightPhase::ApproachCruise => {
450 airship_context.route_timer = Duration::from_secs_f32(1.0);
451 airship_context.cruise_direction = Dir::from_unnormalized(
452 (approach.approach_transition_pos - approach.midpoint).with_z(0.0),
453 );
454 let speed = (fly_distance / fly_duration) / nominal_speed;
457 debug_airships!(
458 4,
459 "Airship {} route {} leg {} ApproachCruise, fly_distance {:.1}, fly_duration \
460 {:.1}s, speed factor {:.3}",
461 format!("{:?}", ctx.npc_id),
462 airship_context.route_index,
463 airship_context.current_leg,
464 fly_distance,
465 fly_duration,
466 speed
467 );
468 fly_airship_inner(
470 AirshipFlightPhase::ApproachCruise,
471 ctx.npc.wpos,
472 leg_segment.to_world_pos.with_z(0.0),
473 50.0,
474 leg_ctx_time_begin,
475 fly_duration,
476 context_end_time,
477 speed,
478 approach.height,
479 true,
480 None,
481 FlightMode::FlyThrough,
482 )
483 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
484 airship_context.leg_ctx_time_begin = ctx.time.0;
485 airship_context.first_leg = false;
486 #[cfg(debug_assertions)]
487 check_phase_completion_time(
488 ctx,
489 airship_context,
490 1,
491 AirshipFlightPhase::ApproachCruise,
492 );
493 }))
494 .map(|_, _| ())
495 .boxed()
496 },
497 AirshipFlightPhase::Transition => {
498 airship_context.route_timer = Duration::from_secs_f32(1.0);
501 let speed = (fly_distance / fly_duration) / nominal_speed;
504 debug_airships!(
505 4,
506 "Airship {} route {} leg {} Transition, fly_distance {:.1}, fly_duration \
507 {:.1}s, speed factor {:.3}",
508 format!("{:?}", ctx.npc_id),
509 airship_context.route_index,
510 airship_context.current_leg,
511 fly_distance,
512 fly_duration,
513 speed
514 );
515 fly_airship_inner(
517 AirshipFlightPhase::Transition,
518 ctx.npc.wpos,
519 leg_segment
520 .to_world_pos
521 .with_z(approach.airship_pos.z + approach.height),
522 20.0,
523 leg_ctx_time_begin,
524 fly_duration,
525 context_end_time,
526 speed,
527 approach.height,
528 true,
529 Some(approach.airship_direction),
530 FlightMode::Braking(BrakingMode::Normal),
531 )
532 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
533 airship_context.leg_ctx_time_begin = ctx.time.0;
534 airship_context.first_leg = false;
535 #[cfg(debug_assertions)]
536 check_phase_completion_time(
537 ctx,
538 airship_context,
539 2,
540 AirshipFlightPhase::Transition,
541 );
542 }))
543 .map(|_, _| ())
544 .boxed()
545 },
546 AirshipFlightPhase::Descent => {
547 airship_context.route_timer = Duration::from_secs_f32(1.0);
549 let desired_offset = ctx.rng.random_range(4.0..6.0);
560 let descent_offset = if fly_distance > desired_offset {
561 desired_offset
562 } else {
563 0.0
564 };
565 let descent_dist = fly_distance - descent_offset;
566 let step1_dist = descent_dist * ctx.rng.random_range(0.7..0.85);
568 let step1_dur = fly_duration * ctx.rng.random_range(0.4..0.55);
569 let speed_mult = if matches!(ctx.npc.mode, SimulationMode::Loaded) {
571 ctx.rng.random_range(1.1..1.25)
572 } else {
573 1.0
574 };
575 let speed1 = (step1_dist / step1_dur) / nominal_speed * speed_mult;
576 let speed2 = ((descent_dist - step1_dist) / (fly_duration - step1_dur))
577 / nominal_speed
578 * speed_mult;
579 debug_airships!(
580 4,
581 "Airship {} route {} leg {} Descent, fly_distance {:.1}, fly_duration {:.1}s, \
582 desired_offset {:.1}, descent_offset {:.1}, descent_dist {:.1}, step1_dist \
583 {:.1}, step1_dur {:.3}s, speedmult {:.1}, speed1 {:.3}, speed2 {:.3}",
584 format!("{:?}", ctx.npc_id),
585 airship_context.route_index,
586 airship_context.current_leg,
587 fly_distance,
588 fly_duration,
589 desired_offset,
590 descent_offset,
591 descent_dist,
592 step1_dist,
593 step1_dur,
594 speed_mult,
595 speed1,
596 speed2
597 );
598
599 fly_airship_inner(
601 AirshipFlightPhase::Descent,
602 ctx.npc.wpos,
603 ctx.npc.wpos - Vec3::unit_z() * step1_dist,
604 20.0,
605 leg_ctx_time_begin,
606 step1_dur,
607 context_end_time - step1_dur as f64,
608 speed1,
609 0.0,
610 false,
611 Some(approach.airship_direction),
612 FlightMode::Braking(BrakingMode::Normal),
613 )
614 .then (fly_airship_inner(
615 AirshipFlightPhase::Descent,
616 ctx.npc.wpos - Vec3::unit_z() * step1_dist,
617 approach.airship_pos + Vec3::unit_z() * descent_offset,
618 20.0,
619 leg_ctx_time_begin + step1_dur as f64,
620 fly_duration - step1_dur,
621 context_end_time,
622 speed2,
623 0.0,
624 false,
625 Some(approach.airship_direction),
626 FlightMode::Braking(BrakingMode::Precise),
627 ))
628 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
630 airship_context.leg_ctx_time_begin = ctx.time.0;
631 airship_context.first_leg = false;
632 #[cfg(debug_assertions)]
633 check_phase_completion_time(ctx, airship_context, 3, AirshipFlightPhase::Descent);
634 log_airship_position(ctx, airship_context.route_index, &AirshipFlightPhase::Docked);
635 ctx.controller
636 .say(None, Content::localized("npc-speech-pilot-landed"));
637 }))
638 .map(|_, _| ()).boxed()
639 },
640 AirshipFlightPhase::Docked => {
641 debug_airships!(
642 4,
643 "Airship {} route {} leg {} Docked ctx time {:.1}, \
644 airship_context.leg_ctx_time_begin {:.1}, docking duration {:.1}s",
645 format!("{:?}", ctx.npc_id),
646 airship_context.route_index,
647 airship_context.current_leg,
648 leg_ctx_time_begin,
649 airship_context.leg_ctx_time_begin,
650 fly_duration,
651 );
652 let announcement_times = {
669 let mut times = Vec::new();
670 if fly_duration > 10.0 {
671 let first_time = ctx.rng.random_range(5.0..8.0);
672 let last_time = fly_duration - ctx.rng.random_range(10.0..12.0);
673 if first_time + 10.0 > last_time {
674 let mid_time = fly_duration / 2.0;
676 if mid_time > 5.0 && (fly_duration - mid_time) > 10.0 {
677 times.push(mid_time);
678 }
679 } else {
680 times.push(first_time);
682 let mut last_time = first_time;
683 let mut t = first_time + ctx.rng.random_range(10.0..16.0);
684 while t < fly_duration - 10.0 {
685 times.push(t);
686 last_time = t;
687 t += ctx.rng.random_range(10.0..16.0);
688 }
689 if last_time < fly_duration - 22.0 {
690 times.push(last_time + 12.0);
692 }
693 times.sort_by(|a, b| {
694 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
695 });
696 }
697 }
698 times
699 };
700 airship_context.announcements = announcement_times.into_iter().collect();
701 now(move |ctx, airship_context: &mut AirshipRouteContext| {
702 let dock_ctx_end_time = if let Some(at) = airship_context.announcements.front()
705 {
706 leg_ctx_time_begin + *at as f64
707 } else {
708 context_end_time
709 };
710 fly_airship_inner(
711 AirshipFlightPhase::Docked,
712 ctx.npc.wpos,
713 approach.airship_pos,
714 5.0,
715 leg_ctx_time_begin,
716 fly_duration,
717 dock_ctx_end_time,
718 0.75,
719 0.0,
720 false,
721 Some(approach.airship_direction),
722 FlightMode::Braking(BrakingMode::Precise),
723 )
724 .then(just(
725 |ctx, airship_context: &mut AirshipRouteContext| {
726 if airship_context.announcements.pop_front().is_some() {
730 let (dst_site_name, dst_site_dir) = if let Some(next_leg_approach) =
732 airship_context.next_leg_approach
733 {
734 (
735 ctx.index
736 .sites
737 .get(next_leg_approach.site_id)
738 .name()
739 .unwrap_or("Unknown Site")
740 .to_string(),
741 Direction::from_dir(
742 next_leg_approach.approach_transition_pos
743 - ctx.npc.wpos.xy(),
744 )
745 .localize_npc(),
746 )
747 } else {
748 ("Unknown Site".to_string(), Direction::North.localize_npc())
749 };
750 ctx.controller.say(
751 None,
752 Content::localized("npc-speech-pilot-announce_next")
753 .with_arg("dir", dst_site_dir)
754 .with_arg("dst", dst_site_name),
755 );
756 log_airship_position(
757 ctx,
758 airship_context.route_index,
759 &AirshipFlightPhase::Docked,
760 );
761 }
762 },
763 ))
764 })
765 .repeat()
766 .stop_if(move |ctx: &mut NpcCtx| ctx.time.0 >= context_end_time)
767 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
768 airship_context.leg_ctx_time_begin = ctx.time.0;
769 airship_context.first_leg = false;
770 #[cfg(debug_assertions)]
771 check_phase_completion_time(
772 ctx,
773 airship_context,
774 4,
775 AirshipFlightPhase::Docked,
776 );
777 log_airship_position(
778 ctx,
779 airship_context.route_index,
780 &AirshipFlightPhase::Docked,
781 );
782 }))
783 .map(|_, _| ())
784 .boxed()
785 },
786 AirshipFlightPhase::Ascent => {
787 log_airship_position(
788 ctx,
789 airship_context.route_index,
790 &AirshipFlightPhase::Ascent,
791 );
792 airship_context.route_timer = Duration::from_secs_f32(0.5);
793 let speed = (fly_distance / fly_duration) / nominal_speed;
795 debug_airships!(
796 4,
797 "Airship {} route {} leg {} Ascent, fly_distance {:.1}, fly_duration {:.1}s, \
798 speed factor {:.3}",
799 format!("{:?}", ctx.npc_id),
800 airship_context.route_index,
801 airship_context.current_leg,
802 fly_distance,
803 fly_duration,
804 speed
805 );
806 let src_site_name = ctx
807 .index
808 .sites
809 .get(approach.site_id)
810 .name()
811 .unwrap_or("Unknown Site")
812 .to_string();
813 let dst_site_name =
814 if let Some(next_leg_approach) = airship_context.next_leg_approach {
815 ctx.index
816 .sites
817 .get(next_leg_approach.site_id)
818 .name()
819 .unwrap_or("Unknown Site")
820 .to_string()
821 } else {
822 "Unknown Site".to_string()
823 };
824 ctx.controller.say(
825 None,
826 Content::localized("npc-speech-pilot-takeoff")
827 .with_arg("src", src_site_name)
828 .with_arg("dst", dst_site_name),
829 );
830 fly_airship_inner(
831 AirshipFlightPhase::Ascent,
832 ctx.npc.wpos,
833 approach.airship_pos + Vec3::unit_z() * approach.height,
834 20.0,
835 leg_ctx_time_begin,
836 fly_duration,
837 context_end_time,
838 speed,
839 0.0,
840 false,
841 Some(approach.airship_direction),
842 FlightMode::Braking(BrakingMode::Normal),
843 )
844 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
845 airship_context.leg_ctx_time_begin = ctx.time.0;
846 airship_context.first_leg = false;
847 #[cfg(debug_assertions)]
848 check_phase_completion_time(
849 ctx,
850 airship_context,
851 5,
852 AirshipFlightPhase::Ascent,
853 );
854 }))
855 .map(|_, _| ())
856 .boxed()
857 },
858 }
859 })
860}
861
862fn fly_airship_inner(
864 phase: AirshipFlightPhase,
865 from: Vec3<f32>,
866 to: Vec3<f32>,
867 goal_dist: f32,
868 leg_ctx_time_begin: f64,
869 leg_duration: f32,
870 context_tgt_end_time: f64,
871 speed_factor: f32,
872 height_offset: f32,
873 with_terrain_following: bool,
874 direction_override: Option<Dir>,
875 flight_mode: FlightMode,
876) -> impl Action<AirshipRouteContext> {
877 just(move |ctx, airship_context: &mut AirshipRouteContext| {
878 let stuck_tracker_target_loc = to.xy();
882
883 let nominal_pos = match phase {
885 AirshipFlightPhase::DepartureCruise
886 | AirshipFlightPhase::ApproachCruise
887 | AirshipFlightPhase::Transition => {
888 let route_interpolation_ratio =
890 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
891 (from.xy() + (to.xy() - from.xy()) * route_interpolation_ratio).with_z(to.z)
892 },
893 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
894 let route_interpolation_ratio =
898 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
899 let nominal_z = from.z + (to.z - from.z) * route_interpolation_ratio;
900 to.xy().with_z(nominal_z)
901 },
902 _ => {
903 to
905 },
906 };
907
908 let timer = airship_context
910 .route_timer
911 .checked_sub(Duration::from_secs_f32(ctx.dt));
912 airship_context.route_timer =
914 timer.unwrap_or(Duration::from_secs_f32(AIRSHIP_PROGRESS_UPDATE_INTERVAL));
915 if timer.is_none() {
916 #[cfg(feature = "airship_log")]
919 {
920 let distance_to_nominal = match phase {
922 AirshipFlightPhase::DepartureCruise
923 | AirshipFlightPhase::ApproachCruise
924 | AirshipFlightPhase::Transition => {
925 ctx.npc
927 .wpos
928 .xy()
929 .as_::<f64>()
930 .distance(nominal_pos.xy().as_())
931 },
932 _ => ctx.npc.wpos.as_::<f64>().distance(nominal_pos.as_()),
933 };
934 log_airship_position_plus(
935 ctx,
936 airship_context.route_index,
937 &phase,
938 distance_to_nominal,
939 0.0,
940 );
941 }
942
943 if matches!(
946 phase,
947 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
948 ) {
949 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
951 && stuck_tracker.is_stuck(ctx, &ctx.npc.wpos, &stuck_tracker_target_loc)
952 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
953 {
954 airship_context.stuck_backout_pos = Some(backout_pos);
955 } else if airship_context.stuck_backout_pos.is_some() {
956 #[cfg(debug_assertions)]
957 debug_airships!(
958 2,
959 "{:?} unstuck at pos: {} {}",
960 ctx.npc_id,
961 ctx.npc.wpos.x as i32,
962 ctx.npc.wpos.y as i32,
963 );
964 airship_context.stuck_backout_pos = None;
965 };
966 airship_context.cruise_direction =
968 Dir::from_unnormalized((to.xy() - ctx.npc.wpos.xy()).with_z(0.0));
969 }
970 }
971 if let Some(backout_pos) = airship_context.stuck_backout_pos {
973 ctx.controller.do_goto_with_height_and_dir(
975 backout_pos,
976 1.5,
977 None,
978 None,
979 FlightMode::Braking(BrakingMode::Normal),
980 );
981 } else {
982 let height_offset_opt = if with_terrain_following {
984 Some(height_offset)
985 } else {
986 None
987 };
988 let dir_opt = if direction_override.is_some() {
996 direction_override
997 } else if matches!(
998 phase,
999 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
1000 ) {
1001 airship_context.cruise_direction
1002 } else {
1003 None
1004 };
1005 ctx.controller.do_goto_with_height_and_dir(
1006 nominal_pos,
1007 speed_factor,
1008 height_offset_opt,
1009 dir_opt,
1010 flight_mode,
1011 );
1012 }
1013 })
1014 .repeat()
1015 .boxed()
1016 .stop_if(move |ctx: &mut NpcCtx| {
1017 match phase {
1018 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
1019 ctx.time.0 >= context_tgt_end_time
1020 || ctx.npc.wpos.as_::<f64>().distance_squared(to.as_())
1021 < (goal_dist as f64).powi(2)
1022 },
1023 AirshipFlightPhase::Docked => {
1024 if ctx.time.0 >= context_tgt_end_time {
1026 debug_airships!(
1027 4,
1028 "Airship {} docking phase complete time now {:.1} >= context_tgt_end_time \
1029 {:.1}",
1030 format!("{:?}", ctx.npc_id),
1031 ctx.time.0,
1032 context_tgt_end_time,
1033 );
1034 }
1035 ctx.time.0 >= context_tgt_end_time
1036 },
1037 _ => {
1038 if flight_mode == FlightMode::FlyThrough {
1039 ctx.npc
1041 .wpos
1042 .xy()
1043 .as_::<f64>()
1044 .distance_squared(to.xy().as_())
1045 < (goal_dist as f64).powi(2)
1046 } else {
1047 ctx.npc.wpos.as_::<f64>().distance_squared(to.as_())
1049 < (goal_dist as f64).powi(2)
1050 }
1051 },
1052 }
1053 })
1054 .debug(move || {
1055 format!(
1056 "fly airship, phase:{:?}, tgt pos:({}, {}, {}), goal dist:{}, leg dur: {}, initial \
1057 speed:{}, height:{}, terrain following:{}, FlightMode:{:?}",
1058 phase,
1059 to.x,
1060 to.y,
1061 to.z,
1062 goal_dist,
1063 leg_duration,
1064 speed_factor,
1065 height_offset,
1066 with_terrain_following,
1067 flight_mode,
1068 )
1069 })
1070 .map(|_, _| ())
1071}
1072
1073pub fn pilot_airship<S: State>() -> impl Action<S> {
1078 now(move |ctx, airship_context: &mut AirshipRouteContext| {
1079 if let Some((route_index, start_leg_index)) =
1081 ctx.data.airship_sim.assigned_routes.get(&ctx.npc_id)
1082 {
1083 let is_initial_startup = airship_context.route_index == usize::MAX;
1086 if is_initial_startup {
1087 setup_airship_route_context(ctx, airship_context, route_index, start_leg_index);
1088 } else {
1089 airship_context.current_leg = ctx
1091 .world
1092 .civs()
1093 .airships
1094 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1095 if airship_context.current_leg == 0 {
1096 airship_context.route_time_zero +=
1099 ctx.world.civs().airships.routes[airship_context.route_index].total_time;
1100 debug_airships!(
1101 4,
1102 "Airship {} route {} completed full route, route time zero now {:.1}, \
1103 ctx.time.0 - route_time_zero = {:.3}",
1104 format!("{:?}", ctx.npc_id),
1105 airship_context.route_index,
1106 airship_context.route_time_zero,
1107 ctx.time.0 - airship_context.route_time_zero
1108 );
1109 } else {
1110 debug_airships!(
1111 4,
1112 "Airship {} route {} starting next leg {}, current route time {:.1}",
1113 format!("{:?}", ctx.npc_id),
1114 airship_context.route_index,
1115 airship_context.current_leg,
1116 ctx.time.0 - airship_context.route_time_zero
1117 );
1118 }
1119 }
1120
1121 airship_context.current_leg_approach =
1124 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1125 airship_context.route_index,
1126 airship_context.current_leg,
1127 &ctx.world.sim().map_size_lg(),
1128 ));
1129
1130 if airship_context.current_leg_approach.is_none() {
1131 tracing::error!(
1132 "Airship pilot {:?} approach not found for route {} leg {}, stopping \
1133 pilot_airship loop.",
1134 ctx.npc_id,
1135 airship_context.route_index,
1136 airship_context.current_leg
1137 );
1138 return finish().map(|_, _| ()).boxed();
1139 }
1140
1141 let next_leg_index = ctx
1144 .world
1145 .civs()
1146 .airships
1147 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1148 airship_context.next_leg_approach =
1149 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1150 airship_context.route_index,
1151 next_leg_index,
1152 &ctx.world.sim().map_size_lg(),
1153 ));
1154 if airship_context.next_leg_approach.is_none() {
1155 tracing::warn!(
1156 "Airship pilot {:?} approach not found for next route {} leg {}",
1157 ctx.npc_id,
1158 airship_context.route_index,
1159 next_leg_index
1160 );
1161 }
1162
1163 if is_initial_startup {
1165 let my_route = &ctx.world.civs().airships.routes[airship_context.route_index];
1169 if let Some(my_spawn_loc) = my_route.spawning_locations.iter().min_by(|a, b| {
1170 let dist_a = ctx.npc.wpos.xy().as_::<f64>().distance_squared(a.pos.as_());
1171 let dist_b = ctx.npc.wpos.xy().as_::<f64>().distance_squared(b.pos.as_());
1172 dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal)
1173 }) {
1174 airship_context.route_time_zero = ctx.time.0 - my_spawn_loc.spawn_route_time;
1179 airship_context.leg_ctx_time_begin = ctx.time.0;
1180 airship_context.first_leg = true;
1181 debug_airships!(
1182 4,
1183 "Airship {} route {} leg {}, initial start up on phase {:?}, setting \
1184 route_time_zero to {:.1} (ctx.time.0 {} - my_spawn_loc.spawn_route_time \
1185 {})",
1186 format!("{:?}", ctx.npc_id),
1187 airship_context.route_index,
1188 airship_context.current_leg,
1189 my_spawn_loc.flight_phase,
1190 airship_context.route_time_zero,
1191 ctx.time.0,
1192 my_spawn_loc.spawn_route_time,
1193 );
1194 initial_flight_sequence(my_spawn_loc.flight_phase)
1195 .map(|_, _| ())
1196 .boxed()
1197 } else {
1198 tracing::error!(
1200 "Airship pilot {:?} spawning location not found for route {} leg {}",
1201 ctx.npc_id,
1202 airship_context.route_index,
1203 next_leg_index
1204 );
1205 finish().map(|_, _| ()).boxed()
1206 }
1207 } else {
1208 nominal_flight_sequence().map(|_, _| ()).boxed()
1209 }
1210 } else {
1211 finish().map(|_, _| ()).boxed()
1215 }
1216 })
1217 .repeat()
1218 .with_state(AirshipRouteContext::default())
1219 .map(|_, _| ())
1220}
1221
1222fn setup_airship_route_context(
1223 _ctx: &mut NpcCtx,
1224 route_context: &mut AirshipRouteContext,
1225 route_index: &usize,
1226 leg_index: &usize,
1227) {
1228 route_context.route_index = *route_index;
1229 route_context.current_leg = *leg_index;
1230
1231 #[cfg(debug_assertions)]
1232 {
1233 let current_approach = _ctx.world.civs().airships.approach_for_route_and_leg(
1234 route_context.route_index,
1235 route_context.current_leg,
1236 &_ctx.world.sim().map_size_lg(),
1237 );
1238 debug_airships!(
1239 4,
1240 "Server startup, airship pilot {:?} starting on route {} leg {}, target dock: {} {}",
1241 _ctx.npc_id,
1242 route_context.route_index,
1243 route_context.current_leg,
1244 current_approach.airship_pos.x as i32,
1245 current_approach.airship_pos.y as i32,
1246 );
1247 }
1248}
1249
1250fn initial_flight_sequence(start_phase: AirshipFlightPhase) -> impl Action<AirshipRouteContext> {
1251 now(move |_, airship_context: &mut AirshipRouteContext| {
1252 let approach = airship_context.current_leg_approach.unwrap();
1253 let phases = match start_phase {
1254 AirshipFlightPhase::DepartureCruise => vec![
1255 (AirshipFlightPhase::DepartureCruise, approach),
1256 (AirshipFlightPhase::ApproachCruise, approach),
1257 (AirshipFlightPhase::Transition, approach),
1258 (AirshipFlightPhase::Descent, approach),
1259 (AirshipFlightPhase::Docked, approach),
1260 (AirshipFlightPhase::Ascent, approach),
1261 ],
1262 AirshipFlightPhase::ApproachCruise => vec![
1263 (AirshipFlightPhase::ApproachCruise, approach),
1264 (AirshipFlightPhase::Transition, approach),
1265 (AirshipFlightPhase::Descent, approach),
1266 (AirshipFlightPhase::Docked, approach),
1267 (AirshipFlightPhase::Ascent, approach),
1268 ],
1269 AirshipFlightPhase::Transition => vec![
1270 (AirshipFlightPhase::Transition, approach),
1271 (AirshipFlightPhase::Descent, approach),
1272 (AirshipFlightPhase::Docked, approach),
1273 (AirshipFlightPhase::Ascent, approach),
1274 ],
1275 AirshipFlightPhase::Descent => vec![
1276 (AirshipFlightPhase::Descent, approach),
1277 (AirshipFlightPhase::Docked, approach),
1278 (AirshipFlightPhase::Ascent, approach),
1279 ],
1280 AirshipFlightPhase::Docked => {
1281 vec![
1283 (AirshipFlightPhase::Docked, approach),
1284 (AirshipFlightPhase::Ascent, approach),
1285 ]
1286 },
1287 AirshipFlightPhase::Ascent => vec![(AirshipFlightPhase::Ascent, approach)],
1288 };
1289 seq(phases
1290 .into_iter()
1291 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1292 })
1293}
1294
1295fn nominal_flight_sequence() -> impl Action<AirshipRouteContext> {
1296 now(move |_, airship_context: &mut AirshipRouteContext| {
1297 let approach = airship_context.current_leg_approach.unwrap();
1298 let phases = vec![
1299 (AirshipFlightPhase::DepartureCruise, approach),
1300 (AirshipFlightPhase::ApproachCruise, approach),
1301 (AirshipFlightPhase::Transition, approach),
1302 (AirshipFlightPhase::Descent, approach),
1303 (AirshipFlightPhase::Docked, approach),
1304 (AirshipFlightPhase::Ascent, approach),
1305 ];
1306 seq(phases
1307 .into_iter()
1308 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1309 })
1310}
1311
1312#[cfg(feature = "airship_log")]
1313fn log_airship_position(ctx: &NpcCtx, route_index: usize, phase: &AirshipFlightPhase) {
1315 log_airship_position_plus(ctx, route_index, phase, 0.0, 0.0);
1316}
1317
1318#[cfg(feature = "airship_log")]
1319fn log_airship_position_plus(
1320 ctx: &NpcCtx,
1321 route_index: usize,
1322 phase: &AirshipFlightPhase,
1323 value1: f64,
1324 value2: f64,
1325) {
1326 if let Ok(mut logger) = airship_logger() {
1327 logger.log_position(
1328 ctx.npc_id,
1329 ctx.index.seed,
1330 route_index,
1331 phase,
1332 ctx.time.0,
1333 ctx.npc.wpos,
1334 matches!(ctx.npc.mode, SimulationMode::Loaded),
1335 value1,
1336 value2,
1337 );
1338 } else {
1339 tracing::warn!("Failed to log airship position for {:?}", ctx.npc_id);
1340 }
1341}
1342
1343#[cfg(not(feature = "airship_log"))]
1344fn log_airship_position(_: &NpcCtx, _: usize, _: &AirshipFlightPhase) {}