Compare commits

...

1 Commits

7 changed files with 121 additions and 21 deletions
+42 -3
View File
@@ -317,6 +317,7 @@ impl AirDef {
// A searchlight's spot where its beam meets the target.
if s.kind == SamKind::Light
&& let Some(t) = s.target.and_then(|t| self.sim.drones.iter().find(|d| d.id == t))
&& self.sim.lit.contains(&t.id)
{
add(s.id + 14, t.pos, (0.95, 0.97, 1.0), 220.0, 30.0);
}
@@ -487,7 +488,24 @@ impl AirDef {
fn save(&mut self) {
let mut text = String::new();
for s in &self.sim.sams {
text += &format!("sam {} {} {} {} {} {} {} {}\n", s.kind.key(), s.pos.x, s.pos.y, s.pos.z, s.heading, s.roles, s.health, s.cargo);
// Then its ammunition (missiles, bursts, out and waiting for a truck, reload ticks
// left) and where it was driving, if anywhere.
let dest = s.dest.map_or("- - -".to_string(), |d| format!("{} {} {}", d.x, d.y, d.z));
text += &format!(
"sam {} {} {} {} {} {} {} {} {} {} {} {} {dest}\n",
s.kind.key(),
s.pos.x,
s.pos.y,
s.pos.z,
s.heading,
s.roles,
s.health,
s.cargo,
s.missiles,
s.bursts,
u8::from(s.empty),
s.reloading,
);
}
for (i, t) in self.sim.targets.iter().enumerate() {
let h = self.sim.target_health.get(i).copied().unwrap_or(100.0);
@@ -531,7 +549,22 @@ impl AirDef {
if let Some(c) = num(8) {
sam.cargo = c as u32;
}
// Older saves have no ammunition: full, as before.
if let (Some(m), Some(b)) = (num(9), num(10)) {
sam.missiles = (m as u32).min(sam.missiles);
sam.bursts = (b as u32).min(sam.bursts);
sam.empty = num(11) == Some(1.0);
sam.reloading = num(12).map_or(0, |r| r as u32);
}
let dest = match (num(13), num(14), num(15)) {
(Some(x), Some(y), Some(z)) => Some(DVec3::new(x, y, z)),
_ => None,
};
self.sim.sams.push(sam);
if let Some(to) = dest {
let i = self.sim.sams.len() - 1;
self.sim.order_move(i, to);
}
}
}
Some(&"target") => {
@@ -993,12 +1026,18 @@ impl Mod for AirDef {
}
}
}
// Damage is saved too: a destroyed system stays destroyed.
// Damage is saved too: a destroyed system stays destroyed. So are where the units stand
// (or drive to: not every step of the way), their ammunition and the depots' stock.
let mark = self
.sim
.sams
.iter()
.map(|s| (s.health * 1000.0) as u64)
.map(|s| {
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;
((s.health * 1000.0) as u64).wrapping_mul(1_000_003) ^ place ^ ammo.rotate_left(7)
})
.chain(self.sim.target_health.iter().map(|h| (h * 10.0) as u64))
.fold(self.sim.sams.len() as u64, |a, h| a.wrapping_mul(31).wrapping_add(h));
if mark != self.health_mark {
+3 -3
View File
@@ -145,7 +145,7 @@ impl Sim {
out
}
/// During a raid: drones not yet launched go for what just gave itself away (a jamming station
/// During a smart raid: its drones not yet launched go for what just gave itself away (a jamming station
/// first, then systems that fired), and drones whose system target is gone pick another.
pub(super) fn adapt(&mut self) {
if self.tick % 40 != 0 {
@@ -160,7 +160,7 @@ impl Sim {
}
let pick = |i: usize, list: &[(u32, DVec3)]| list[i % list.len()].1;
let mut n = 0usize;
for (_, d) in self.queue.iter_mut() {
for (_, d) in self.queue.iter_mut().filter(|(_, d)| d.smart) {
let hunter = matches!(d.kind, DroneKind::Lancet | DroneKind::Kh22 | DroneKind::Zircon);
if !hunter && !(d.kind == DroneKind::Shahed && n % 4 == 0) {
n += 1;
@@ -176,7 +176,7 @@ impl Sim {
n += 1;
}
// In flight: a target system destroyed — the next known one, or keep going.
for d in self.drones.iter_mut().filter(|d| d.alive()) {
for d in self.drones.iter_mut().filter(|d| d.alive() && d.smart) {
let aimed_at_wreck = self.sams.iter().any(|s| !s.alive() && s.pos.distance(d.target) < 6.0);
if aimed_at_wreck {
let next = known.iter().min_by(|a, b| a.1.distance(d.pos).total_cmp(&b.1.distance(d.pos)));
+6 -3
View File
@@ -103,12 +103,15 @@ impl Sim {
sam.yaw = slew(sam.yaw, want_yaw, 0.12);
sam.pitch = slew(sam.pitch, want_pitch.clamp(0.05, 1.5), 0.08);
sam.target = target.map(|t| t.0);
if let Some((t, _)) = target {
self.lit.push(t);
}
// The beam: segments widening with distance, out to the target.
let (head, rot) = self.sams[i].mount();
let dir = rot * DVec3::NEG_Z;
// The target is lit once the beam is on it, not while it still swings round.
if let Some((t, at)) = target
&& dir.angle_between(at - head) < 0.08
{
self.lit.push(t);
}
let mut along = 1.0;
for k in 0..10u32 {
if along >= reach {
+4 -2
View File
@@ -12,9 +12,11 @@ const HANDOVER: f64 = 10.0;
const TRUCK_LOAD: u32 = 2;
impl Sim {
/// Whether ammunition is counted: a depot stands (without one, magazines refill as before).
/// Whether ammunition is counted: a depot was set up (without one, magazines refill as
/// before). A destroyed depot still counts: losing the last one leaves the systems short,
/// not supplied from nowhere again.
pub fn logistics(&self) -> bool {
self.sams.iter().any(|s| s.kind == SamKind::Depot && s.alive())
self.sams.iter().any(|s| s.kind == SamKind::Depot)
}
/// Reloads from depots near the systems, and the trucks' rounds.
+14 -6
View File
@@ -657,6 +657,8 @@ pub struct Drone {
pub jammed: bool,
/// The approach sector it came from (the enemy keeps score).
sector: u8,
/// Sent by the smart enemy: it may be turned on systems that give themselves away.
smart: bool,
}
impl Drone {
@@ -1122,7 +1124,7 @@ pub struct Sim {
pub night: bool,
/// Illumination flares in the sky, and the drones lit this tick.
pub flares: Vec<lights::Flare>,
lit: Vec<u32>,
pub lit: Vec<u32>,
}
/// Ids: objects get multiples of 16, their parts the low bits.
@@ -1230,6 +1232,7 @@ impl Sim {
low: false,
jammed: false,
sector: 0,
smart: false,
});
self.launched += 1;
}
@@ -1519,6 +1522,7 @@ impl Sim {
low: false,
jammed: false,
sector: 0,
smart: false,
});
}
for d in &self.drones {
@@ -1715,7 +1719,10 @@ impl Sim {
if !self.sams[i].kind.armed() {
let sam = &mut self.sams[i];
sam.state = SamState::Search;
sam.target = None;
// A searchlight's target is its beam's (step_lights): kept for its spot of light.
if sam.kind != SamKind::Light {
sam.target = None;
}
sam.gun_target = None;
continue;
}
@@ -1879,7 +1886,11 @@ impl Sim {
let n = load.magazine - sam.missiles;
let (muzzle, dir) = sam.muzzle(n);
launches.push((load.kind, muzzle, dir, mt, sam.id));
// Its missile is on it: the next launch looks for another target.
// Its missile is on it from now, so the systems after this one this tick see
// the target taken; this one's next launch looks for another.
if let Some(d) = self.drones.iter_mut().find(|d| d.id == mt) {
d.engaged += 1;
}
sam.target = None;
sam.launched = 12;
sam.missiles -= 1;
@@ -1900,9 +1911,6 @@ impl Sim {
let id = self.id();
let v0 = if spec.cold > 0 { 0.7 } else { 0.8 };
self.missiles.push(Missile { id, kind, pos: muzzle, vel: dir * v0, target, sam, age: 0 });
if let Some(d) = self.drones.iter_mut().find(|d| d.id == target) {
d.engaged += 1;
}
if spec.cold > 0 {
// Thrown out by gas: a puff, a thump; the motor lights later.
out.particles("minecraft:cloud", muzzle, 0.6, dir * 0.05, 0.05, 30);
+26 -1
View File
@@ -298,7 +298,8 @@ impl Sim {
// Timing: smart raids arrive at once; others leave at once (a moment apart).
let now = self.tick;
let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max);
for (d, time, early, gap) in plan.done {
for (mut d, time, early, gap) in plan.done {
d.smart = plan.smart;
let wait = if plan.smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 };
self.queue.push((now + wait.clamp(0.0, 2400.0) as u64, d));
}
@@ -348,6 +349,7 @@ impl Sim {
low: false,
jammed: false,
sector: 0,
smart: false,
}
}
@@ -537,6 +539,29 @@ mod tests {
eprintln!("4000 ticks in {:?}, kills {} hits {}", t.elapsed(), sim.kills, sim.hits);
assert!(sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()), "the raid never ended");
}
#[test]
fn systems_ready_together_send_one_missile_at_a_drone() {
let mut sim = Sim::new(3);
// Vertical launchers of one kind side by side: the drone comes into their reach in the
// same tick, and they fire without turning.
for i in 0..6 {
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Tor, DVec3::new(2.0 * i as f64, 70.0, 0.0), 0.0));
}
let mut ground = Ground::default();
let mut out = Out::default();
for _ in 0..30 {
sim.step(&mut ground, None, &mut out);
}
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, -400.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
let id = sim.drones[0].id;
for _ in 0..1200 {
sim.step(&mut ground, None, &mut out);
let on_it = sim.missiles.iter().filter(|m| m.target == id).count();
assert!(on_it <= 1, "{on_it} missiles on one Shahed");
}
assert!(sim.sams.iter().any(|s| s.fired > 0), "nothing was fired");
}
}
#[cfg(test)]
+26 -3
View File
@@ -12,6 +12,7 @@
Minecraft 26.3), AIRDEF_GAME (готовый horizon-client вместо собранного).
"""
import glob
import hashlib
import os
import platform
import shutil
@@ -51,21 +52,43 @@ def cargo() -> str:
return found
def fork_stamp(commit: str, patches) -> str:
"""Что должно стоять в horizon/: коммит апстрима и содержимое патчей."""
h = hashlib.sha256(commit.encode())
for p in patches:
h.update(p.name.encode())
h.update(p.read_bytes())
return h.hexdigest()
def fork():
"""horizon/ = апстрим на коммите HORIZON_COMMIT + патчи horizon-patches/."""
"""horizon/ = апстрим на коммите HORIZON_COMMIT + патчи horizon-patches/.
Ветка airdef пересобирается, когда коммит или патчи изменились (после обновления репозитория);
прежняя остаётся в ветке airdef-prev."""
commit = (ROOT / "HORIZON_COMMIT").read_text().strip()
patches = sorted((ROOT / "horizon-patches").glob("*.patch"))
stamp = fork_stamp(commit, patches)
stamp_file = FORK / ".git" / "airdef-stamp"
if not (FORK / ".git").exists():
run(["git", "clone", "--filter=blob:none", UPSTREAM, FORK])
branch = subprocess.run(["git", "-C", FORK, "branch", "--show-current"], capture_output=True, text=True).stdout.strip()
if branch == "airdef":
print("horizon/: ветка airdef уже есть")
if branch == "airdef" and stamp_file.exists() and stamp_file.read_text().strip() == stamp:
print("horizon/: ветка airdef на нужных коммите и патчах")
return
dirty = subprocess.run(["git", "-C", FORK, "status", "--porcelain", "--untracked-files=no"],
capture_output=True, text=True).stdout.strip()
if dirty:
sys.exit("horizon/: есть незакоммиченные изменения, а движок надо обновить. Закоммить или убери их и запусти снова.")
if branch == "airdef":
print("horizon/: коммит или патчи изменились, ветка airdef пересобирается (прежняя: airdef-prev)")
run(["git", "-C", FORK, "branch", "-f", "airdef-prev", "airdef"])
run(["git", "-C", FORK, "fetch", "--quiet", "origin", commit])
run(["git", "-C", FORK, "checkout", "-B", "airdef", commit])
if patches:
run(["git", "-C", FORK, "-c", "user.name=airdef", "-c", "user.email=airdef@localhost",
"am", "--3way", *patches])
stamp_file.write_text(stamp + "\n")
def game():