diff --git a/airdef-mod/src/commands.rs b/airdef-mod/src/commands.rs index d536c6e..05eaa81 100644 --- a/airdef-mod/src/commands.rs +++ b/airdef-mod/src/commands.rs @@ -60,6 +60,7 @@ pub fn tree() -> Tree { Node::lit("raid").runs(), Node::lit("sites").runs(), Node::lit("ground-stock").then(Node::arg("склад", Value::int_from(1)).then(Node::arg("количество", Value::int_from(0)).runs())), + Node::lit("ground-cancel").then(Node::arg("комплекс", Value::int_from(1)).runs()), Node::lit("ground-unload").then(Node::arg("комплекс", Value::int_from(1)).runs()), Node::lit("ground-load").then(Node::arg("комплекс", Value::int_from(1)).runs()), Node::lit("strike").then(Node::arg("номер", Value::int_from(1)).runs()), @@ -239,7 +240,7 @@ mod tests { fn completion_offers_the_next_word_only() { let top = complete("", &[]); for w in ["menu", "monitor", "grid", "replay", "scenario", "why", "raid", "repair", "import", "ew", "flare", "move", - "target", "give", "deploy", "wave", "clear", "targets", "damage", "truth", "shader", "hold", "lastchance", "roles", "doctrine", "stats", "sites", "strike", "ground-stock", "ground-load", "ground-unload"] { + "target", "give", "deploy", "wave", "clear", "targets", "damage", "truth", "shader", "hold", "lastchance", "roles", "doctrine", "stats", "sites", "strike", "ground-stock", "ground-load", "ground-unload", "ground-cancel"] { assert!(top.contains(&w.to_string()), "{w} in {top:?}"); } assert_eq!(top.len(), 33); diff --git a/airdef-mod/src/layout.rs b/airdef-mod/src/layout.rs index 26176b8..8ceac51 100644 --- a/airdef-mod/src/layout.rs +++ b/airdef-mod/src/layout.rs @@ -25,6 +25,8 @@ pub struct SamRec { pub comps: Vec, pub ground_rounds: u32, pub ground_request: bool, + pub ground_return: bool, + pub return_cargo: bool, pub ground_stock: u32, pub air_48n6_stock: u32, pub mode: sim::battery::SamMode, @@ -143,6 +145,9 @@ impl Layout { let ground_request = l.num(28, "заявка RM48U", 0.0, 1.0)?.unwrap_or(0.0) == 1.0; if ground_request && kind != SamKind::S400 { return Err(l.bad("заявка RM48U", "нужен С-400")); } let radar_on = l.flag(29, "излучение РЛС")?.unwrap_or(true); + let ground_return = l.flag(30, "заявка на вывоз RM48U")?.unwrap_or(false); + let return_cargo = l.flag(31, "возвращаемый груз RM48U")?.unwrap_or(false); + if (ground_return && (kind != SamKind::S400 || ground_request || ground_rounds == 0)) || (return_cargo && (kind != SamKind::Tzm || ground_stock == 0)) { return Err(l.bad("возврат RM48U", "несовместимое состояние")); } if radar_role != sim::battery::RadarRole::Multifunction && kind != SamKind::Radar { return Err(l.bad("роль РЛС", "роль поддерживается только отдельной РЛС")); } @@ -155,7 +160,7 @@ impl Layout { if let Some((m, _, _, _)) = ammo && m + ground_rounds > 4 && kind == SamKind::S400 { return Err(l.bad("боезапас", "больше четырёх ячеек")); } - out.sams.push(SamRec { kind, pos, heading, roles, health, cargo, ammo, dest, comps, ground_rounds, ground_stock, air_48n6_stock, mode, radar_role, spent_cells, ground_request, radar_on }); + out.sams.push(SamRec { kind, pos, heading, roles, health, cargo, ammo, dest, comps, ground_rounds, ground_stock, air_48n6_stock, mode, radar_role, spent_cells, ground_request, radar_on, ground_return, return_cargo }); } "target" => { if out.targets.len() >= MAX_UNITS { diff --git a/airdef-mod/src/lib.rs b/airdef-mod/src/lib.rs index fcfef74..2d28f89 100644 --- a/airdef-mod/src/lib.rs +++ b/airdef-mod/src/lib.rs @@ -898,7 +898,7 @@ impl AirDef { let comps = if fresh { new.comps } else { s.comps }; let parts = comps.0.iter().map(|v| format!("{v:.3}")).collect::>().join(" "); text += &format!( - "sam {} {} {} {} {} {} {} {} {} {} {} {} {dest} {parts} {} {} {} {} {} {} {} {}\n", + "sam {} {} {} {} {} {} {} {} {} {} {} {} {dest} {parts} {} {} {} {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, @@ -911,7 +911,7 @@ impl AirDef { bursts, u8::from(empty), reloading, - s.ground_rounds, s.ground_stock, s.air_48n6_stock, s.mode.key(), s.radar_role.key(), if fresh { 0 } else { s.spent_cells }, u8::from(!fresh && s.ground_request), s.radar_on, + s.ground_rounds, s.ground_stock, s.air_48n6_stock, s.mode.key(), s.radar_role.key(), if fresh { 0 } else { s.spent_cells }, u8::from(!fresh && s.ground_request), s.radar_on, !fresh && s.ground_return, !fresh && s.return_cargo, ); } for (i, t) in self.sim.targets.iter().enumerate() { @@ -1021,6 +1021,8 @@ impl AirDef { } sam.ground_rounds = r.ground_rounds; sam.ground_request = r.ground_request; + sam.ground_return = r.ground_return; + sam.return_cargo = r.return_cargo; sam.ground_stock = r.ground_stock; sam.air_48n6_stock = r.air_48n6_stock; sam.fit_cells(); @@ -1120,7 +1122,7 @@ fn durable_mark(sim: &Sim) -> u64 { let at = s.dest.unwrap_or(s.pos); let place = (at.x * 10.0) as i64 as u64 ^ ((at.z * 10.0) as i64 as u64).rotate_left(21) ^ (if s.dest.is_some() { 0 } else { (s.heading * 100.0) as i64 as u64 }).rotate_left(42); let ammo = u64::from(s.missiles) | u64::from(s.bursts) << 16 | u64::from(s.cargo) << 32 | u64::from(s.empty) << 48 | u64::from(s.reloading > 0) << 49; - let typed = u64::from(s.ground_rounds) ^ u64::from(s.ground_stock).rotate_left(17) ^ u64::from(s.air_48n6_stock).rotate_left(39) ^ u64::from(s.ground_request).rotate_left(55); + let typed = u64::from(s.ground_rounds) ^ u64::from(s.ground_stock).rotate_left(17) ^ u64::from(s.air_48n6_stock).rotate_left(39) ^ u64::from(s.ground_request).rotate_left(55) ^ u64::from(s.ground_return).rotate_left(56) ^ u64::from(s.return_cargo).rotate_left(57); let parts = s.comps.0.iter().fold(0u64, |a, v| a.wrapping_mul(131).wrapping_add((v * 100.0) as u64)); ((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7) ^ parts.rotate_left(13) ^ typed.rotate_left(29) ^ (s.mode as u64).rotate_left(3) ^ (s.radar_role as u64).rotate_left(11) ^ u64::from(s.spent_cells).rotate_left(19) ^ u64::from(s.radar_on).rotate_left(23) }) @@ -2386,6 +2388,12 @@ impl Mod for AirDef { self.dirty = true; Some(message) } + ["ground-cancel", n] => { + let i = n.parse::().ok().and_then(|v| v.checked_sub(1)).ok_or("/airdef ground-cancel <номер комплекса>")?; + let message = self.sim.cancel_ground_request(i)?; + self.dirty = true; + Some(message) + } ["strike", n] => { // A manual order: the simulation validates availability; player position is no launcher. let p = player().ok_or("Нет игрока")?; @@ -2589,6 +2597,56 @@ mod tests { } } + #[test] + fn return_round_drives_and_survives_both_state_round_trips() { + struct Flat; + impl sim::world::WorldSource for Flat { + fn solid(&self, _: i32, y: i32, _: i32) -> Option { Some(y < 64) } + } + let mut game = test_mod(); + for (kind, x) in [(SamKind::Depot, 0.0), (SamKind::S400, 160.0), (SamKind::Tzm, 0.0)] { + let id = game.sim.id(); + game.sim.sams.push(Sam::new(id, kind, DVec3::new(x, 64.0, 0.0), 0.0)); + } + game.sim.sams[1].ground_rounds = 1; + game.sim.sams[1].missiles = 3; + game.sim.unload_ground_round(1).unwrap(); + assert!(game.sim.sams[1].ground_return); + assert!(game.sim.load_ground_round(1).is_err()); + let saved = game.state_text(false, false); + let mut restored = test_mod(); + restored.apply_state(&saved).unwrap(); + game = restored; + assert!(game.sim.sams[1].ground_return); + let mut out = Out::default(); + let mut picked = false; + let mut far: f64 = 0.0; + for _ in 0..12000 { + let now = game.sim.tick; + let cols = game.ground.take_wanted().into_iter().map(|(x, z)| (x, z, 64)).collect(); + game.ground.answer(cols, now); + let sections = game.ground.world.take_wanted(now); + game.ground.world.serve(&Flat, sections, now); + game.sim.step(&mut game.ground, None, &mut out); + far = far.max(game.sim.sams[2].pos.x); + if !picked && game.sim.sams[2].return_cargo { + assert_eq!((game.sim.sams[1].ground_rounds, game.sim.sams[0].ground_stock), (0, 0)); + let saved = game.state_text(false, false); + let mut restored = test_mod(); + restored.apply_state(&saved).unwrap(); + game = restored; + assert!(game.sim.sams[2].return_cargo); + assert_eq!(game.sim.sams[2].ground_stock, 1); + picked = true; + } + if picked && game.sim.sams[0].ground_stock == 1 { break; } + out = Out::default(); + } + assert!(picked && far > 140.0); + assert_eq!((game.sim.sams[0].ground_stock, game.sim.sams[1].ground_rounds, game.sim.sams[2].ground_stock), (1, 0, 0)); + assert!(!game.sim.sams[2].return_cargo); + } + #[test] fn delivery_request_and_cargo_survive_full_state_round_trip() { struct Flat; diff --git a/airdef-mod/src/map.rs b/airdef-mod/src/map.rs index a0c78bf..5edcb9f 100644 --- a/airdef-mod/src/map.rs +++ b/airdef-mod/src/map.rs @@ -2341,7 +2341,7 @@ impl AirDef { lines.push((sam.status(), WHITE)); lines.push((format!("Исправен {:.0}% · {:.0} {:.0}", sam.health * 100.0, sam.pos.x, sam.pos.z), GREY)); if k.armed() { - if sam.kind == SamKind::S400 { lines.push((format!("Ударных RM48U: {} (общие ячейки с ПВО)", sam.ground_rounds), WHITE)); } + if sam.kind == SamKind::S400 { lines.push((format!("Ударных RM48U: {} (общие ячейки с ПВО){}", sam.ground_rounds, if sam.ground_return { " · ожидает вывоза" } else if sam.ground_request { " · ожидает доставки" } else { "" }), WHITE)); } lines.push((format!("Боезапас: {} ракет, {} очередей{}", sam.missiles, sam.bursts, if sam.reloading > 0 { format!(" · перезарядка {} с", sam.reloading / 20) } else { String::new() }), WHITE)); lines.push((format!("Дальность огня: {}", distance(k.range())), WHITE)); } diff --git a/airdef-mod/src/sim/damage.rs b/airdef-mod/src/sim/damage.rs index 88d6777..f180501 100644 --- a/airdef-mod/src/sim/damage.rs +++ b/airdef-mod/src/sim/damage.rs @@ -350,6 +350,8 @@ impl Sim { self.sams[i].air_48n6_stock = 0; self.sams[i].ground_rounds = 0; self.sams[i].ground_request = false; + self.sams[i].ground_return = false; + self.sams[i].return_cargo = false; let missiles = missiles + stock * 2 + typed; let charge = spec.missile.as_ref().map_or(0.0, |m| m.kind.charge() * missiles as f64) + bursts as f64 / 60.0 + stock as f64 * 1.5; diff --git a/airdef-mod/src/sim/logistics.rs b/airdef-mod/src/sim/logistics.rs index afa20f8..d82614c 100644 --- a/airdef-mod/src/sim/logistics.rs +++ b/airdef-mod/src/sim/logistics.rs @@ -10,6 +10,7 @@ const DEPOT_REACH: f64 = 60.0; const HANDOVER: f64 = 10.0; /// What a truck carries. const TRUCK_LOAD: u32 = 2; +const STOCK_LIMIT: u32 = 1_000_000; impl Sim { /// Whether ammunition is counted: a depot was set up (without one, magazines refill as @@ -26,6 +27,7 @@ impl Sim { if !s.alive() || s.moving() || s.preparing.is_some() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 { return Err("Комплекс должен быть исправен и развёрнут".into()); } + if s.ground_return { return Err("Сначала отмените заявку на вывоз RM48U".into()); } if s.ground_rounds >= s.cells() { return Err("Нет свободной ячейки для RM48U".into()); } let depot = self.sams.iter().enumerate().filter(|(_, d)| d.kind == SamKind::Depot && d.alive() && d.ground_stock > 0 && d.pos.distance(s.pos) < DEPOT_REACH) .min_by(|(_, a), (_, b)| a.pos.distance(s.pos).total_cmp(&b.pos.distance(s.pos)).then(a.id.cmp(&b.id))) @@ -56,10 +58,18 @@ impl Sim { if s.kind != SamKind::S400 || !s.alive() || s.moving() || s.preparing.is_some() || s.reloading > 0 || s.part(Part::Launcher) <= 0.0 || s.ground_rounds == 0 { return Err("Нужен исправный развёрнутый С-400 с загруженной RM48U".into()); } + if s.ground_request { return Err("Сначала отмените заявку на доставку RM48U".into()); } + if s.ground_return { return Err("Заявка на вывоз RM48U уже ожидает ТЗМ".into()); } let depot = self.sams.iter().enumerate().filter(|(_, d)| d.kind == SamKind::Depot && d.alive() && d.pos.distance(s.pos) < DEPOT_REACH) .min_by(|(_, a), (_, b)| a.pos.distance(s.pos).total_cmp(&b.pos.distance(s.pos)).then(a.id.cmp(&b.id))) - .map(|(j, _)| j).ok_or("Нет живого склада рядом (60 блоков)")?; - let stock = self.sams[depot].ground_stock.checked_add(1).ok_or("Запас склада переполнен")?; + .map(|(j, _)| j); + let Some(depot) = depot else { + if !self.sams.iter().any(|d| d.kind == SamKind::Depot && d.alive() && d.ground_stock < STOCK_LIMIT) { return Err("Нет доступного склада для возврата RM48U".into()); } + if !self.sams.iter().any(|t| t.kind == SamKind::Tzm && t.alive() && t.drive_speed() > 0.0 && t.ground_stock.saturating_add(t.air_48n6_stock) < TRUCK_LOAD) { return Err("Нет исправной ТЗМ со свободным местом для RM48U".into()); } + self.sams[i].ground_return = true; + return Ok(format!("С-400 №{}: RM48U зарезервирована, ожидается вывоз ТЗМ", i + 1)); + }; + let stock = self.sams[depot].ground_stock.checked_add(1).filter(|n| *n <= STOCK_LIMIT).ok_or("Запас склада переполнен")?; self.sams[i].ground_rounds -= 1; self.sams[depot].ground_stock = stock; if self.sams[depot].air_48n6_stock > 0 { @@ -69,6 +79,14 @@ impl Sim { Ok(format!("С-400 №{}: выгружена RM48U; ударных {}, ПВО {}", i + 1, self.sams[i].ground_rounds, self.sams[i].missiles)) } + pub fn cancel_ground_request(&mut self, i: usize) -> Result { + let s = self.sams.get_mut(i).ok_or("Нет такого комплекса")?; + if s.kind != SamKind::S400 || (!s.ground_request && !s.ground_return) { return Err("Нет ожидающей заявки RM48U".into()); } + s.ground_request = false; + s.ground_return = false; + Ok(format!("С-400 №{}: заявка RM48U отменена", i + 1)) + } + /// Reloads from depots near the systems, and the trucks' rounds. pub(super) fn step_supply(&mut self, out: &mut Out) { if !self.logistics() { @@ -105,6 +123,42 @@ impl Sim { } let at = truck.pos; let truck_id = truck.id; + // A return is picked up physically; its cargo remains marked until a depot accepts it. + let can_pick = truck.ground_stock == 0 && truck.air_48n6_stock < TRUCK_LOAD; + let return_work = truck.return_cargo || (can_pick && self.sams.iter().any(|s| s.kind == SamKind::S400 && s.alive() && s.ground_return)); + if return_work { + let taken: Vec = self.sams.iter().filter(|s| s.kind == SamKind::Tzm && s.alive() && s.id != truck_id).filter_map(|s| s.job).collect(); + let target = if truck.return_cargo { + self.sams.iter().filter(|s| s.kind == SamKind::Depot && s.alive() && s.ground_stock < STOCK_LIMIT && !self.routes.out_of_reach(truck_id, s.id, now)).min_by(|a,b| a.pos.distance(at).total_cmp(&b.pos.distance(at))) + } else { + self.sams.iter().filter(|s| s.kind == SamKind::S400 && s.alive() && s.ground_return && s.ground_rounds > 0 && !s.moving() && s.preparing.is_none() && s.reloading == 0 && s.part(Part::Launcher) > 0.0 && !taken.contains(&s.id) && !self.routes.out_of_reach(truck_id, s.id, now)).min_by(|a,b| a.pos.distance(at).total_cmp(&b.pos.distance(at))) + }.map(|s| (s.id, s.pos)); + self.sams[t].job = target.map(|x| x.0); + if let Some((id, to)) = target { + if at.distance(to) > HANDOVER { + let side = horizontal(at - to).normalize_or(DVec3::X) * (HANDOVER * 0.6); + self.order_move_near(t, to + side, 3.5); + } else if let Some(j) = self.sams.iter().position(|s| s.id == id) { + if self.sams[t].return_cargo { + self.sams[j].ground_stock += 1; + self.sams[t].ground_stock -= 1; + self.sams[t].return_cargo = false; + } else { + self.sams[j].ground_rounds -= 1; + self.sams[j].ground_return = false; + self.sams[t].ground_stock += 1; + self.sams[t].return_cargo = true; + if self.sams[t].air_48n6_stock > 0 && self.sams[j].missiles + self.sams[j].ground_rounds < self.sams[j].cells() { + self.sams[t].air_48n6_stock -= 1; + self.sams[j].missiles += 1; + } + } + self.sams[t].job = None; + out.sound("minecraft:block.piston.extend", at, 1.5, 0.8); + } + } + continue; + } // Typed strike cargo takes precedence over generic reloads. Jobs are reconstructed // after loading a save; the request and physical cargo survive independently. let typed_work = truck.ground_stock > 0 || truck.air_48n6_stock > 0 || @@ -123,6 +177,11 @@ impl Sim { self.sams[j].air_48n6_stock = n; self.sams[t].air_48n6_stock = 0; } + if self.sams[t].ground_stock > 0 && !self.sams.iter().any(|s| s.kind == SamKind::S400 && s.alive() && s.ground_request) && self.sams[j].ground_stock < STOCK_LIMIT { + let take = self.sams[t].ground_stock.min(STOCK_LIMIT - self.sams[j].ground_stock); + self.sams[j].ground_stock += take; + self.sams[t].ground_stock -= take; + } if self.sams[t].ground_stock == 0 && self.sams[j].ground_stock > 0 && self.sams[t].air_48n6_stock == 0 && self.sams.iter().any(|s| s.kind == SamKind::S400 && s.alive() && s.ground_request) { self.sams[j].ground_stock -= 1; self.sams[t].ground_stock += 1; @@ -147,10 +206,10 @@ impl Sim { } else { self.sams[t].job = None; } } let taken: Vec = self.sams.iter().filter(|s| s.kind == SamKind::Tzm && s.alive() && s.id != truck_id).filter_map(|s| s.job).collect(); - let next = if self.sams[t].ground_stock > 0 { + let next = if self.sams[t].ground_stock > 0 && self.sams.iter().any(|s| s.kind == SamKind::S400 && s.alive() && s.ground_request) { self.sams.iter().filter(|s| s.kind == SamKind::S400 && s.alive() && s.ground_request && !s.moving() && s.preparing.is_none() && s.reloading == 0 && s.part(Part::Launcher) > 0.0 && s.ground_rounds < s.cells() && !taken.contains(&s.id) && !self.routes.out_of_reach(truck_id, s.id, now)).min_by(|a,b| a.pos.distance(at).total_cmp(&b.pos.distance(at))) } else { - self.sams.iter().filter(|s| s.kind == SamKind::Depot && s.alive() && (self.sams[t].air_48n6_stock > 0 || s.ground_stock > 0) && !self.routes.out_of_reach(truck_id, s.id, now)).min_by(|a,b| a.pos.distance(at).total_cmp(&b.pos.distance(at))) + self.sams.iter().filter(|s| s.kind == SamKind::Depot && s.alive() && (self.sams[t].air_48n6_stock > 0 || (self.sams[t].ground_stock > 0 && s.ground_stock < STOCK_LIMIT) || (self.sams[t].ground_stock == 0 && s.ground_stock > 0)) && !self.routes.out_of_reach(truck_id, s.id, now)).min_by(|a,b| a.pos.distance(at).total_cmp(&b.pos.distance(at))) }.map(|s| (s.id, s.pos)); if let Some((id, to)) = next { self.sams[t].job = Some(id); @@ -229,6 +288,36 @@ impl Sim { #[cfg(test)] mod tests { use super::*; + #[test] + fn return_waits_for_capacity_and_ready_carrier_and_can_be_cancelled() { + let mut sim = Sim::new(3); + for (kind, x) in [(SamKind::Depot, 0.0), (SamKind::S400, 160.0), (SamKind::Tzm, 160.0)] { + let id = sim.id(); sim.sams.push(Sam::new(id, kind, DVec3::new(x, 70.0, 0.0), 0.0)); + } + sim.sams[1].ground_rounds = 1; + sim.sams[1].missiles = 3; + sim.sams[2].air_48n6_stock = 2; + assert!(sim.unload_ground_round(1).is_err()); + sim.sams[2].air_48n6_stock = 1; + sim.unload_ground_round(1).unwrap(); + sim.cancel_ground_request(1).unwrap(); + assert!(!sim.sams[1].ground_return); + sim.unload_ground_round(1).unwrap(); + sim.tick = 2; + sim.sams[1].reloading = 1; + sim.step_supply(&mut Out::default()); + assert_eq!(sim.sams[1].ground_rounds, 1); + sim.sams[1].reloading = 0; + sim.step_supply(&mut Out::default()); + assert_eq!((sim.sams[1].ground_rounds, sim.sams[1].missiles), (0, 4)); + assert_eq!((sim.sams[2].ground_stock, sim.sams[2].air_48n6_stock), (1, 0)); + assert!(sim.sams[2].return_cargo); + sim.direct_hit(sim.sams[2].pos + DVec3::Y, DVec3::ZERO, 1, &mut Out::default()); + sim.repair(); + assert_eq!((sim.sams[0].ground_stock, sim.sams[1].ground_rounds, sim.sams[2].ground_stock), (0, 0, 0)); + assert!(!sim.sams[2].return_cargo); + } + #[test] fn typed_delivery_uses_stock_and_shared_cells_and_survives_interruption() { let mut sim = Sim::new(1); diff --git a/airdef-mod/src/sim/mod.rs b/airdef-mod/src/sim/mod.rs index 8492d1c..7cb7b3b 100644 --- a/airdef-mod/src/sim/mod.rs +++ b/airdef-mod/src/sim/mod.rs @@ -1397,6 +1397,8 @@ pub struct Sam { pub ground_rounds: u32, /// Pending player request for one delivered ground round. pub ground_request: bool, + pub ground_return: bool, + pub return_cargo: bool, /// Separate typed rounds and returned S-400 air rounds held by a depot. pub ground_stock: u32, pub air_48n6_stock: u32, @@ -1487,6 +1489,8 @@ impl Sam { missiles: spec.missile.as_ref().map_or(0, |m| m.magazine), ground_rounds: 0, ground_request: false, + ground_return: false, + return_cargo: false, ground_stock: 0, air_48n6_stock: 0, bursts: spec.gun.as_ref().map_or(0, |g| g.bursts), @@ -1570,7 +1574,7 @@ impl Sam { } match self.kind { SamKind::Depot => return format!("{text}: {} перезарядок, RM48U {}, снятых ПВО {}", self.cargo, self.ground_stock, self.air_48n6_stock), - SamKind::Tzm => return format!("{text}: на борту {}/2, RM48U {}, снятых ПВО {}{}", self.cargo, self.ground_stock, self.air_48n6_stock, if self.job.is_some() { ", в пути" } else { "" }), + SamKind::Tzm => return format!("{text}: на борту {}/2, RM48U {}, снятых ПВО {}{}", self.cargo, self.ground_stock, self.air_48n6_stock, if self.return_cargo { ", везёт RM48U на склад" } else if self.job.is_some() { ", в пути" } else { "" }), SamKind::Ew => { let state = if self.ew_on { "ГЛУШИТ" } else if self.ew_lock { "остывает" } else { "готова" }; return format!("{text}: {state}, нагрев {:.0}%", self.heat * 100.0); @@ -1600,6 +1604,7 @@ impl Sam { if let Some(m) = &spec.missile { if self.kind == SamKind::S400 { text += &format!(" ПВО {} · RM48U {} / {} ячеек", self.missiles, self.ground_rounds, self.cells()); + if self.ground_return { text += " · ожидает вывоза RM48U"; } } else { text += &format!(" ракеты {}/{}", self.missiles, m.magazine); } diff --git a/airdef-mod/src/sim/strike.rs b/airdef-mod/src/sim/strike.rs index 55adbc9..32f4e63 100644 --- a/airdef-mod/src/sim/strike.rs +++ b/airdef-mod/src/sim/strike.rs @@ -704,7 +704,7 @@ impl Sim { let dist = (est.pos - DVec2::new(s.pos.x, s.pos.z)).length(); (matches!(catalog::unit(s.kind).ground_strike(), catalog::GroundStrike::Rm48uGame) && s.alive() && !s.moving() && s.reloading == 0 && s.cooldown == 0 - && s.part(Part::Launcher) > 0.0 && s.ground_rounds > 0 && s.preparing.is_none() && dist <= TUNE.range) + && s.part(Part::Launcher) > 0.0 && s.ground_rounds > u32::from(s.ground_return) && s.preparing.is_none() && dist <= TUNE.range) .then_some((i, dist)) }).min_by(|(i,a), (j,b)| self.sams[*j].ground_rounds.cmp(&self.sams[*i].ground_rounds) .then(a.total_cmp(b)).then(self.sams[*i].id.cmp(&self.sams[*j].id))) @@ -728,7 +728,7 @@ impl Sim { let sam = &self.sams[i]; let valid = valid_site && sam.preparing.is_some_and(|q| q.ground && q.target == p.site) && sam.alive() && !sam.moving() && sam.reloading == 0 && sam.cooldown == 0 - && sam.part(Part::Launcher) > 0.0 && sam.ground_rounds > 0; + && sam.part(Part::Launcher) > 0.0 && sam.ground_rounds > u32::from(sam.ground_return); if !valid { out.messages.push(format!("Подготовка RM48U по ПУ №{} отменена: изменилось состояние носителя или цели", p.site)); if self.sams[i].preparing.is_some_and(|q| q.ground && q.target == p.site) { self.sams[i].cancel_prepare(); } @@ -1334,6 +1334,24 @@ mod tests { } } + #[test] + fn return_request_reserves_one_round_from_strikes() { + let mut sim = Sim::new(11); + let a = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 0); + let mut sam = Sam::new(123, SamKind::S400, DVec3::ZERO, 0.0); + sam.missiles = 3; sam.ground_rounds = 1; sam.ground_return = true; sam.cooldown = 0; + sim.sams.push(sam); + let mut out = Out::default(); + assert!(sim.order_strike(a, DVec3::ZERO, &mut out).is_err()); + sim.sams[0].ground_rounds = 2; + sim.sams[0].missiles = 2; + sim.order_strike(a, DVec3::ZERO, &mut out).unwrap(); + sim.tick += launch::OPEN_TICKS; + sim.prepare_ground_strikes(&mut out); + assert_eq!(sim.sams[0].ground_rounds, 1); + assert!(sim.sams[0].ground_return); + } + #[test] fn ground_orders_reserve_the_carrier_and_target_until_ejection() { let mut sim = Sim::new(11);