Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb35975642 | |||
| f2838a8f08 | |||
| c42d97af08 | |||
| 6efec3e546 |
@@ -24,12 +24,8 @@ jobs:
|
||||
run: |
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
|
||||
- uses: actions/cache@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ~/.cargo/bin/cargo-horizon-mod
|
||||
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
|
||||
# Без actions/cache: кэш-сервер раннера из контейнеров задач недоступен — восстановление
|
||||
# падало по таймауту, а сохранение вешало «Complete job» на пару минут. Инструмент собирается заново.
|
||||
- name: Форк Horizon
|
||||
run: nice -n 19 python3 tools/airdef.py fork
|
||||
- name: Мод
|
||||
|
||||
@@ -97,6 +97,14 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
|
||||
25 с на цель в темноте), игрок — **осветительным пистолетом** или `/airdef flare`. Ракета висит ~30 с и
|
||||
освещает всё вокруг на ~60–90 м.
|
||||
|
||||
**Графика: три пресета** (меню N → «Огонь, ремонт, эффекты…» → «Эффекты», или `/airdef shader off|on|hd`):
|
||||
выкл — ванильная картинка; обычные — шейдерные огненные шары, ударная волна и подсветка; **HD** — свой рендер
|
||||
освещения в форке: мир дополнительно рисуется в G-буфер (цвет без света и нормали), и все источники света мода
|
||||
освещают его попиксельно в HDR — факелы ракет и двигатели, раскалённые боевые блоки, осветительные ракеты, вспышки
|
||||
выстрелов, трассеры, взрывы (вспышка и догорающий шар), горящие остовы, пятна прожекторов, — затем bloom и мягкий
|
||||
тонмаппинг (ванильный диапазон не трогается, сжимается только то, что ярче). Источники передаются движку новым
|
||||
API мода `render.set-lights` (до 512, плавно интерполируются), режим — `render.set-lighting`.
|
||||
|
||||
**Сетка J** после сбития ещё 3,5 с держит камеру на сбитой цели, и комплекс в это время не пропадает из сетки.
|
||||
|
||||
**Повреждения.** Взрыв рядом с комплексом повреждает его: он хуже поворачивается, хуже видит и попадает,
|
||||
|
||||
@@ -115,7 +115,8 @@ fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
|
||||
// the pixel sees, much stronger at night.
|
||||
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
|
||||
let strength = mix(0.35, 2.2, night);
|
||||
if wall < 1.0e5 {
|
||||
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
|
||||
if wall < 1.0e5 && frame.params[0].w < 0.5 {
|
||||
let p = dir * wall;
|
||||
var light = vec3<f32>(0.0);
|
||||
for (var i = 0; i < blasts; i++) {
|
||||
|
||||
+136
-21
@@ -7,7 +7,7 @@ use horizon_mod_sdk::{
|
||||
Mod, Registration, chat, command, game,
|
||||
hud::{self, Corner},
|
||||
input::{self, Event},
|
||||
render::{self, ModelInstance, ParticleSpawn},
|
||||
render::{self, Lighting, ModelInstance, ParticleSpawn, PointLight},
|
||||
storage,
|
||||
ui::{self, ButtonWidget, ItemWidget, ListWidget, Screen, Widget},
|
||||
};
|
||||
@@ -72,6 +72,8 @@ struct AirDef {
|
||||
far: bool,
|
||||
/// The unit the move remote picked (its id), waiting for where to go.
|
||||
moving: Option<u32>,
|
||||
/// The HD lighting preset (the engine's lights, bloom, tone mapping).
|
||||
hd: bool,
|
||||
/// The world whose state is loaded (`game.world-id`), once one is.
|
||||
world: Option<Option<String>>,
|
||||
/// The systems' and objects' health as last saved, to save again when it changes.
|
||||
@@ -276,11 +278,80 @@ impl AirDef {
|
||||
self.emit(out);
|
||||
}
|
||||
|
||||
/// The HD lighting's lights: flames, glowing warheads, flares, muzzle flashes, tracers,
|
||||
/// blasts (a flash, then the fireball's glow dying down), burning wrecks and the
|
||||
/// searchlights' spots.
|
||||
fn lights(&self, out: &Out) -> Vec<PointLight> {
|
||||
let mut lights: Vec<PointLight> = Vec::new();
|
||||
let mut add = |id: u32, at: DVec3, rgb: (f32, f32, f32), intensity: f32, radius: f32| {
|
||||
if lights.len() < 500 && intensity > 0.05 {
|
||||
lights.push(PointLight { id, x: at.x, y: at.y, z: at.z, color: rgb, intensity, radius: radius.clamp(1.0, 256.0) });
|
||||
}
|
||||
};
|
||||
let tick = self.sim.tick;
|
||||
let flicker = |id: u32| 0.85 + 0.15 * (((tick as u32).wrapping_mul(2654435761) ^ id.wrapping_mul(97)) % 100) as f32 / 100.0;
|
||||
for m in out.models.iter().filter(|m| m.glow) {
|
||||
let s = m.scale.max(0.1);
|
||||
match m.name {
|
||||
"airdef:flame" => add(m.id, m.at, (1.0, 0.55, 0.2), 45.0 * s * flicker(m.id), 32.0 * s.sqrt()),
|
||||
"airdef:geran3_flame" => add(m.id, m.at, (1.0, 0.6, 0.25), 25.0 * s * flicker(m.id), 22.0),
|
||||
"airdef:reentry" => add(m.id, m.at, (1.0, 0.6, 0.32), 60.0 * s, 40.0),
|
||||
"airdef:flare" => add(m.id, m.at, (1.0, 0.93, 0.78), 420.0 * s, 110.0),
|
||||
"airdef:flash" => add(m.id, m.at, (1.0, 0.8, 0.45), 30.0, 14.0),
|
||||
"airdef:tracer" => add(m.id, m.at, (1.0, 0.25, 0.1), 2.5, 6.0),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for b in &self.sim.blasts {
|
||||
let age = tick.saturating_sub(b.start) as f32;
|
||||
let size = b.size;
|
||||
let glow = (-age / (b.life as f32 * 0.15).max(1.0)).exp();
|
||||
let flash = (-age * 0.35).exp() * 4.0;
|
||||
let id = 0x6100_0000 ^ (b.start as u32).wrapping_mul(31) ^ (b.at.x as i32 as u32).wrapping_mul(7);
|
||||
add(id, b.at + DVec3::Y * size as f64 * 0.5, (1.0, 0.58, 0.25), size * size * 18.0 * (glow + flash), size * 14.0);
|
||||
}
|
||||
for s in &self.sim.sams {
|
||||
if !s.alive() && s.burning() {
|
||||
add(s.id + 15, s.pos + DVec3::Y * 1.5, (1.0, 0.5, 0.18), 80.0 * flicker(s.id), 30.0);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
lights
|
||||
}
|
||||
|
||||
/// The effects preset: none, the post pass, or the post pass under the HD lighting.
|
||||
fn set_effects(&mut self, shader: bool, hd: bool) {
|
||||
self.sim.shader = shader;
|
||||
if shader {
|
||||
render::set_post(include_str!("../shaders/effects.wgsl"));
|
||||
} else {
|
||||
render::clear_post();
|
||||
}
|
||||
self.hd = shader && hd;
|
||||
self.apply_hd();
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Switches the HD lighting.
|
||||
fn apply_hd(&self) {
|
||||
render::set_lighting(Lighting { hd: self.hd, exposure: 1.0, bloom: 0.55 });
|
||||
if !self.hd {
|
||||
render::set_lights(&[]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hands the simulation's output to the game.
|
||||
/// The post pass's parameters: blasts and glowing parts (shaders/effects.wgsl).
|
||||
fn shader_params(&self, out: &Out) -> Vec<f32> {
|
||||
let mut p = vec![0.0f32; 4];
|
||||
p[0] = self.sim.tick as f32;
|
||||
p[3] = if self.hd { 1.0 } else { 0.0 };
|
||||
let blasts: Vec<&sim::Blast> = self.sim.blasts.iter().rev().take(48).collect();
|
||||
p[1] = blasts.len() as f32;
|
||||
for b in &blasts {
|
||||
@@ -300,7 +371,6 @@ impl AirDef {
|
||||
"airdef:tracer" => (1.0, 0.12, 0.05, 0.9),
|
||||
"airdef:reentry" => (1.0, 0.6, 0.3, 1.4),
|
||||
"airdef:flare" => (1.0, 0.94, 0.78, 1.2),
|
||||
"airdef:beam" => (1.0, 0.97, 0.88, 0.15),
|
||||
_ => continue,
|
||||
};
|
||||
// The shader lights about six times the radius round a light and draws a glow of
|
||||
@@ -318,6 +388,9 @@ impl AirDef {
|
||||
if self.sim.shader {
|
||||
render::set_params(&self.shader_params(&out));
|
||||
}
|
||||
if self.hd {
|
||||
render::set_lights(&self.lights(&out));
|
||||
}
|
||||
let models: Vec<ModelInstance> = out
|
||||
.models
|
||||
.into_iter()
|
||||
@@ -415,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);
|
||||
@@ -423,6 +513,7 @@ impl AirDef {
|
||||
}
|
||||
text += &format!("damage {}\n", self.sim.damage);
|
||||
text += &format!("far {}\n", self.far);
|
||||
text += &format!("effects {} {}\n", self.sim.shader, self.hd);
|
||||
for (kind, roles) in &self.sim.roles {
|
||||
text += &format!("roles {} {}\n", kind.key(), roles);
|
||||
}
|
||||
@@ -458,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") => {
|
||||
@@ -469,6 +575,11 @@ impl AirDef {
|
||||
}
|
||||
Some(&"damage") => self.sim.damage = w.get(1) != Some(&"false"),
|
||||
Some(&"far") => self.far = w.get(1) != Some(&"false"),
|
||||
Some(&"effects") => {
|
||||
let shader = w.get(1) != Some(&"false");
|
||||
let hd = w.get(2) == Some(&"true");
|
||||
self.set_effects(shader, hd);
|
||||
}
|
||||
Some(&"roles") => {
|
||||
if let (Some(kind), Some(r)) = (w.get(1).and_then(|k| SamKind::from_key(k)), w.get(2).and_then(|v| v.parse().ok())) {
|
||||
self.sim.roles.insert(kind, r);
|
||||
@@ -699,7 +810,8 @@ impl AirDef {
|
||||
}));
|
||||
let broken = self.sim.sams.iter().filter(|s| s.health < 1.0).count();
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_REPAIR, label: format!("Ремонт всех (повреждено: {broken})") }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {}", onoff(self.sim.shader)) }));
|
||||
let fx = if !self.sim.shader { "выкл" } else if self.hd { "HD (свет, bloom)" } else { "обычные" };
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_SHADER, label: format!("Эффекты: {fx}") }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_DAMAGE, label: format!("Разрушения: {}", onoff(self.sim.damage)) }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_FAR, label: format!("Дальний ландшафт: {}", onoff(self.far)) }));
|
||||
widgets.push(Widget::Button(ButtonWidget { id: BTN_BACK, label: "← Назад".into() }));
|
||||
@@ -764,7 +876,7 @@ impl AirDef {
|
||||
}
|
||||
}
|
||||
|
||||
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
|
||||
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | ew on|off | flare | move <номер> | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off|hd | hold on|off | roles [комплекс классы] | stats";
|
||||
|
||||
impl Mod for AirDef {
|
||||
fn init() -> Result<(Self, Registration), String> {
|
||||
@@ -782,6 +894,7 @@ impl Mod for AirDef {
|
||||
replay: None,
|
||||
far: true,
|
||||
moving: None,
|
||||
hd: false,
|
||||
world: None,
|
||||
health_mark: 0,
|
||||
menu: None,
|
||||
@@ -913,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 {
|
||||
@@ -1032,12 +1151,13 @@ impl Mod for AirDef {
|
||||
return;
|
||||
}
|
||||
(Page::More, BTN_SHADER) => {
|
||||
self.sim.shader = !self.sim.shader;
|
||||
if self.sim.shader {
|
||||
render::set_post(include_str!("../shaders/effects.wgsl"));
|
||||
} else {
|
||||
render::clear_post();
|
||||
}
|
||||
// Off → effects → HD → off.
|
||||
let (shader, hd) = match (self.sim.shader, self.hd) {
|
||||
(false, _) => (true, false),
|
||||
(true, false) => (true, true),
|
||||
(true, true) => (false, false),
|
||||
};
|
||||
self.set_effects(shader, hd);
|
||||
}
|
||||
(Page::More, BTN_FAR) => {
|
||||
self.far = !self.far;
|
||||
@@ -1224,14 +1344,9 @@ impl Mod for AirDef {
|
||||
self.sim.hold = *v == "on";
|
||||
Some(if self.sim.hold { "ПВО: огонь запрещён".into() } else { "ПВО: огонь разрешён".into() })
|
||||
}
|
||||
["shader", v @ ("on" | "off")] => {
|
||||
self.sim.shader = *v == "on";
|
||||
if self.sim.shader {
|
||||
render::set_post(include_str!("../shaders/effects.wgsl"));
|
||||
} else {
|
||||
render::clear_post();
|
||||
}
|
||||
Some(format!("Шейдерные эффекты: {}", if self.sim.shader { "вкл" } else { "выкл" }))
|
||||
["shader", v @ ("on" | "off" | "hd")] => {
|
||||
self.set_effects(*v != "off", *v == "hd");
|
||||
Some(format!("Эффекты: {}", match *v { "off" => "выкл", "hd" => "HD", _ => "обычные" }))
|
||||
}
|
||||
["stats"] => Some(format!(
|
||||
"В воздухе: {}, ракет: {}, ПВО: {}, сбито: {}, поражений: {}",
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:37:45 +0300
|
||||
Subject: [PATCH 01/10] Mod API (AirDef fork): world models, client particles,
|
||||
Subject: [PATCH 01/12] Mod API (AirDef fork): world models, client particles,
|
||||
sounds, ground heights
|
||||
|
||||
render.set-models draws block models with an interpolated pose per id;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:39:45 +0300
|
||||
Subject: [PATCH 02/10] cargo-horizon-mod: report particles and sounds
|
||||
Subject: [PATCH 02/12] cargo-horizon-mod: report particles and sounds
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 13:33:10 +0300
|
||||
Subject: [PATCH 03/10] Mod API (AirDef fork): full-screen post pass for mods
|
||||
Subject: [PATCH 03/12] Mod API (AirDef fork): full-screen post pass for mods
|
||||
|
||||
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
|
||||
time, 256 vec4 of params, the world's colour and depth) and runs it after
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:09:30 +0300
|
||||
Subject: [PATCH 04/10] Mod models: draw far models as scaled impostors past
|
||||
Subject: [PATCH 04/12] Mod models: draw far models as scaled impostors past
|
||||
the far plane, cull at 4096
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:31:00 +0300
|
||||
Subject: [PATCH 05/10] Mod cameras: render.set-views draws the world from
|
||||
Subject: [PATCH 05/12] Mod cameras: render.set-views draws the world from
|
||||
other points into pictures that HUD view shapes show (API level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:08:21 +0300
|
||||
Subject: [PATCH 06/10] Mods: more fuel per tick and per event (AirDef plans
|
||||
Subject: [PATCH 06/12] Mods: more fuel per tick and per event (AirDef plans
|
||||
raids)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:25:29 +0300
|
||||
Subject: [PATCH 07/10] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
Subject: [PATCH 07/12] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:15:11 +0300
|
||||
Subject: [PATCH 08/10] Mod cameras: distant terrain tiles, fog measured from
|
||||
Subject: [PATCH 08/12] Mod cameras: distant terrain tiles, fog measured from
|
||||
the camera, no impostors while one is shown
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:25:45 +0300
|
||||
Subject: [PATCH 09/10] Mods: game.world-id, so mods keep state per world (API
|
||||
Subject: [PATCH 09/12] Mods: game.world-id, so mods keep state per world (API
|
||||
level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 12:01:08 +0300
|
||||
Subject: [PATCH 10/10] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
Subject: [PATCH 10/12] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
packing-only tool skips wasmtime
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+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