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 let nominal_speed = ctx.world.civs().airships.nominal_speed;
287 let leg_segment = &route_leg.segments[phase as usize];
288 let leg_ctx_time_begin = airship_context.leg_ctx_time_begin;
290
291 let fly_duration = if airship_context.first_leg {
304 debug_airships!(
316 4,
317 "Airship {} route {} leg {} first leg phase {} from {},{} dur {:.1}s",
318 format!("{:?}", ctx.npc_id),
319 airship_context.route_index,
320 airship_context.current_leg,
321 phase,
322 ctx.npc.wpos.x,
323 ctx.npc.wpos.y,
324 leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)
325 );
326 ((leg_segment.route_time - (ctx.time.0 - airship_context.route_time_zero)) as f32)
327 .max(1.0)
328 } else {
329 let leg_start_route_time =
333 airship_context.leg_ctx_time_begin - airship_context.route_time_zero;
334 let expected_leg_route_start_time =
337 leg_segment.route_time - leg_segment.duration as f64;
338 let route_time_err = (leg_start_route_time - expected_leg_route_start_time) as f32;
339 if route_time_err > 10.0 {
340 debug_airships!(
341 4,
342 "Airship {} route {} leg {} starting {} late by {:.1}s, rt {:.2}, expected rt \
343 {:.2}, leg dur {:.1}",
344 format!("{:?}", ctx.npc_id),
345 airship_context.route_index,
346 airship_context.current_leg,
347 phase,
348 route_time_err,
349 leg_start_route_time,
350 expected_leg_route_start_time,
351 leg_segment.duration - route_time_err
352 );
353 leg_segment.duration - route_time_err
354 } else if route_time_err < -10.0 {
355 debug_airships!(
356 4,
357 "Airship {} route {} leg {} starting {} early by {:.1}s, rt {:.2}, expected \
358 rt {:.2}, leg dur {:.1}",
359 format!("{:?}", ctx.npc_id),
360 airship_context.route_index,
361 airship_context.current_leg,
362 phase,
363 -route_time_err,
364 leg_start_route_time,
365 expected_leg_route_start_time,
366 leg_segment.duration - route_time_err
367 );
368 leg_segment.duration - route_time_err
369 } else {
370 debug_airships!(
371 4,
372 "Airship {} route {} leg {} starting {} on time, leg dur {:.1}",
373 format!("{:?}", ctx.npc_id),
374 airship_context.route_index,
375 airship_context.current_leg,
376 phase,
377 leg_segment.duration
378 );
379 leg_segment.duration
380 }
381 .max(1.0)
382 };
383
384 let fly_distance = match phase {
385 AirshipFlightPhase::DepartureCruise
386 | AirshipFlightPhase::ApproachCruise
387 | AirshipFlightPhase::Transition => {
388 ctx.npc.wpos.xy().distance(leg_segment.to_world_pos)
389 },
390 AirshipFlightPhase::Descent => ctx.npc.wpos.z - approach.airship_pos.z,
391 AirshipFlightPhase::Ascent => approach.airship_pos.z + approach.height - ctx.npc.wpos.z,
392 AirshipFlightPhase::Docked => 0.0,
393 };
394
395 let context_end_time = ctx.time.0 + fly_duration as f64;
396
397 match phase {
398 AirshipFlightPhase::DepartureCruise => {
399 airship_context.my_stuck_tracker = Some(StuckAirshipTracker::default());
400 airship_context.route_timer = Duration::from_secs_f32(1.0);
401 airship_context.cruise_direction =
402 Dir::from_unnormalized((approach.midpoint - ctx.npc.wpos.xy()).with_z(0.0));
403 let speed = (fly_distance / fly_duration) / nominal_speed;
406 debug_airships!(
407 4,
408 "Airship {} route {} leg {} DepartureCruise, fly_distance {:.1}, fly_duration \
409 {:.1}s, speed factor {:.3}",
410 format!("{:?}", ctx.npc_id),
411 airship_context.route_index,
412 airship_context.current_leg,
413 fly_distance,
414 fly_duration,
415 speed
416 );
417 fly_airship_inner(
419 AirshipFlightPhase::DepartureCruise,
420 ctx.npc.wpos,
421 leg_segment.to_world_pos.with_z(0.0),
422 50.0,
423 leg_ctx_time_begin,
424 fly_duration,
425 context_end_time,
426 speed,
427 approach.height,
428 true,
429 None,
430 FlightMode::FlyThrough,
431 )
432 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
433 airship_context.leg_ctx_time_begin = ctx.time.0;
434 airship_context.first_leg = false;
435 #[cfg(debug_assertions)]
436 check_phase_completion_time(
437 ctx,
438 airship_context,
439 0,
440 AirshipFlightPhase::DepartureCruise,
441 );
442 }))
443 .map(|_, _| ())
444 .boxed()
445 },
446 AirshipFlightPhase::ApproachCruise => {
447 airship_context.route_timer = Duration::from_secs_f32(1.0);
448 airship_context.cruise_direction = Dir::from_unnormalized(
449 (approach.approach_transition_pos - approach.midpoint).with_z(0.0),
450 );
451 let speed = (fly_distance / fly_duration) / nominal_speed;
454 debug_airships!(
455 4,
456 "Airship {} route {} leg {} ApproachCruise, fly_distance {:.1}, fly_duration \
457 {:.1}s, speed factor {:.3}",
458 format!("{:?}", ctx.npc_id),
459 airship_context.route_index,
460 airship_context.current_leg,
461 fly_distance,
462 fly_duration,
463 speed
464 );
465 fly_airship_inner(
467 AirshipFlightPhase::ApproachCruise,
468 ctx.npc.wpos,
469 leg_segment.to_world_pos.with_z(0.0),
470 50.0,
471 leg_ctx_time_begin,
472 fly_duration,
473 context_end_time,
474 speed,
475 approach.height,
476 true,
477 None,
478 FlightMode::FlyThrough,
479 )
480 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
481 airship_context.leg_ctx_time_begin = ctx.time.0;
482 airship_context.first_leg = false;
483 #[cfg(debug_assertions)]
484 check_phase_completion_time(
485 ctx,
486 airship_context,
487 1,
488 AirshipFlightPhase::ApproachCruise,
489 );
490 }))
491 .map(|_, _| ())
492 .boxed()
493 },
494 AirshipFlightPhase::Transition => {
495 airship_context.route_timer = Duration::from_secs_f32(1.0);
498 let speed = (fly_distance / fly_duration) / nominal_speed;
501 debug_airships!(
502 4,
503 "Airship {} route {} leg {} Transition, fly_distance {:.1}, fly_duration \
504 {:.1}s, speed factor {:.3}",
505 format!("{:?}", ctx.npc_id),
506 airship_context.route_index,
507 airship_context.current_leg,
508 fly_distance,
509 fly_duration,
510 speed
511 );
512 fly_airship_inner(
514 AirshipFlightPhase::Transition,
515 ctx.npc.wpos,
516 leg_segment
517 .to_world_pos
518 .with_z(approach.airship_pos.z + approach.height),
519 20.0,
520 leg_ctx_time_begin,
521 fly_duration,
522 context_end_time,
523 speed,
524 approach.height,
525 true,
526 Some(approach.airship_direction),
527 FlightMode::Braking(BrakingMode::Normal),
528 )
529 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
530 airship_context.leg_ctx_time_begin = ctx.time.0;
531 airship_context.first_leg = false;
532 #[cfg(debug_assertions)]
533 check_phase_completion_time(
534 ctx,
535 airship_context,
536 2,
537 AirshipFlightPhase::Transition,
538 );
539 }))
540 .map(|_, _| ())
541 .boxed()
542 },
543 AirshipFlightPhase::Descent => {
544 airship_context.route_timer = Duration::from_secs_f32(1.0);
546 let desired_offset = ctx.rng.random_range(4.0..6.0);
557 let descent_offset = if fly_distance > desired_offset {
558 desired_offset
559 } else {
560 0.0
561 };
562 let descent_dist = fly_distance - descent_offset;
563 let step1_dist = descent_dist * ctx.rng.random_range(0.7..0.85);
565 let step1_dur = fly_duration * ctx.rng.random_range(0.4..0.55);
566 let speed_mult = if matches!(ctx.npc.mode, SimulationMode::Loaded) {
568 ctx.rng.random_range(1.1..1.25)
569 } else {
570 1.0
571 };
572 let speed1 = (step1_dist / step1_dur) / nominal_speed * speed_mult;
573 let speed2 = ((descent_dist - step1_dist) / (fly_duration - step1_dur))
574 / nominal_speed
575 * speed_mult;
576 debug_airships!(
577 4,
578 "Airship {} route {} leg {} Descent, fly_distance {:.1}, fly_duration {:.1}s, \
579 desired_offset {:.1}, descent_offset {:.1}, descent_dist {:.1}, step1_dist \
580 {:.1}, step1_dur {:.3}s, speedmult {:.1}, speed1 {:.3}, speed2 {:.3}",
581 format!("{:?}", ctx.npc_id),
582 airship_context.route_index,
583 airship_context.current_leg,
584 fly_distance,
585 fly_duration,
586 desired_offset,
587 descent_offset,
588 descent_dist,
589 step1_dist,
590 step1_dur,
591 speed_mult,
592 speed1,
593 speed2
594 );
595
596 fly_airship_inner(
598 AirshipFlightPhase::Descent,
599 ctx.npc.wpos,
600 ctx.npc.wpos - Vec3::unit_z() * step1_dist,
601 20.0,
602 leg_ctx_time_begin,
603 step1_dur,
604 context_end_time - step1_dur as f64,
605 speed1,
606 0.0,
607 false,
608 Some(approach.airship_direction),
609 FlightMode::Braking(BrakingMode::Normal),
610 )
611 .then (fly_airship_inner(
612 AirshipFlightPhase::Descent,
613 ctx.npc.wpos - Vec3::unit_z() * step1_dist,
614 approach.airship_pos + Vec3::unit_z() * descent_offset,
615 20.0,
616 leg_ctx_time_begin + step1_dur as f64,
617 fly_duration - step1_dur,
618 context_end_time,
619 speed2,
620 0.0,
621 false,
622 Some(approach.airship_direction),
623 FlightMode::Braking(BrakingMode::Precise),
624 ))
625 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
627 airship_context.leg_ctx_time_begin = ctx.time.0;
628 airship_context.first_leg = false;
629 #[cfg(debug_assertions)]
630 check_phase_completion_time(ctx, airship_context, 3, AirshipFlightPhase::Descent);
631 log_airship_position(ctx, airship_context.route_index, &AirshipFlightPhase::Docked);
632 ctx.controller
633 .say(None, Content::localized("npc-speech-pilot-landed"));
634 }))
635 .map(|_, _| ()).boxed()
636 },
637 AirshipFlightPhase::Docked => {
638 debug_airships!(
639 4,
640 "Airship {} route {} leg {} Docked ctx time {:.1}, \
641 airship_context.leg_ctx_time_begin {:.1}, docking duration {:.1}s",
642 format!("{:?}", ctx.npc_id),
643 airship_context.route_index,
644 airship_context.current_leg,
645 leg_ctx_time_begin,
646 airship_context.leg_ctx_time_begin,
647 fly_duration,
648 );
649 let announcement_times = {
666 let mut times = Vec::new();
667 if fly_duration > 10.0 {
668 let first_time = ctx.rng.random_range(5.0..8.0);
669 let last_time = fly_duration - ctx.rng.random_range(10.0..12.0);
670 if first_time + 10.0 > last_time {
671 let mid_time = fly_duration / 2.0;
673 if mid_time > 5.0 && (fly_duration - mid_time) > 10.0 {
674 times.push(mid_time);
675 }
676 } else {
677 times.push(first_time);
679 let mut last_time = first_time;
680 let mut t = first_time + ctx.rng.random_range(10.0..16.0);
681 while t < fly_duration - 10.0 {
682 times.push(t);
683 last_time = t;
684 t += ctx.rng.random_range(10.0..16.0);
685 }
686 if last_time < fly_duration - 22.0 {
687 times.push(last_time + 12.0);
689 }
690 times.sort_by(|a, b| {
691 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
692 });
693 }
694 }
695 times
696 };
697 airship_context.announcements = announcement_times.into_iter().collect();
698 now(move |ctx, airship_context: &mut AirshipRouteContext| {
699 let dock_ctx_end_time = if let Some(at) = airship_context.announcements.front()
702 {
703 leg_ctx_time_begin + *at as f64
704 } else {
705 context_end_time
706 };
707 fly_airship_inner(
708 AirshipFlightPhase::Docked,
709 ctx.npc.wpos,
710 approach.airship_pos,
711 5.0,
712 leg_ctx_time_begin,
713 fly_duration,
714 dock_ctx_end_time,
715 0.75,
716 0.0,
717 false,
718 Some(approach.airship_direction),
719 FlightMode::Braking(BrakingMode::Precise),
720 )
721 .then(just(
722 |ctx, airship_context: &mut AirshipRouteContext| {
723 if airship_context.announcements.pop_front().is_some() {
727 let (dst_site_name, dst_site_dir) = if let Some(next_leg_approach) =
729 airship_context.next_leg_approach
730 {
731 (
732 ctx.index
733 .sites
734 .get(next_leg_approach.site_id)
735 .name()
736 .unwrap_or("Unknown Site")
737 .to_string(),
738 Direction::from_dir(
739 next_leg_approach.approach_transition_pos
740 - ctx.npc.wpos.xy(),
741 )
742 .localize_npc(),
743 )
744 } else {
745 ("Unknown Site".to_string(), Direction::North.localize_npc())
746 };
747 ctx.controller.say(
748 None,
749 Content::localized("npc-speech-pilot-announce_next")
750 .with_arg("dir", dst_site_dir)
751 .with_arg("dst", dst_site_name),
752 );
753 log_airship_position(
754 ctx,
755 airship_context.route_index,
756 &AirshipFlightPhase::Docked,
757 );
758 }
759 },
760 ))
761 })
762 .repeat()
763 .stop_if(move |ctx: &mut NpcCtx| ctx.time.0 >= context_end_time)
764 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
765 airship_context.leg_ctx_time_begin = ctx.time.0;
766 airship_context.first_leg = false;
767 #[cfg(debug_assertions)]
768 check_phase_completion_time(
769 ctx,
770 airship_context,
771 4,
772 AirshipFlightPhase::Docked,
773 );
774 log_airship_position(
775 ctx,
776 airship_context.route_index,
777 &AirshipFlightPhase::Docked,
778 );
779 }))
780 .map(|_, _| ())
781 .boxed()
782 },
783 AirshipFlightPhase::Ascent => {
784 log_airship_position(
785 ctx,
786 airship_context.route_index,
787 &AirshipFlightPhase::Ascent,
788 );
789 airship_context.route_timer = Duration::from_secs_f32(0.5);
790 let speed = (fly_distance / fly_duration) / nominal_speed;
792 debug_airships!(
793 4,
794 "Airship {} route {} leg {} Ascent, fly_distance {:.1}, fly_duration {:.1}s, \
795 speed factor {:.3}",
796 format!("{:?}", ctx.npc_id),
797 airship_context.route_index,
798 airship_context.current_leg,
799 fly_distance,
800 fly_duration,
801 speed
802 );
803 let src_site_name = ctx
804 .index
805 .sites
806 .get(approach.site_id)
807 .name()
808 .unwrap_or("Unknown Site")
809 .to_string();
810 let dst_site_name =
811 if let Some(next_leg_approach) = airship_context.next_leg_approach {
812 ctx.index
813 .sites
814 .get(next_leg_approach.site_id)
815 .name()
816 .unwrap_or("Unknown Site")
817 .to_string()
818 } else {
819 "Unknown Site".to_string()
820 };
821 ctx.controller.say(
822 None,
823 Content::localized("npc-speech-pilot-takeoff")
824 .with_arg("src", src_site_name)
825 .with_arg("dst", dst_site_name),
826 );
827 fly_airship_inner(
828 AirshipFlightPhase::Ascent,
829 ctx.npc.wpos,
830 approach.airship_pos + Vec3::unit_z() * approach.height,
831 20.0,
832 leg_ctx_time_begin,
833 fly_duration,
834 context_end_time,
835 speed,
836 0.0,
837 false,
838 Some(approach.airship_direction),
839 FlightMode::Braking(BrakingMode::Normal),
840 )
841 .then(just(|ctx, airship_context: &mut AirshipRouteContext| {
842 airship_context.leg_ctx_time_begin = ctx.time.0;
843 airship_context.first_leg = false;
844 #[cfg(debug_assertions)]
845 check_phase_completion_time(
846 ctx,
847 airship_context,
848 5,
849 AirshipFlightPhase::Ascent,
850 );
851 }))
852 .map(|_, _| ())
853 .boxed()
854 },
855 }
856 })
857}
858
859fn fly_airship_inner(
861 phase: AirshipFlightPhase,
862 from: Vec3<f32>,
863 to: Vec3<f32>,
864 goal_dist: f32,
865 leg_ctx_time_begin: f64,
866 leg_duration: f32,
867 context_tgt_end_time: f64,
868 speed_factor: f32,
869 height_offset: f32,
870 with_terrain_following: bool,
871 direction_override: Option<Dir>,
872 flight_mode: FlightMode,
873) -> impl Action<AirshipRouteContext> {
874 just(move |ctx, airship_context: &mut AirshipRouteContext| {
875 let stuck_tracker_target_loc = to.xy();
879
880 let nominal_pos = match phase {
882 AirshipFlightPhase::DepartureCruise
883 | AirshipFlightPhase::ApproachCruise
884 | AirshipFlightPhase::Transition => {
885 let route_interpolation_ratio =
887 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
888 (from.xy() + (to.xy() - from.xy()) * route_interpolation_ratio).with_z(to.z)
889 },
890 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
891 let route_interpolation_ratio =
895 ((ctx.time.0 - leg_ctx_time_begin) as f32 / leg_duration).clamp(0.0, 1.0);
896 let nominal_z = from.z + (to.z - from.z) * route_interpolation_ratio;
897 to.xy().with_z(nominal_z)
898 },
899 _ => {
900 to
902 },
903 };
904
905 let timer = airship_context
907 .route_timer
908 .checked_sub(Duration::from_secs_f32(ctx.dt));
909 airship_context.route_timer =
911 timer.unwrap_or(Duration::from_secs_f32(AIRSHIP_PROGRESS_UPDATE_INTERVAL));
912 if timer.is_none() {
913 #[cfg(feature = "airship_log")]
916 {
917 let distance_to_nominal = match phase {
919 AirshipFlightPhase::DepartureCruise
920 | AirshipFlightPhase::ApproachCruise
921 | AirshipFlightPhase::Transition => {
922 ctx.npc
924 .wpos
925 .xy()
926 .as_::<f64>()
927 .distance(nominal_pos.xy().as_())
928 },
929 _ => ctx.npc.wpos.as_::<f64>().distance(nominal_pos.as_()),
930 };
931 log_airship_position_plus(
932 ctx,
933 airship_context.route_index,
934 &phase,
935 distance_to_nominal,
936 0.0,
937 );
938 }
939
940 if matches!(
943 phase,
944 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
945 ) {
946 if let Some(stuck_tracker) = &mut airship_context.my_stuck_tracker
948 && stuck_tracker.is_stuck(ctx, &ctx.npc.wpos, &stuck_tracker_target_loc)
949 && let Some(backout_pos) = stuck_tracker.current_backout_pos(ctx)
950 {
951 airship_context.stuck_backout_pos = Some(backout_pos);
952 } else if airship_context.stuck_backout_pos.is_some() {
953 #[cfg(debug_assertions)]
954 debug_airships!(
955 2,
956 "{:?} unstuck at pos: {} {}",
957 ctx.npc_id,
958 ctx.npc.wpos.x as i32,
959 ctx.npc.wpos.y as i32,
960 );
961 airship_context.stuck_backout_pos = None;
962 };
963 airship_context.cruise_direction =
965 Dir::from_unnormalized((to.xy() - ctx.npc.wpos.xy()).with_z(0.0));
966 }
967 }
968 if let Some(backout_pos) = airship_context.stuck_backout_pos {
970 ctx.controller.do_goto_with_height_and_dir(
972 backout_pos,
973 1.5,
974 None,
975 None,
976 FlightMode::Braking(BrakingMode::Normal),
977 );
978 } else {
979 let height_offset_opt = if with_terrain_following {
981 Some(height_offset)
982 } else {
983 None
984 };
985 let dir_opt = if direction_override.is_some() {
993 direction_override
994 } else if matches!(
995 phase,
996 AirshipFlightPhase::DepartureCruise | AirshipFlightPhase::ApproachCruise
997 ) {
998 airship_context.cruise_direction
999 } else {
1000 None
1001 };
1002 ctx.controller.do_goto_with_height_and_dir(
1003 nominal_pos,
1004 speed_factor,
1005 height_offset_opt,
1006 dir_opt,
1007 flight_mode,
1008 );
1009 }
1010 })
1011 .repeat()
1012 .boxed()
1013 .stop_if(move |ctx: &mut NpcCtx| {
1014 match phase {
1015 AirshipFlightPhase::Descent | AirshipFlightPhase::Ascent => {
1016 ctx.time.0 >= context_tgt_end_time
1017 || ctx.npc.wpos.as_::<f64>().distance_squared(to.as_())
1018 < (goal_dist as f64).powi(2)
1019 },
1020 AirshipFlightPhase::Docked => {
1021 if ctx.time.0 >= context_tgt_end_time {
1023 debug_airships!(
1024 4,
1025 "Airship {} docking phase complete time now {:.1} >= context_tgt_end_time \
1026 {:.1}",
1027 format!("{:?}", ctx.npc_id),
1028 ctx.time.0,
1029 context_tgt_end_time,
1030 );
1031 }
1032 ctx.time.0 >= context_tgt_end_time
1033 },
1034 _ => {
1035 if flight_mode == FlightMode::FlyThrough {
1036 ctx.npc
1038 .wpos
1039 .xy()
1040 .as_::<f64>()
1041 .distance_squared(to.xy().as_())
1042 < (goal_dist as f64).powi(2)
1043 } else {
1044 ctx.npc.wpos.as_::<f64>().distance_squared(to.as_())
1046 < (goal_dist as f64).powi(2)
1047 }
1048 },
1049 }
1050 })
1051 .debug(move || {
1052 format!(
1053 "fly airship, phase:{:?}, tgt pos:({}, {}, {}), goal dist:{}, leg dur: {}, initial \
1054 speed:{}, height:{}, terrain following:{}, FlightMode:{:?}",
1055 phase,
1056 to.x,
1057 to.y,
1058 to.z,
1059 goal_dist,
1060 leg_duration,
1061 speed_factor,
1062 height_offset,
1063 with_terrain_following,
1064 flight_mode,
1065 )
1066 })
1067 .map(|_, _| ())
1068}
1069
1070pub fn pilot_airship<S: State>() -> impl Action<S> {
1075 now(move |ctx, airship_context: &mut AirshipRouteContext| {
1076 if let Some((route_index, start_leg_index)) =
1078 ctx.data.airship_sim.assigned_routes.get(&ctx.npc_id)
1079 {
1080 let is_initial_startup = airship_context.route_index == usize::MAX;
1083 if is_initial_startup {
1084 setup_airship_route_context(ctx, airship_context, route_index, start_leg_index);
1085 } else {
1086 airship_context.current_leg = ctx
1088 .world
1089 .civs()
1090 .airships
1091 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1092 if airship_context.current_leg == 0 {
1093 airship_context.route_time_zero +=
1096 ctx.world.civs().airships.routes[airship_context.route_index].total_time;
1097 debug_airships!(
1098 4,
1099 "Airship {} route {} completed full route, route time zero now {:.1}, \
1100 ctx.time.0 - route_time_zero = {:.3}",
1101 format!("{:?}", ctx.npc_id),
1102 airship_context.route_index,
1103 airship_context.route_time_zero,
1104 ctx.time.0 - airship_context.route_time_zero
1105 );
1106 } else {
1107 debug_airships!(
1108 4,
1109 "Airship {} route {} starting next leg {}, current route time {:.1}",
1110 format!("{:?}", ctx.npc_id),
1111 airship_context.route_index,
1112 airship_context.current_leg,
1113 ctx.time.0 - airship_context.route_time_zero
1114 );
1115 }
1116 }
1117
1118 airship_context.current_leg_approach =
1121 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1122 airship_context.route_index,
1123 airship_context.current_leg,
1124 &ctx.world.sim().map_size_lg(),
1125 ));
1126
1127 if airship_context.current_leg_approach.is_none() {
1128 tracing::error!(
1129 "Airship pilot {:?} approach not found for route {} leg {}, stopping \
1130 pilot_airship loop.",
1131 ctx.npc_id,
1132 airship_context.route_index,
1133 airship_context.current_leg
1134 );
1135 return finish().map(|_, _| ()).boxed();
1136 }
1137
1138 let next_leg_index = ctx
1141 .world
1142 .civs()
1143 .airships
1144 .increment_route_leg(airship_context.route_index, airship_context.current_leg);
1145 airship_context.next_leg_approach =
1146 Some(ctx.world.civs().airships.approach_for_route_and_leg(
1147 airship_context.route_index,
1148 next_leg_index,
1149 &ctx.world.sim().map_size_lg(),
1150 ));
1151 if airship_context.next_leg_approach.is_none() {
1152 tracing::warn!(
1153 "Airship pilot {:?} approach not found for next route {} leg {}",
1154 ctx.npc_id,
1155 airship_context.route_index,
1156 next_leg_index
1157 );
1158 }
1159
1160 if is_initial_startup {
1162 let my_route = &ctx.world.civs().airships.routes[airship_context.route_index];
1166 if let Some(my_spawn_loc) = my_route.spawning_locations.iter().min_by(|a, b| {
1167 let dist_a = ctx.npc.wpos.xy().as_::<f64>().distance_squared(a.pos.as_());
1168 let dist_b = ctx.npc.wpos.xy().as_::<f64>().distance_squared(b.pos.as_());
1169 dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal)
1170 }) {
1171 airship_context.route_time_zero = ctx.time.0 - my_spawn_loc.spawn_route_time;
1176 airship_context.leg_ctx_time_begin = ctx.time.0;
1177 airship_context.first_leg = true;
1178 debug_airships!(
1179 4,
1180 "Airship {} route {} leg {}, initial start up on phase {:?}, setting \
1181 route_time_zero to {:.1} (ctx.time.0 {} - my_spawn_loc.spawn_route_time \
1182 {})",
1183 format!("{:?}", ctx.npc_id),
1184 airship_context.route_index,
1185 airship_context.current_leg,
1186 my_spawn_loc.flight_phase,
1187 airship_context.route_time_zero,
1188 ctx.time.0,
1189 my_spawn_loc.spawn_route_time,
1190 );
1191 initial_flight_sequence(my_spawn_loc.flight_phase)
1192 .map(|_, _| ())
1193 .boxed()
1194 } else {
1195 tracing::error!(
1197 "Airship pilot {:?} spawning location not found for route {} leg {}",
1198 ctx.npc_id,
1199 airship_context.route_index,
1200 next_leg_index
1201 );
1202 finish().map(|_, _| ()).boxed()
1203 }
1204 } else {
1205 nominal_flight_sequence().map(|_, _| ()).boxed()
1206 }
1207 } else {
1208 finish().map(|_, _| ()).boxed()
1212 }
1213 })
1214 .repeat()
1215 .with_state(AirshipRouteContext::default())
1216 .map(|_, _| ())
1217}
1218
1219fn setup_airship_route_context(
1220 _ctx: &mut NpcCtx,
1221 route_context: &mut AirshipRouteContext,
1222 route_index: &usize,
1223 leg_index: &usize,
1224) {
1225 route_context.route_index = *route_index;
1226 route_context.current_leg = *leg_index;
1227
1228 #[cfg(debug_assertions)]
1229 {
1230 let current_approach = _ctx.world.civs().airships.approach_for_route_and_leg(
1231 route_context.route_index,
1232 route_context.current_leg,
1233 &_ctx.world.sim().map_size_lg(),
1234 );
1235 debug_airships!(
1236 4,
1237 "Server startup, airship pilot {:?} starting on route {} leg {}, target dock: {} {}",
1238 _ctx.npc_id,
1239 route_context.route_index,
1240 route_context.current_leg,
1241 current_approach.airship_pos.x as i32,
1242 current_approach.airship_pos.y as i32,
1243 );
1244 }
1245}
1246
1247fn initial_flight_sequence(start_phase: AirshipFlightPhase) -> impl Action<AirshipRouteContext> {
1248 now(move |_, airship_context: &mut AirshipRouteContext| {
1249 let approach = airship_context.current_leg_approach.unwrap();
1250 let phases = match start_phase {
1251 AirshipFlightPhase::DepartureCruise => vec![
1252 (AirshipFlightPhase::DepartureCruise, approach),
1253 (AirshipFlightPhase::ApproachCruise, approach),
1254 (AirshipFlightPhase::Transition, approach),
1255 (AirshipFlightPhase::Descent, approach),
1256 (AirshipFlightPhase::Docked, approach),
1257 (AirshipFlightPhase::Ascent, approach),
1258 ],
1259 AirshipFlightPhase::ApproachCruise => vec![
1260 (AirshipFlightPhase::ApproachCruise, approach),
1261 (AirshipFlightPhase::Transition, approach),
1262 (AirshipFlightPhase::Descent, approach),
1263 (AirshipFlightPhase::Docked, approach),
1264 (AirshipFlightPhase::Ascent, approach),
1265 ],
1266 AirshipFlightPhase::Transition => vec![
1267 (AirshipFlightPhase::Transition, approach),
1268 (AirshipFlightPhase::Descent, approach),
1269 (AirshipFlightPhase::Docked, approach),
1270 (AirshipFlightPhase::Ascent, approach),
1271 ],
1272 AirshipFlightPhase::Descent => vec![
1273 (AirshipFlightPhase::Descent, approach),
1274 (AirshipFlightPhase::Docked, approach),
1275 (AirshipFlightPhase::Ascent, approach),
1276 ],
1277 AirshipFlightPhase::Docked => {
1278 vec![
1280 (AirshipFlightPhase::Docked, approach),
1281 (AirshipFlightPhase::Ascent, approach),
1282 ]
1283 },
1284 AirshipFlightPhase::Ascent => vec![(AirshipFlightPhase::Ascent, approach)],
1285 };
1286 seq(phases
1287 .into_iter()
1288 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1289 })
1290}
1291
1292fn nominal_flight_sequence() -> impl Action<AirshipRouteContext> {
1293 now(move |_, airship_context: &mut AirshipRouteContext| {
1294 let approach = airship_context.current_leg_approach.unwrap();
1295 let phases = vec![
1296 (AirshipFlightPhase::DepartureCruise, approach),
1297 (AirshipFlightPhase::ApproachCruise, approach),
1298 (AirshipFlightPhase::Transition, approach),
1299 (AirshipFlightPhase::Descent, approach),
1300 (AirshipFlightPhase::Docked, approach),
1301 (AirshipFlightPhase::Ascent, approach),
1302 ];
1303 seq(phases
1304 .into_iter()
1305 .map(|(phase, current_approach)| fly_airship(phase, current_approach)))
1306 })
1307}
1308
1309#[cfg(feature = "airship_log")]
1310fn log_airship_position(ctx: &NpcCtx, route_index: usize, phase: &AirshipFlightPhase) {
1312 log_airship_position_plus(ctx, route_index, phase, 0.0, 0.0);
1313}
1314
1315#[cfg(feature = "airship_log")]
1316fn log_airship_position_plus(
1317 ctx: &NpcCtx,
1318 route_index: usize,
1319 phase: &AirshipFlightPhase,
1320 value1: f64,
1321 value2: f64,
1322) {
1323 if let Ok(mut logger) = airship_logger() {
1324 logger.log_position(
1325 ctx.npc_id,
1326 ctx.index.seed,
1327 route_index,
1328 phase,
1329 ctx.time.0,
1330 ctx.npc.wpos,
1331 matches!(ctx.npc.mode, SimulationMode::Loaded),
1332 value1,
1333 value2,
1334 );
1335 } else {
1336 tracing::warn!("Failed to log airship position for {:?}", ctx.npc_id);
1337 }
1338}
1339
1340#[cfg(not(feature = "airship_log"))]
1341fn log_airship_position(_: &NpcCtx, _: usize, _: &AirshipFlightPhase) {}