Observe emitting radars and expose emission control
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -71,7 +71,7 @@ pub fn tree() -> Tree {
|
||||
Node::lit("target").then(Node::lit("here").runs()),
|
||||
Node::lit("give").runs(),
|
||||
Node::lit("battery").runs().then(Node::arg("номер", Value::int_from(1)).then(Node::arg("режим", Value::choice(["integrated", "launcher"])).runs())),
|
||||
Node::lit("radar").then(Node::arg("номер", Value::int_from(1)).then(Node::arg("роль", Value::choice(["multi", "search", "engagement", "height"])).runs())),
|
||||
Node::lit("radar").then(Node::arg("номер", Value::int_from(1)).then(Node::arg("роль", Value::choice(["on", "off", "multi", "search", "engagement", "height"])).runs())),
|
||||
Node::lit("deploy").then(Node::arg("комплекс", Value::choice(deploy_kinds)).runs()),
|
||||
Node::lit("wave").then(
|
||||
Node::arg("тип", Value::choice(wave_kinds)).runs().then(
|
||||
@@ -166,7 +166,7 @@ mod tests {
|
||||
"replay export mine all fps=30 size=1280x720 crf=18 sec=12.5 noaudio", "replay export fps=60", "replay stop",
|
||||
"scenario save a", "scenario save a fresh", "scenario load a", "scenario load a force", "scenario list",
|
||||
"scenario delete a", "why", "why 2", "ew on", "ew off", "move 4", "target here", "targets clear",
|
||||
"sites", "strike 3", "battery", "battery 1 launcher", "battery 1 integrated", "radar 2 engagement", "radar 2 search", "deploy s400_launcher", "deploy s500_launcher", "deploy radar_search", "deploy radar_track", "deploy radar_height", "deploy patriot", "deploy light", "deploy s400", "wave mix", "wave shahed", "wave shahed 20", "wave kh101 80 800", "wave shahed 20 4000",
|
||||
"sites", "strike 3", "battery", "battery 1 launcher", "battery 1 integrated", "radar 2 on", "radar 2 off", "radar 2 engagement", "radar 2 search", "deploy s400_launcher", "deploy s500_launcher", "deploy radar_search", "deploy radar_track", "deploy radar_height", "deploy patriot", "deploy light", "deploy s400", "wave mix", "wave shahed", "wave shahed 20", "wave kh101 80 800", "wave shahed 20 4000",
|
||||
"wave oreshnik 1 60", "damage on", "damage off", "truth on", "shader hd", "shader off", "shader on", "hold on", "lastchance on", "lastchance off",
|
||||
"roles all default", "doctrine", "doctrine 1 reserve 4", "doctrine all salvo eco", "doctrine 2 sector 90 45", "doctrine 2 sector off", "doctrine 1 guard 1,2", "doctrine 1 reset", "roles patriot drone,jet", "roles pantsir all", "roles tor ballistic",
|
||||
] {
|
||||
|
||||
@@ -208,12 +208,15 @@ mod tests {
|
||||
fn battery_modes_load_and_old_layouts_default() {
|
||||
let old = Layout::parse("sam s400 0 70 0 0").unwrap();
|
||||
assert_eq!(old.sams[0].mode, sim::battery::SamMode::Integrated);
|
||||
assert!(old.sams[0].radar_on);
|
||||
assert_eq!(old.sams[0].radar_role, sim::battery::RadarRole::Multifunction);
|
||||
let data = "sam s400 0 70 0 0 30 1 0 4 0 0 0 - - - 1 1 1 1 1 1 0 0 0 launcher multi 5\nsam radar 1 70 0 0 0 1 0 0 0 0 0 - - - 1 1 1 1 1 1 0 0 0 integrated engagement";
|
||||
let l = Layout::parse(data).unwrap();
|
||||
assert_eq!(l.sams[0].mode, sim::battery::SamMode::LauncherOnly);
|
||||
assert_eq!(l.sams[0].spent_cells, 5);
|
||||
assert_eq!(l.sams[1].radar_role, sim::battery::RadarRole::Engagement);
|
||||
assert!(!Layout::parse(&data.replace("launcher multi 5", "launcher multi 5 false")).unwrap().sams[0].radar_on);
|
||||
assert!(Layout::parse(&data.replace("launcher multi 5", "launcher multi 5 nope")).is_err());
|
||||
assert!(Layout::parse(&data.replace("engagement", "invalid")).is_err());
|
||||
assert!(Layout::parse(&data.replace("launcher multi 5", "launcher multi 256")).is_err());
|
||||
}
|
||||
|
||||
+12
-1
@@ -104,6 +104,7 @@ enum Act {
|
||||
Reserve,
|
||||
Salvo,
|
||||
Role(u8),
|
||||
RadarEmission,
|
||||
ResetDoc,
|
||||
Strike(u32),
|
||||
Centre,
|
||||
@@ -1033,6 +1034,12 @@ impl AirDef {
|
||||
}
|
||||
ui.mode = Mode::View;
|
||||
}
|
||||
Act::RadarEmission => {
|
||||
if let Some(i) = self.sel_sam(ui) {
|
||||
self.sim.sams[i].radar_on = !self.sim.sams[i].radar_on;
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
Act::Role(bit) => {
|
||||
if let Some(i) = self.sel_sam(ui) {
|
||||
let s = &mut self.sim.sams[i];
|
||||
@@ -2327,6 +2334,10 @@ impl AirDef {
|
||||
} else if sam.mode == sim::battery::SamMode::LauncherOnly {
|
||||
lines.push(("Отдельная пусковая: внешняя РЛС сопровождения".into(), WHITE));
|
||||
}
|
||||
if k.spec().sensor > 0.0 && sam.mode != sim::battery::SamMode::LauncherOnly {
|
||||
lines.push((format!("Излучение РЛС: {}", if sam.radar_on { "вкл" } else { "выкл" }), WHITE));
|
||||
btns.push((if sam.radar_on { "Выключить РЛС" } else { "Включить РЛС" }.into(), Act::RadarEmission, sam.radar_on));
|
||||
}
|
||||
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() {
|
||||
@@ -2334,7 +2345,7 @@ impl AirDef {
|
||||
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));
|
||||
}
|
||||
if let Some(sensor) = self.sim.low_sensor(k, sam.pos, Some(sam.id)) {
|
||||
if let Some(sensor) = sam.radar_up().then(|| self.sim.low_sensor(k, sam.pos, Some(sam.id))).flatten() {
|
||||
lines.push((format!("Свой обзор для низкой цели: {} (зелёное кольцо)", distance(sensor.reach)), WHITE));
|
||||
}
|
||||
if let Some(t) = sam.gun_target.or(sam.target) {
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Sim {
|
||||
pub fn battery_status(&self) -> Vec<String> {
|
||||
self.sams.iter().enumerate().filter_map(|(i, sam)| {
|
||||
if sam.kind == SamKind::Radar {
|
||||
Some(format!("№{} РЛС: {}, {}", i + 1, sam.radar_role.name(), if sam.radar_up() && sam.link_range() > 0.0 { "работает" } else { "нет связи или РЛС не работает" }))
|
||||
Some(format!("№{} РЛС: {}, излучение {}, {}", i + 1, sam.radar_role.name(), if sam.radar_on { "вкл" } else { "выкл" }, if sam.radar_up() && sam.link_range() > 0.0 { "работает" } else { "нет связи или РЛС не работает" }))
|
||||
} 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 { "" }))
|
||||
@@ -255,3 +255,35 @@ mod tests {
|
||||
assert_eq!(other.mode, SamMode::Integrated);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod emission_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn silence_accepts_network_without_inventing_own_measurements_and_contacts_age() {
|
||||
let mut sim = Sim::new(121);
|
||||
sim.tick = 20;
|
||||
let mut source = Sam::new(1, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
let mut receiver = Sam::new(2, SamKind::Radar, DVec3::ZERO, 0.0);
|
||||
receiver.set_radar_role(RadarRole::Engagement);
|
||||
receiver.radar_on = false;
|
||||
sim.add_drone(DroneKind::Kh101, DVec3::new(0.0, 70.0, -100.0), DVec3::Z, DVec3::ZERO);
|
||||
let id = sim.drones[0].id;
|
||||
source.tracks.push(Track { id, kind: DroneKind::Kh101, pos: sim.drones[0].pos, vel: DVec3::ZERO, tick: 20, own_tick: 20, ttl: 200, hits: 2, locked: false });
|
||||
sim.sams.extend([source, receiver]);
|
||||
let mut ground = Ground::default();
|
||||
sim.update_sight(&mut ground);
|
||||
assert_eq!(sim.sams[1].track(id).unwrap().own_tick, 0);
|
||||
assert_eq!(sim.sams[1].track(id).unwrap().tick, 20);
|
||||
assert!(!sim.sams[1].radar_up());
|
||||
assert_eq!(sim.known_units(), vec![(1, DVec3::ZERO)]);
|
||||
sim.sams[0].radar_on = false;
|
||||
sim.sams[0].pos = DVec3::new(400.0, 0.0, 0.0);
|
||||
sim.tick = 40;
|
||||
sim.update_sight(&mut ground);
|
||||
assert_eq!(sim.known_units(), vec![(1, DVec3::ZERO)]);
|
||||
assert_eq!(sim.sams[1].track(id).unwrap().tick, 20);
|
||||
sim.tick = 6020;
|
||||
assert!(sim.known_units().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,8 @@ impl Sim {
|
||||
/// Units the enemy knows of and would go for, newest first: (id, where).
|
||||
pub(super) fn known_units(&self) -> Vec<(u32, DVec3)> {
|
||||
let mut out: Vec<(u32, DVec3)> = Vec::new();
|
||||
for &(id, at, _) in self.intel.revealed.iter().rev() {
|
||||
for &(id, at, observed) in self.intel.revealed.iter().rev() {
|
||||
if self.tick.saturating_sub(observed) >= 6000 { continue; }
|
||||
if !out.iter().any(|o| o.0 == id) && self.sams.iter().any(|s| s.id == id && s.alive()) {
|
||||
out.push((id, at));
|
||||
}
|
||||
@@ -145,10 +146,9 @@ impl Sim {
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether the enemy has this unit in its intelligence: a radar emits, so it is known; the rest only once
|
||||
/// they gave themselves away (a launch, jamming) and are in `known`.
|
||||
/// Whether an observation or another disclosure placed this unit in enemy intelligence.
|
||||
pub(super) fn enemy_knows(s: &Sam, known: &[(u32, DVec3)]) -> bool {
|
||||
s.alive() && (s.kind == SamKind::Radar || known.iter().any(|k| k.0 == s.id))
|
||||
s.alive() && known.iter().any(|k| k.0 == s.id)
|
||||
}
|
||||
|
||||
/// During a smart raid: its drones not yet launched go for what just gave itself away (a jamming station
|
||||
|
||||
+22
-18
@@ -588,10 +588,14 @@ impl Sim {
|
||||
// themselves away; the rest of the player's layout is not in its hands.
|
||||
let known = self.known_units();
|
||||
let alive: Vec<&Sam> = alive.iter().copied().filter(|s| Self::enemy_knows(s, &known)).collect();
|
||||
let pick_known = |list: &[&Sam], r: f64| -> Option<(DVec3, u32)> {
|
||||
let s = *list.get((r * list.len() as f64) as usize)?;
|
||||
known.iter().find(|(id, _)| *id == s.id).map(|(_, pos)| (*pos, s.id))
|
||||
};
|
||||
let known_ew: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind == SamKind::Ew).collect();
|
||||
if matches!(kind, DroneKind::Lancet | DroneKind::Kh22) && !known_ew.is_empty() && roll < 0.6 {
|
||||
let s = known_ew[(r * known_ew.len() as f64) as usize % known_ew.len()];
|
||||
return (s.pos, Some(s.id));
|
||||
return (known.iter().find(|(id, _)| *id == s.id).unwrap().1, Some(s.id));
|
||||
}
|
||||
// Short-range systems (guns, Tor, P1-SUN) for the Lancets; long-range ones and the radars
|
||||
// that feed them for the fast missiles.
|
||||
@@ -599,10 +603,10 @@ impl Sim {
|
||||
let long: Vec<&Sam> =
|
||||
alive.iter().copied().filter(|s| s.kind.range() >= 450.0 || s.kind == SamKind::Radar).collect();
|
||||
let choice = match kind {
|
||||
DroneKind::Lancet if roll < 0.75 => pick(&guns, r).or_else(|| pick(&alive, r)),
|
||||
DroneKind::Kh22 | DroneKind::Zircon if roll < 0.45 => pick(&long, r),
|
||||
DroneKind::Iskander | DroneKind::Kinzhal if roll < 0.3 => pick(&long, r),
|
||||
DroneKind::Shahed | DroneKind::Geran3 if roll < 0.2 => pick(&alive, r),
|
||||
DroneKind::Lancet if roll < 0.75 => pick_known(&guns, r).or_else(|| pick_known(&alive, r)),
|
||||
DroneKind::Kh22 | DroneKind::Zircon if roll < 0.45 => pick_known(&long, r),
|
||||
DroneKind::Iskander | DroneKind::Kinzhal if roll < 0.3 => pick_known(&long, r),
|
||||
DroneKind::Shahed | DroneKind::Geran3 if roll < 0.2 => pick_known(&alive, r),
|
||||
_ => None,
|
||||
};
|
||||
choice.map_or((object, None), |(p, id)| (p, Some(id)))
|
||||
@@ -627,11 +631,12 @@ impl Sim {
|
||||
if kind.jammable() {
|
||||
let known = self.known_units();
|
||||
for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known.iter().any(|k| k.0 == s.id)) {
|
||||
let (ci, cj) = grid.index(s.pos);
|
||||
let pos = known.iter().find(|(id, _)| *id == s.id).unwrap().1;
|
||||
let (ci, cj) = grid.index(pos);
|
||||
let r = (EW_RADIUS / grid.cell).ceil() as i32;
|
||||
for i in (ci - r).max(0)..=(ci + r).min(n - 1) {
|
||||
for j in (cj - r).max(0)..=(cj + r).min(n - 1) {
|
||||
if horizontal(grid.centre((i, j), s.pos.y) - s.pos).length() < EW_RADIUS {
|
||||
if horizontal(grid.centre((i, j), pos.y) - pos).length() < EW_RADIUS {
|
||||
prices[(i * n + j) as usize] += 12.0;
|
||||
}
|
||||
}
|
||||
@@ -640,30 +645,29 @@ impl Sim {
|
||||
}
|
||||
// Only the systems the enemy knows of are priced in (#93): a hidden one is flown into.
|
||||
for s in self.sams.iter().filter(|s| s.alive() && s.roles & spec.class.bit() != 0 && Self::enemy_knows(s, &known_now)) {
|
||||
let pos = known_now.iter().find(|(id, _)| *id == s.id).unwrap().1;
|
||||
let sp = s.kind.spec();
|
||||
let (lo, hi) = sp.missile.as_ref().map_or((0.0, 0.0), |m| (m.min, m.max));
|
||||
let gun = sp.gun.as_ref().map_or(0.0, |g| g.range);
|
||||
// A gun needs its own radar; a missile also flies on the tracks of any radar within data-link range.
|
||||
let own = sight::reach(s, kind, height);
|
||||
let net = self
|
||||
.sams
|
||||
.iter()
|
||||
.filter(|r| r.alive() && r.id != s.id && r.pos.distance(s.pos) <= sight::LINK)
|
||||
.map(|r| sight::reach(r, kind, height))
|
||||
.fold(0.0, f64::max);
|
||||
let net = self.sams.iter().filter_map(|r| {
|
||||
let at = known_now.iter().find(|(id, _)| *id == r.id)?.1;
|
||||
(r.id != s.id && at.distance(pos) <= sight::LINK).then(|| sight::reach(r, kind, height))
|
||||
}).fold(0.0, f64::max);
|
||||
let (hi, gun) = (hi.min(own.max(net)), gun.min(own));
|
||||
let far = hi.max(gun);
|
||||
let at = sight::antenna(s);
|
||||
let horizon = self.horizons.get(&s.id);
|
||||
let at = pos + (sight::antenna(s) - s.pos);
|
||||
let horizon = (pos == s.pos).then(|| self.horizons.get(&s.id)).flatten();
|
||||
// Only the cells round it.
|
||||
let (ci, cj) = grid.index(s.pos);
|
||||
let (ci, cj) = grid.index(pos);
|
||||
let r = (far / grid.cell).ceil() as i32 + 1;
|
||||
for i in (ci - r).max(0)..=(ci + r).min(n - 1) {
|
||||
for j in (cj - r).max(0)..=(cj + r).min(n - 1) {
|
||||
cost += 1;
|
||||
let ground = land.map(|l| l[(i * n + j) as usize]).filter(|g| g.is_finite());
|
||||
let p = grid.centre((i, j), ground.map_or(s.pos.y + height, |g| f64::from(g) + height));
|
||||
let d = horizontal(p - s.pos).length();
|
||||
let p = grid.centre((i, j), ground.map_or(pos.y + height, |g| f64::from(g) + height));
|
||||
let d = horizontal(p - pos).length();
|
||||
if d > far {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -466,11 +466,20 @@ impl Sim {
|
||||
}
|
||||
// The network: every unit gets the tracks the radars within data-link range measured themselves,
|
||||
// with the time they were measured: a relayed track is never fresher than its measurement.
|
||||
// Fictional passive observation: hostile aircraft within 1600 blocks fix an emitting radar
|
||||
// every 20 ticks. Silence leaves the previous report to age normally.
|
||||
if now % 20 == 0 {
|
||||
for s in self.sams.iter().filter(|s| s.radar_up()) {
|
||||
if self.drones.iter().any(|d| d.alive() && d.pos.distance(s.pos) <= 1600.0) {
|
||||
self.intel.revealed.push((s.id, s.pos, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
let shared: Vec<(u32, DVec3, f64, Vec<Track>)> = self
|
||||
.sams
|
||||
.iter()
|
||||
.filter(|s| s.radar_up() && s.link_range() > 0.0)
|
||||
.map(|s| (s.id, s.pos, s.link_range(), s.tracks.iter().filter(|t| t.own_tick == t.tick).copied().collect::<Vec<_>>()))
|
||||
.map(|s| (s.id, s.pos, s.link_range(), s.tracks.iter().filter(|t| t.own_tick > 0 && t.own_tick == t.tick).copied().collect::<Vec<_>>()))
|
||||
.filter(|s| !s.3.is_empty())
|
||||
.collect();
|
||||
for sam in &mut self.sams {
|
||||
|
||||
Reference in New Issue
Block a user