Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb35975642 |
+42
-3
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user