diff --git a/airdef-mod/src/battery_view.rs b/airdef-mod/src/battery_view.rs new file mode 100644 index 0000000..580df9e --- /dev/null +++ b/airdef-mod/src/battery_view.rs @@ -0,0 +1,42 @@ +//! Player-facing labels for battery configuration snapshots. +use crate::sim::battery::{BatteryStatus, RadarRole, SamMode}; + +pub fn radar_role_name(role: RadarRole) -> &'static str { + match role { + RadarRole::Multifunction => "многофункциональная", + RadarRole::Search => "обзорная", + RadarRole::Engagement => "сопровождения", + RadarRole::Height => "мачтовая", + } +} + +pub fn status(status: BatteryStatus) -> String { + match status { + BatteryStatus::Radar { index, role, emitting, operational } => format!( + "№{} РЛС: {}, излучение {}, {}", index + 1, radar_role_name(role), + if emitting { "вкл" } else { "выкл" }, + if operational { "работает" } else { "нет связи или РЛС не работает" } + ), + BatteryStatus::Launcher { index, kind, mode, linked } => format!( + "№{} {}: {}{}", index + 1, kind.name(), mode.key(), + if mode == SamMode::LauncherOnly { + if linked { ", РЛС связана" } else { ", нет связанной РЛС сопровождения" } + } else { "" } + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sim::SamKind; + + #[test] + fn renders_battery_status_in_existing_format() { + assert_eq!(status(BatteryStatus::Radar { index: 0, role: RadarRole::Search, emitting: false, operational: false }), "№1 РЛС: обзорная, излучение выкл, нет связи или РЛС не работает"); + assert_eq!(status(BatteryStatus::Radar { index: 1, role: RadarRole::Engagement, emitting: true, operational: true }), "№2 РЛС: сопровождения, излучение вкл, работает"); + assert_eq!(status(BatteryStatus::Launcher { index: 2, kind: SamKind::S400, mode: SamMode::LauncherOnly, linked: true }), "№3 С-400: launcher, РЛС связана"); + assert_eq!(status(BatteryStatus::Launcher { index: 3, kind: SamKind::S500, mode: SamMode::LauncherOnly, linked: false }), "№4 С-500: launcher, нет связанной РЛС сопровождения"); + assert_eq!(status(BatteryStatus::Launcher { index: 4, kind: SamKind::S400, mode: SamMode::Integrated, linked: false }), "№5 С-400: integrated"); + } +} diff --git a/airdef-mod/src/lib.rs b/airdef-mod/src/lib.rs index 2255f57..50ad017 100644 --- a/airdef-mod/src/lib.rs +++ b/airdef-mod/src/lib.rs @@ -12,6 +12,7 @@ use horizon_mod_sdk::{ ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget}, }; +mod battery_view; mod cinema; mod capabilities; mod engines; @@ -2301,7 +2302,7 @@ impl Mod for AirDef { ["deploy", kind] => Some(self.deploy_configured(kind)?), ["battery"] => { let status = self.sim.battery_status(); - for line in &status { say(line); } + for entry in status { say(&battery_view::status(entry)); } Some("/airdef battery <номер> integrated|launcher; /airdef radar <номер> on|off|multi|search|engagement|height".into()) } ["battery", n, mode] => { diff --git a/airdef-mod/src/map.rs b/airdef-mod/src/map.rs index 899ee09..2f205e8 100644 --- a/airdef-mod/src/map.rs +++ b/airdef-mod/src/map.rs @@ -2330,7 +2330,7 @@ impl AirDef { let k = sam.kind; lines.push((format!("{} №{}", k.name(), n + 1), CYAN)); if k == SamKind::Radar { - lines.push((format!("Роль РЛС: {}", sam.radar_role.name()), WHITE)); + lines.push((format!("Роль РЛС: {}", crate::battery_view::radar_role_name(sam.radar_role)), WHITE)); } else if sam.mode == sim::battery::SamMode::LauncherOnly { lines.push(("Отдельная пусковая: внешняя РЛС сопровождения".into(), WHITE)); } diff --git a/airdef-mod/src/sim/battery.rs b/airdef-mod/src/sim/battery.rs index 9c0300b..e5f7060 100644 --- a/airdef-mod/src/sim/battery.rs +++ b/airdef-mod/src/sim/battery.rs @@ -33,9 +33,6 @@ impl RadarRole { pub fn from_key(key: &str) -> Option { match key { "multi" => Some(Self::Multifunction), "search" => Some(Self::Search), "engagement" => Some(Self::Engagement), "height" => Some(Self::Height), _ => None } } - pub fn name(self) -> &'static str { - match self { Self::Multifunction => "многофункциональная", Self::Search => "обзорная", Self::Engagement => "сопровождения", Self::Height => "мачтовая" } - } pub fn authorizes(self) -> bool { matches!(self, Self::Multifunction | Self::Engagement) } pub(super) fn reach_factor(self) -> f64 { match self { Self::Multifunction => 1.0, Self::Search => 1.15, Self::Engagement => 0.8, Self::Height => 1.05 } } } @@ -68,15 +65,22 @@ impl Sam { } } +/// A snapshot of the relevant configuration, independent of its presentation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BatteryStatus { + Radar { index: usize, role: RadarRole, emitting: bool, operational: bool }, + Launcher { index: usize, kind: SamKind, mode: SamMode, linked: bool }, +} + impl Sim { /// Current configurations and whether a launcher has an operational radar link. - pub fn battery_status(&self) -> Vec { - self.sams.iter().enumerate().filter_map(|(i, sam)| { + pub fn battery_status(&self) -> Vec { + self.sams.iter().enumerate().filter_map(|(index, sam)| { if sam.kind == SamKind::Radar { - Some(format!("№{} РЛС: {}, излучение {}, {}", i + 1, sam.radar_role.name(), if sam.radar_on { "вкл" } else { "выкл" }, if sam.radar_up() && sam.link_range() > 0.0 { "работает" } else { "нет связи или РЛС не работает" })) + Some(BatteryStatus::Radar { index, role: sam.radar_role, emitting: sam.radar_on, operational: sam.radar_up() && sam.link_range() > 0.0 }) } else if matches!(sam.kind, SamKind::S400 | SamKind::S500) { let linked = self.sams.iter().any(|r| r.kind == SamKind::Radar && r.radar_role.authorizes() && r.radar_up() && r.link_range() > 0.0 && sam.link_range() > 0.0 && r.pos.distance(sam.pos) <= r.link_range().min(sam.link_range())); - Some(format!("№{} {}: {}{}", i + 1, sam.kind.name(), sam.mode.key(), if sam.mode == SamMode::LauncherOnly { if linked { ", РЛС связана" } else { ", нет связанной РЛС сопровождения" } } else { "" })) + Some(BatteryStatus::Launcher { index, kind: sam.kind, mode: sam.mode, linked }) } else { None } }).collect() } @@ -87,6 +91,27 @@ mod tests { use super::*; use super::super::{sight, why}; + #[test] + fn status_reports_real_radar_links_and_preserves_indices() { + let mut sim = Sim::new(1); + sim.sams.push(Sam::new(1, SamKind::Pantsir, DVec3::ZERO, 0.0)); + let mut radar = Sam::new(2, SamKind::Radar, DVec3::ZERO, 0.0); + radar.radar_role = RadarRole::Engagement; + sim.sams.push(radar); + let mut launcher = Sam::new(3, SamKind::S400, DVec3::new(10.0, 0.0, 0.0), 0.0); + launcher.set_mode(SamMode::LauncherOnly); + sim.sams.push(launcher); + assert_eq!(sim.battery_status(), vec![ + BatteryStatus::Radar { index: 1, role: RadarRole::Engagement, emitting: true, operational: true }, + BatteryStatus::Launcher { index: 2, kind: SamKind::S400, mode: SamMode::LauncherOnly, linked: true }, + ]); + sim.sams[1].radar_on = false; + assert_eq!(sim.battery_status(), vec![ + BatteryStatus::Radar { index: 1, role: RadarRole::Engagement, emitting: false, operational: false }, + BatteryStatus::Launcher { index: 2, kind: SamKind::S400, mode: SamMode::LauncherOnly, linked: false }, + ]); + } + fn track(now: u64) -> Track { Track { id: 42, kind: DroneKind::Kh101, pos: DVec3::new(0.0, 100.0, -200.0), vel: DVec3::ZERO, tick: now, own_tick: now, ttl: 400, hits: 3, locked: false } }