Remove seed-only defenders and reject unsupported player-launched strikes (#118)
No compatible coordinate-strike loadout is implemented yet. Preserve flight and collision mechanics through explicit test fixtures without allowing generic stock or player position to create a production missile. Physical carriers, typed ammunition and logistics remain to implement. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
@@ -1118,7 +1118,6 @@ mod tests {
|
||||
launches: 1,
|
||||
last: 0,
|
||||
state: super::super::strike::SiteState::Active,
|
||||
cover: None,
|
||||
aimed: false,
|
||||
});
|
||||
id
|
||||
@@ -1126,11 +1125,10 @@ mod tests {
|
||||
#[test]
|
||||
fn a_wave_keeps_the_site_a_strike_is_still_flying_to() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0));
|
||||
let mut ground = Ground::default();
|
||||
let mut out = Out::default();
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
// The next wave is planned while the missile is in the air: its housekeeping must not take the target away.
|
||||
let refs = sim.site_refs();
|
||||
assert!(refs.contains(&id), "the flight is no reference to its site");
|
||||
|
||||
+83
-110
@@ -10,11 +10,10 @@
|
||||
//! Directly observed launches provide independent point sightings, combined by weighted least squares (`solve`).
|
||||
//! A site without a sufficiently accurate fix remains a bearing and cannot be struck.
|
||||
//!
|
||||
//! **Striking it.** `order_strike`: one of a few strike missiles (a stock that refills slowly), fired from where the
|
||||
//! player stands at the *estimated* place. It flies `STRIKE_SPEED`; on arrival the enemy's cover may shoot it down; else
|
||||
//! it lands with its own scatter. The true distance from the site decides: inside `HIT_R` the site is destroyed (stock
|
||||
//! burnt, jobs cancelled, wreck and fire), inside `SUPPRESS_R` it is stunned for a while (its launches are delayed, not
|
||||
//! cancelled), else it hit the ground beside it and nothing changed. So a badly known site is very likely a wasted missile.
|
||||
//! **Striking it.** Production orders require a real carrier and an explicitly supported coordinate-strike
|
||||
//! munition profile. No such loadout is currently implemented, so orders are rejected without spending ammunition.
|
||||
//! The flight/impact mechanics remain available to test fixtures and existing flights; there is no virtual enemy
|
||||
//! air defence. Only an actual simulated interceptor can justify an intercepted outcome.
|
||||
//!
|
||||
//! **Off the map** (`offmap`: no land was found or loaded, so no kit stands anywhere): the site is scouted like any other,
|
||||
//! as bearing and range, but there is nothing to see burn. A strike that would have hit just ends its launches
|
||||
@@ -56,9 +55,6 @@ pub struct Tune {
|
||||
pub suppress_r: f64,
|
||||
/// Ticks a stunned site launches nothing.
|
||||
pub suppress_ticks: u64,
|
||||
/// The chance the enemy's cover shoots the missile down: none, light, heavy; and the shares of sites that have them.
|
||||
pub cover_p: [f64; 3],
|
||||
pub cover_share: [f64; 2],
|
||||
/// Ticks the wreck of a destroyed site smokes.
|
||||
pub burn: u64,
|
||||
/// A launched drone not taken into account after this many ticks is forgotten.
|
||||
@@ -84,8 +80,6 @@ pub const TUNE: Tune = Tune {
|
||||
hit_r: 35.0,
|
||||
suppress_r: 90.0,
|
||||
suppress_ticks: 600,
|
||||
cover_p: [0.0, 0.3, 0.65],
|
||||
cover_share: [0.4, 0.75],
|
||||
burn: 1800,
|
||||
scout_life: 1800,
|
||||
};
|
||||
@@ -240,8 +234,6 @@ pub struct Known {
|
||||
pub launches: u32,
|
||||
pub last: u64,
|
||||
pub state: SiteState,
|
||||
/// What a strike taught of its cover (0 none, 1 light, 2 heavy).
|
||||
pub cover: Option<u8>,
|
||||
/// Where the strike on it is aimed, if one is on its way.
|
||||
pub aimed: bool,
|
||||
}
|
||||
@@ -265,8 +257,6 @@ pub struct Target {
|
||||
pub stock: u32,
|
||||
pub offmap: bool,
|
||||
pub alive: bool,
|
||||
/// What its cover was (0 none, 1 light, 2 heavy), fixed at the launch.
|
||||
pub cover: u8,
|
||||
}
|
||||
|
||||
/// A strike missile in flight: the shot it was fired as, where it is, and where it is going.
|
||||
@@ -321,8 +311,6 @@ pub struct Strikes {
|
||||
pub stunned: u32,
|
||||
pub missed: u32,
|
||||
pub shot_down: u32,
|
||||
/// The enemy's cover forced (tests); else it follows the seed.
|
||||
pub cover_override: Option<u8>,
|
||||
}
|
||||
|
||||
impl Strikes {
|
||||
@@ -391,33 +379,11 @@ impl Known {
|
||||
}
|
||||
}
|
||||
};
|
||||
let cover = match self.cover {
|
||||
Some(0) => " · ПВО не прикрыта",
|
||||
Some(_) => " · прикрыта ПВО",
|
||||
None => "",
|
||||
};
|
||||
format!("ПУ №{} ({}): {place}{state}{cover}; пусков замечено {}", self.site, class_name(self.class), self.launches)
|
||||
format!("ПУ №{} ({}): {place}{state}; пусков замечено {}", self.site, class_name(self.class), self.launches)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sim {
|
||||
/// Whether the enemy covers this site: 0 none, 1 light, 2 heavy; fixed by the seed and the site.
|
||||
pub fn cover_of(&self, site: &LaunchSite) -> u8 {
|
||||
if let Some(c) = self.strikes.cover_override {
|
||||
return c;
|
||||
}
|
||||
let r = dice(self.seed, site.id as u64, site.side as u64, 101);
|
||||
// A ballistic battery is a prize: guarded more often.
|
||||
let r = if site.class == SiteClass::Battery { r * 0.7 } else { r };
|
||||
if r < TUNE.cover_share[0] {
|
||||
0
|
||||
} else if r < TUNE.cover_share[1] {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
/// A drone just left site `site` (a launch the radars may have seen).
|
||||
pub(super) fn scout_launch(&mut self, drone: u32, site: u32, kind: DroneKind, at: DVec3) {
|
||||
let Some(s) = self.sites.get(site) else { return };
|
||||
@@ -455,7 +421,7 @@ impl Sim {
|
||||
let i = match self.strikes.known.iter().position(|k| k.site == site) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
self.strikes.known.push(Known { site, class, obs: Vec::new(), est: None, launches: 0, last: now, state: SiteState::Active, cover: None, aimed: false });
|
||||
self.strikes.known.push(Known { site, class, obs: Vec::new(), est: None, launches: 0, last: now, state: SiteState::Active, aimed: false });
|
||||
self.strikes.known.len() - 1
|
||||
}
|
||||
};
|
||||
@@ -494,6 +460,7 @@ impl Sim {
|
||||
pub(super) fn step_strikes(&mut self, ground: &mut Ground, player: Option<DVec3>, out: &mut Out) {
|
||||
let now = self.tick;
|
||||
out.messages.append(&mut self.strikes.news);
|
||||
#[cfg(any(test, feature = "test-fixtures"))]
|
||||
if self.strikes.stock < TUNE.stock_max {
|
||||
self.strikes.progress += 1;
|
||||
if self.strikes.progress >= TUNE.resupply {
|
||||
@@ -679,8 +646,22 @@ impl Sim {
|
||||
v
|
||||
}
|
||||
|
||||
/// Fires a strike missile at known site `id` from `from`. Returns the line for the player.
|
||||
pub fn order_strike(&mut self, id: u32, from: DVec3, out: &mut Out) -> Result<String, String> {
|
||||
/// A manual order never substitutes the player or a generic stock for a physical launcher.
|
||||
pub fn order_strike(&mut self, id: u32, _from: DVec3, _out: &mut Out) -> Result<String, String> {
|
||||
self.strikes.get(id).ok_or_else(|| format!("ПУ №{id} не найдена: /airdef sites покажет известные"))?;
|
||||
Err(if self.sams.is_empty() {
|
||||
"Удар недоступен: нет собственной пусковой с совместимым боеприпасом".into()
|
||||
} else {
|
||||
let reason = match super::catalog::unit(self.sams[0].kind).ground_strike() {
|
||||
super::catalog::GroundStrike::Unavailable { reason } => reason,
|
||||
};
|
||||
format!("Удар недоступен: {reason}; совместимый боеприпас не загружен")
|
||||
})
|
||||
}
|
||||
|
||||
/// Synthetic launcher used only by flight/impact regression fixtures; absent from production builds.
|
||||
#[cfg(any(test, feature = "test-fixtures"))]
|
||||
pub fn order_test_strike(&mut self, id: u32, from: DVec3, out: &mut Out) -> Result<String, String> {
|
||||
let k = self.strikes.get(id).ok_or_else(|| format!("ПУ №{id} не найдена: /airdef sites покажет известные"))?;
|
||||
let est = k.est.filter(|e| e.fix).ok_or_else(|| {
|
||||
format!("ПУ №{id}: места не знаем, только азимут. Нужно больше пусков на виду или вторая РЛС в стороне от первой")
|
||||
@@ -700,7 +681,7 @@ impl Sim {
|
||||
}
|
||||
// The target as it is now: the missile is judged against this whatever becomes of the site meanwhile (#108).
|
||||
let site = self.sites.get(id).ok_or_else(|| format!("ПУ №{id} уже нет на карте"))?;
|
||||
let snap = Target { pos: DVec2::new(site.pos.x, site.pos.z), stock: site.stock, offmap: site.offmap, alive: site.alive, cover: self.cover_of(site) };
|
||||
let snap = Target { pos: DVec2::new(site.pos.x, site.pos.z), stock: site.stock, offmap: site.offmap, alive: site.alive };
|
||||
self.strikes.stock -= 1;
|
||||
self.strikes.fired += 1;
|
||||
self.strikes.next += 1;
|
||||
@@ -723,7 +704,6 @@ impl Sim {
|
||||
});
|
||||
if let Some(k) = self.strikes.known.iter_mut().find(|k| k.site == id) {
|
||||
k.aimed = true;
|
||||
k.cover = Some(snap.cover);
|
||||
}
|
||||
out.sound(DroneKind::Kalibr.launch_sound(), start, 1.0, 1.0);
|
||||
out.particles("minecraft:large_smoke", start, 0.6, DVec3::ZERO, 0.05, 30);
|
||||
@@ -742,7 +722,6 @@ impl Sim {
|
||||
let now = self.tick;
|
||||
if let Some(k) = self.strikes.known.iter_mut().find(|k| k.site == f.site) {
|
||||
k.aimed = false;
|
||||
k.cover = Some(f.snap.cover);
|
||||
}
|
||||
// The site as it stands now; if it has been taken off the map since the launch (a new wave's housekeeping,
|
||||
// a clear), the shot was still fired at that place: it has its outcome by its coordinates (#108).
|
||||
@@ -750,16 +729,7 @@ impl Sim {
|
||||
Some(s) => (DVec2::new(s.pos.x, s.pos.z), s.offmap, s.alive, s.stock),
|
||||
None => (f.snap.pos, f.snap.offmap, f.snap.alive, f.snap.stock),
|
||||
};
|
||||
let cover = f.snap.cover;
|
||||
// The enemy's cover.
|
||||
if dice(self.seed, f.id as u64, f.site as u64, 201) < TUNE.cover_p[cover as usize] {
|
||||
self.strikes.shot_down += 1;
|
||||
let high = at + DVec3::Y * 40.0;
|
||||
self.blast_at(high, 0, true, false, ground, player, out);
|
||||
self.emit_combat_event(high, record::EventKind::Strike { site: f.site, result: record::StrikeResult::Intercepted });
|
||||
out.messages.push(format!("Удар по ПУ №{}: ракета сбита ПВО противника у площадки, цель цела", f.site));
|
||||
return;
|
||||
}
|
||||
// No seed-only defender: interceptions must come from physical simulated combat (#118).
|
||||
// Where it lands was fixed at the launch; how far from the site decides.
|
||||
let miss = (f.land - truth).length();
|
||||
let result = if offmap && miss <= TUNE.hit_r {
|
||||
@@ -835,7 +805,7 @@ impl Sim {
|
||||
}
|
||||
|
||||
/// A strike missile met the land or a building short of its aim: it goes off there, and the site is untouched
|
||||
/// because nothing was aimed at. One result for the statistics and the record, before any cover (#106).
|
||||
/// because nothing was aimed at. One result for the statistics and the record, before impact (#106).
|
||||
fn crash_strike(&mut self, f: Flight, at: DVec3, ground: &mut Ground, player: Option<DVec3>, out: &mut Out) {
|
||||
if let Some(k) = self.strikes.known.iter_mut().find(|k| k.site == f.site) {
|
||||
k.aimed = false;
|
||||
@@ -880,14 +850,7 @@ impl Sim {
|
||||
if v.is_empty() {
|
||||
v.push("Известных ПУ нет: их находят РЛС по пускам и трекам вылетевших целей".into());
|
||||
}
|
||||
v.push(format!(
|
||||
"Ударных ракет: {}/{}{} · в полёте {} · дальность {}",
|
||||
self.strikes.stock,
|
||||
TUNE.stock_max,
|
||||
if self.strikes.stock < TUNE.stock_max { format!(" (следующая через {} с)", self.strikes.seconds_to_next()) } else { String::new() },
|
||||
self.strikes.flights.len(),
|
||||
super::distance(TUNE.range)
|
||||
));
|
||||
v.push("Удар по координатам недоступен: требуется реализованный совместимый носитель и боеприпас".into());
|
||||
v
|
||||
}
|
||||
}
|
||||
@@ -946,7 +909,7 @@ mod tests {
|
||||
/// (None: no radar).
|
||||
fn scene(seed: u64, n: u32, radar: Option<DVec3>) -> (Sim, Ground, Out, u32) {
|
||||
let mut sim = Sim::new(seed);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
if let Some(at) = radar {
|
||||
let id = sim.id();
|
||||
sim.sams.push(Sam::new(id, SamKind::Patriot, at, 0.0));
|
||||
@@ -1066,10 +1029,10 @@ mod tests {
|
||||
let before = sim.launched;
|
||||
assert!(before >= 3 && before < 14, "{before}");
|
||||
let player = DVec3::new(0.0, 64.0, 0.0);
|
||||
let said = sim.order_strike(id, player, &mut out).unwrap();
|
||||
let said = sim.order_test_strike(id, player, &mut out).unwrap();
|
||||
assert!(said.contains("ПУ"), "{said}");
|
||||
assert_eq!(sim.strikes.stock, TUNE.stock - 1);
|
||||
assert!(sim.order_strike(id, player, &mut out).is_err(), "a second missile at the same site while one flies");
|
||||
assert!(sim.order_test_strike(id, player, &mut out).is_err(), "a second missile at the same site while one flies");
|
||||
out.messages.clear();
|
||||
run(&mut sim, &mut ground, &mut out, 400);
|
||||
let site = sim.sites.get(id).unwrap();
|
||||
@@ -1080,7 +1043,7 @@ mod tests {
|
||||
assert!(sim.strikes.wrecks.len() == 1 && sim.fires.list.len() == 1, "a wreck and a fire");
|
||||
assert!(!sim.blasts.is_empty() || sim.tick > 0);
|
||||
assert!(matches!(sim.strikes.get(id).unwrap().state, SiteState::Destroyed(_)));
|
||||
assert!(sim.order_strike(id, player, &mut out).is_err(), "destroyed already");
|
||||
assert!(sim.order_test_strike(id, player, &mut out).is_err(), "destroyed already");
|
||||
// The result is on the record for replays and stats.
|
||||
assert_eq!((sim.strikes.fired, sim.strikes.destroyed), (1, 1));
|
||||
}
|
||||
@@ -1099,7 +1062,7 @@ mod tests {
|
||||
k.est = Some(e);
|
||||
sim.strikes.known[0].obs.clear();
|
||||
let queued = sim.queue.len();
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
out.messages.clear();
|
||||
run(&mut sim, &mut ground, &mut out, 300);
|
||||
assert!(out.messages.iter().any(|m| m.contains("промах")), "{:?}", out.messages);
|
||||
@@ -1117,7 +1080,7 @@ mod tests {
|
||||
let mut e = k.est.unwrap();
|
||||
e.pos = DVec2::new(0.0, 300.0 + 62.0);
|
||||
k.est = Some(e);
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
let queued = sim.queue.len();
|
||||
run(&mut sim, &mut ground, &mut out, 300);
|
||||
if sim.strikes.stunned == 1 {
|
||||
@@ -1143,14 +1106,13 @@ mod tests {
|
||||
launches: 1,
|
||||
last: 0,
|
||||
state: SiteState::Active,
|
||||
cover: None,
|
||||
aimed: false,
|
||||
});
|
||||
assert!(!sim.strikes.known[0].est.unwrap().fix);
|
||||
let e = sim.order_strike(id, DVec3::ZERO, &mut out).unwrap_err();
|
||||
let e = sim.order_test_strike(id, DVec3::ZERO, &mut out).unwrap_err();
|
||||
assert!(e.contains("азимут"), "{e}");
|
||||
assert_eq!(sim.strikes.stock, TUNE.stock, "no missile spent");
|
||||
assert!(sim.order_strike(99, DVec3::ZERO, &mut out).is_err());
|
||||
assert!(sim.order_test_strike(99, DVec3::ZERO, &mut out).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1158,9 +1120,9 @@ mod tests {
|
||||
let (mut sim, mut ground, mut out, id) = scene(11, 14, Some(DVec3::new(0.0, 64.0, 200.0)));
|
||||
run(&mut sim, &mut ground, &mut out, 120);
|
||||
// Too far.
|
||||
assert!(sim.order_strike(id, DVec3::new(0.0, 64.0, 300.0 - TUNE.range - 100.0), &mut out).is_err());
|
||||
assert!(sim.order_test_strike(id, DVec3::new(0.0, 64.0, 300.0 - TUNE.range - 100.0), &mut out).is_err());
|
||||
sim.strikes.stock = 0;
|
||||
let e = sim.order_strike(id, DVec3::ZERO, &mut out).unwrap_err();
|
||||
let e = sim.order_test_strike(id, DVec3::ZERO, &mut out).unwrap_err();
|
||||
assert!(e.contains("нет"), "{e}");
|
||||
sim.strikes.progress = TUNE.resupply - 1;
|
||||
run(&mut sim, &mut ground, &mut out, 2);
|
||||
@@ -1168,26 +1130,38 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enemy_cover_shoots_some_strikes_down_and_none_with_no_cover() {
|
||||
let mut down = [0u32; 3];
|
||||
for cover in 0..3u8 {
|
||||
for seed in 1..=30u64 {
|
||||
let (mut sim, mut ground, mut out, id) = scene(seed, 14, Some(DVec3::new(0.0, 64.0, 200.0)));
|
||||
sim.strikes.cover_override = Some(cover);
|
||||
run(&mut sim, &mut ground, &mut out, 120);
|
||||
sim.order_strike(id, DVec3::ZERO, &mut out).unwrap();
|
||||
run(&mut sim, &mut ground, &mut out, 200);
|
||||
down[cover as usize] += sim.strikes.shot_down;
|
||||
}
|
||||
fn no_seed_only_enemy_defence_can_intercept_a_flight() {
|
||||
for seed in 1..=30u64 {
|
||||
let (mut sim, mut ground, mut out, id) = scene(seed, 14, Some(DVec3::new(0.0, 64.0, 200.0)));
|
||||
run(&mut sim, &mut ground, &mut out, 120);
|
||||
sim.order_test_strike(id, DVec3::ZERO, &mut out).unwrap();
|
||||
run(&mut sim, &mut ground, &mut out, 200);
|
||||
assert_eq!(sim.strikes.shot_down, 0, "seed {seed}");
|
||||
assert!(!out.messages.iter().any(|m| m.contains("сбита ПВО противника")));
|
||||
assert!(sim.rec.iter().flat_map(|r| &r.events).chain(sim.replays.iter().flat_map(|r| &r.events)).all(|e| !matches!(e.kind, record::EventKind::Strike { result: record::StrikeResult::Intercepted, .. })));
|
||||
}
|
||||
assert_eq!(down[0], 0);
|
||||
assert!(down[1] > 3 && down[1] < 20 && down[2] > down[1], "{down:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_orders_never_launch_from_player_or_spend_generic_stock() {
|
||||
let (mut sim, mut ground, mut out, id) = scene(11, 14, Some(DVec3::new(0.0, 64.0, 200.0)));
|
||||
run(&mut sim, &mut ground, &mut out, 120);
|
||||
let stock = sim.strikes.stock;
|
||||
for player in [DVec3::ZERO, DVec3::new(4000.0, 64.0, 300.0)] {
|
||||
let e = sim.order_strike(id, player, &mut out).unwrap_err();
|
||||
assert!(e.contains("боеприпас"), "{e}");
|
||||
assert!(sim.strikes.flights.is_empty());
|
||||
assert_eq!(sim.strikes.stock, stock);
|
||||
assert_eq!(sim.strikes.fired, 0);
|
||||
}
|
||||
sim.sams.clear();
|
||||
assert!(sim.order_strike(id, DVec3::ZERO, &mut out).unwrap_err().contains("нет собственной"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_off_map_site_is_found_by_bearing_and_a_strike_there_only_ends_its_launches() {
|
||||
let mut sim = Sim::new(5);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
// Two radars apart watch the start of an off-map site's drones.
|
||||
for x in [-150.0, 150.0] {
|
||||
let id = sim.id();
|
||||
@@ -1224,7 +1198,7 @@ mod tests {
|
||||
// ... and the shot aims at what is known: force a good fix to test the outcome.
|
||||
let k = sim.strikes.known.iter_mut().find(|k| k.site == id).unwrap();
|
||||
k.est = Some(Estimate { pos: DVec2::new(0.0, 700.0), radius: 20.0, fix: true, bearing: 3.14, bearing_err: 0.01, origin: DVec2::ZERO });
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
out.messages.clear();
|
||||
let wrecks = sim.fires.list.len();
|
||||
run(&mut sim, &mut ground, &mut out, 500);
|
||||
@@ -1239,7 +1213,7 @@ mod tests {
|
||||
let (mut sim, mut ground, mut out, id) = scene(11, 14, Some(DVec3::new(0.0, 64.0, 200.0)));
|
||||
run(&mut sim, &mut ground, &mut out, 100);
|
||||
// A recording is running (the launched drones started it): the strike lands in it.
|
||||
sim.order_strike(id, DVec3::ZERO, &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::ZERO, &mut out).unwrap();
|
||||
run(&mut sim, &mut ground, &mut out, 200);
|
||||
let count = |r: &record::Recording| r.events.iter().filter(|e| matches!(e.kind, record::EventKind::Strike { .. })).count();
|
||||
let n = sim.rec.iter().chain(sim.replays.iter()).map(count).sum::<usize>();
|
||||
@@ -1262,7 +1236,7 @@ mod tests {
|
||||
let land = Land(|_, _| 64);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let mut sim = Sim::new(21);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let order = RaidOrder { kinds: vec![(DroneKind::Shahed, 30)], approach: Approach::Sector, dist: 300.0, aim: Aim::Objects };
|
||||
sim.order_raid(&order, &[DVec3::new(0.0, 70.0, 0.0)], &mut out);
|
||||
let mut n = 0;
|
||||
@@ -1283,7 +1257,7 @@ mod tests {
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
if !struck && sim.strikes.get(site.id).is_some_and(|k| k.est.is_some_and(|e| e.fix && e.radius < 40.0)) && sim.sites.pending(site.id) > 3 {
|
||||
left_at_strike = sim.sites.pending(site.id);
|
||||
sim.order_strike(site.id, DVec3::new(0.0, 70.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(site.id, DVec3::new(0.0, 70.0, 0.0), &mut out).unwrap();
|
||||
struck = true;
|
||||
}
|
||||
}
|
||||
@@ -1360,7 +1334,6 @@ mod tests {
|
||||
launches: 1,
|
||||
last: 0,
|
||||
state: SiteState::Active,
|
||||
cover: None,
|
||||
aimed: false,
|
||||
});
|
||||
for i in 0..stock {
|
||||
@@ -1378,17 +1351,17 @@ mod tests {
|
||||
}
|
||||
|
||||
// #106: a wall between the shot and the site catches the missile: it goes off at the contact, the site is untouched
|
||||
// and keeps its launches, the stock is spent once and the result is counted once, before any cover roll.
|
||||
// and keeps its launches, the stock is spent once and the result is counted once, before impact handling.
|
||||
#[test]
|
||||
fn a_counter_strike_breaks_on_a_wall_between_it_and_its_site() {
|
||||
let mut sim = Sim::new(11);
|
||||
// Heavy cover would shoot a missile down half the time: a broken one is never intercepted.
|
||||
sim.strikes.cover_override = Some(2);
|
||||
// A broken flight is counted once and never becomes a fictitious interception.
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 4);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let wall = Wall { at: 140, hole: None };
|
||||
warm(&mut ground, &wall, DVec3::new(0.0, 65.5, 0.0), DVec3::new(0.0, 105.0, 300.0));
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
out.messages.clear();
|
||||
let mut crossed = false;
|
||||
for _ in 0..300 {
|
||||
@@ -1425,14 +1398,14 @@ mod tests {
|
||||
#[test]
|
||||
fn a_counter_strike_passes_an_opening_by_the_blocks_shape() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
// No launches: only the strike can make a recording here.
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 0);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
// A slit between 100 and 110 blocks high, where the missile flies at about 104.
|
||||
let wall = Wall { at: 140, hole: Some((100, 110)) };
|
||||
warm(&mut ground, &wall, DVec3::new(0.0, 65.5, 0.0), DVec3::new(0.0, 105.0, 300.0));
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
let mut crossed = false;
|
||||
for _ in 0..400 {
|
||||
serve(&mut ground, &wall, sim.tick);
|
||||
@@ -1454,10 +1427,10 @@ mod tests {
|
||||
#[test]
|
||||
fn terminal_wall_is_resolved_before_landing() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 2);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
let f = sim.strikes.flights[0];
|
||||
let wall = Wall { at: f.land.y.floor() as i32 - 1, hole: None };
|
||||
for _ in 0..500 {
|
||||
@@ -1476,10 +1449,10 @@ mod tests {
|
||||
#[test]
|
||||
fn delayed_world_answers_do_not_accelerate_the_strike() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 1000.0), 0);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
for _ in 0..20 { sim.step(&mut ground, None, &mut out); }
|
||||
let f = sim.strikes.flights[0];
|
||||
let open = Wall { at: 10_000, hole: None };
|
||||
@@ -1501,11 +1474,11 @@ mod tests {
|
||||
#[test]
|
||||
fn a_counter_strike_on_an_open_way_still_reaches_its_site() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 2);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let open = Wall { at: 10_000, hole: None };
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
for _ in 0..400 {
|
||||
serve(&mut ground, &open, sim.tick);
|
||||
sim.step(&mut ground, None, &mut out);
|
||||
@@ -1524,10 +1497,10 @@ mod tests {
|
||||
#[test]
|
||||
fn a_strike_whose_site_is_taken_off_the_map_still_has_its_result() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 300.0), 0);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
// The world unloads (or the sky is cleared): no site to find on arrival.
|
||||
sim.clear();
|
||||
assert!(sim.sites.get(id).is_none());
|
||||
@@ -1551,11 +1524,11 @@ mod tests {
|
||||
#[test]
|
||||
fn a_late_counter_strike_keeps_the_recording_until_it_lands() {
|
||||
let mut sim = Sim::new(11);
|
||||
sim.strikes.cover_override = Some(0);
|
||||
|
||||
let id = fixed_site(&mut sim, DVec3::new(0.0, 64.0, 1000.0), 0);
|
||||
let (mut ground, mut out) = (Ground::default(), Out::default());
|
||||
let open = Wall { at: 10_000, hole: None };
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut out).unwrap();
|
||||
let drone = sim.make_drone(DroneKind::Shahed, DVec3::new(0.0, 65.0, 0.0), -DVec3::Z, DVec3::new(0.0, 64.0, 1000.0));
|
||||
sim.drones.push(drone);
|
||||
serve(&mut ground, &open, sim.tick);
|
||||
|
||||
@@ -338,9 +338,9 @@ mod tests {
|
||||
site: id, class: SiteClass::Ramp, obs: Vec::new(),
|
||||
est: Some(Estimate { pos: DVec2::new(0.0, 300.0), radius: 5.0, fix: true,
|
||||
bearing: 0.0, bearing_err: 0.01, origin: DVec2::ZERO }),
|
||||
launches: 1, last: 0, state: SiteState::Active, cover: None, aimed: false,
|
||||
launches: 1, last: 0, state: SiteState::Active, aimed: false,
|
||||
});
|
||||
sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut crate::sim::Out::default()).unwrap();
|
||||
sim.order_test_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut crate::sim::Out::default()).unwrap();
|
||||
let mut smoke = Smoke::default();
|
||||
sim.tick = 1;
|
||||
smoke.update(&sim);
|
||||
|
||||
Reference in New Issue
Block a user