feat: generate simulation numeric profiles from catalog/specs.json
catalog/specs.json is now the single copy of the numeric rows for
DroneSpec, MissileSpec and SamSpec: speed, guidance, warheads, models'
parts, RCS, seekers, launch and motor sounds, mount drives, gravity,
the Oreshnik split height and logistics limits, plus the named part
sets. tools/gen_catalog.py generates airdef-mod/src/sim/specs_generated.rs
(included by mod.rs, so the mod and airdef-core compile the same rows)
and unit_specs_generated.rs (included by units.rs). The hand-written
spec matches are gone; DroneKind::spec, MissileKind::spec and
SamKind::spec read the generated constants.
load_specs validates the schema, sound references, kind coverage against
content.json, named-part references and unused rows. gen_catalog.py
--check already runs in the CI generated-assets step, so a JSON/Rust
drift fails the build.
Every value was compared with the literals at 632061d field by field.
The only mismatch was the P1-SUN motor voice pitch in JSON (1.6 instead
of the actual 1.7, the flame == 0 branch in voices.rs); the JSON was
corrected and behaviour is unchanged. Save keys, raid order, RNG and
gameplay traces are untouched.
Verified: core release 194 passed + 8 scenarios (7 ignored), adapter
64 unit + 1 boundary + 5 scenarios, WASM contract 27 steps, catalog and
model generators fresh, asset/sound/model-bound/report tests pass.
This commit is contained in:
@@ -172,4 +172,32 @@ mod tests {
|
||||
assert_eq!(kinds, SamKind::ALL.into_iter().collect());
|
||||
assert_eq!(&RAID_KINDS[..11], &[Gerbera, Shahed, Geran3, Lancet, Kh101, Kalibr, Kh22, Zircon, Iskander, Kinzhal, Oreshnik]);
|
||||
}
|
||||
|
||||
/// The catalog is the only copy of the numbers: a kind without a row, or a row under
|
||||
/// another kind, would be a silent balance change.
|
||||
#[test]
|
||||
fn generated_specs_cover_every_kind_and_keep_their_keys() {
|
||||
let mut drones = HashSet::new();
|
||||
let mut drone_keys = HashSet::new();
|
||||
for (kind, key) in super::super::DRONE_SPEC_KEYS {
|
||||
assert_eq!(kind.spec().key, key, "profile/key mismatch for {kind:?}");
|
||||
assert!(drones.insert(kind), "duplicate drone profile {kind:?}");
|
||||
assert!(drone_keys.insert(key), "duplicate drone save key {key}");
|
||||
assert!(kind.spec().rcs > 0.0 && kind.spec().launch_sound.starts_with("airdef:"));
|
||||
}
|
||||
assert_eq!(drones, DroneKind::ALL.into_iter().collect());
|
||||
let mut missiles = HashSet::new();
|
||||
for (kind, model) in super::super::MISSILE_SPEC_MODELS {
|
||||
assert_eq!(kind.spec().model, model, "profile/model mismatch for {kind:?}");
|
||||
assert!(missiles.insert(kind), "duplicate missile profile {kind:?}");
|
||||
}
|
||||
let mut units = HashSet::new();
|
||||
let mut unit_keys = HashSet::new();
|
||||
for (kind, key) in super::super::units::UNIT_SPEC_KEYS {
|
||||
assert_eq!(kind.spec().key, key, "profile/key mismatch for {kind:?}");
|
||||
assert!(units.insert(kind), "duplicate unit profile {kind:?}");
|
||||
assert!(unit_keys.insert(key), "duplicate unit save key {key}");
|
||||
}
|
||||
assert_eq!(units, SamKind::ALL.into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
//! reloads to the systems that wait, nearest first, and back.
|
||||
use super::*;
|
||||
|
||||
/// How near a depot a system reloads straight from it.
|
||||
const DEPOT_REACH: f64 = 60.0;
|
||||
/// How near a truck must come to hand a reload over, or to load up at a depot.
|
||||
const HANDOVER: f64 = 10.0;
|
||||
/// What a truck carries.
|
||||
const TRUCK_LOAD: u32 = 2;
|
||||
const STOCK_LIMIT: u32 = 1_000_000;
|
||||
|
||||
impl Sim {
|
||||
/// 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,
|
||||
|
||||
+17
-418
@@ -592,9 +592,6 @@ pub fn roles_text(mask: u8) -> String {
|
||||
Class::ALL.iter().filter(|c| mask & c.bit() != 0).map(|c| c.letter()).collect::<Vec<_>>().join("")
|
||||
}
|
||||
|
||||
/// Ballistic flight's gravity, blocks a tick².
|
||||
const GRAVITY: f64 = 0.05;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
pub enum DroneKind {
|
||||
Shahed,
|
||||
@@ -645,11 +642,15 @@ pub struct DroneSpec {
|
||||
pub decoy: bool,
|
||||
/// How hard it is to hit (1 normal).
|
||||
stealth: f64,
|
||||
/// Its radar cross-section (a Shahed's 0.3).
|
||||
rcs: f64,
|
||||
/// Its sound: event, pitch, every n ticks.
|
||||
sound: (&'static str, f32, u32),
|
||||
pub class: Class,
|
||||
/// Ballistic: launched this far away on a raid, flying an arc.
|
||||
pub ballistic: Option<f64>,
|
||||
/// The sound of its launch from the ground.
|
||||
launch_sound: &'static str,
|
||||
}
|
||||
|
||||
impl DroneSpec {
|
||||
@@ -680,280 +681,17 @@ impl DroneKind {
|
||||
];
|
||||
/// The sound of a launch from the ground: a light catapult shot, a rocket booster, a heavy ballistic launch.
|
||||
pub fn launch_sound(self) -> &'static str {
|
||||
match self {
|
||||
DroneKind::Shahed | DroneKind::Geran3 | DroneKind::Lancet | DroneKind::Gerbera => sound::LAUNCH_S400_EJECT,
|
||||
DroneKind::Kh101 | DroneKind::Kalibr | DroneKind::Kh22 => sound::LAUNCH_PATRIOT,
|
||||
DroneKind::Zircon => sound::LAUNCH_S400,
|
||||
DroneKind::Iskander | DroneKind::Kinzhal | DroneKind::Oreshnik | DroneKind::OreshnikBlock | DroneKind::OreshnikSub => {
|
||||
sound::LAUNCH_S500
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn spec(self) -> &'static DroneSpec {
|
||||
match self {
|
||||
DroneKind::Shahed => &DroneSpec {
|
||||
name: "Шахед-136",
|
||||
key: "shahed",
|
||||
model: "airdef:shahed",
|
||||
speed: 0.6,
|
||||
turn: 0.035,
|
||||
cruise: 45.0,
|
||||
clearance: 18.0,
|
||||
dive: 32.0,
|
||||
warhead: 3,
|
||||
scale: 1.3,
|
||||
engine: Engine::Prop(1.34, 1.0),
|
||||
decoy: false,
|
||||
stealth: 1.0,
|
||||
sound: (sound::DRONE_SHAHED, 1.0, 48),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Geran3 => &DroneSpec {
|
||||
name: "Герань-3",
|
||||
key: "geran3",
|
||||
model: "airdef:geran3",
|
||||
speed: 1.6,
|
||||
turn: 0.03,
|
||||
cruise: 60.0,
|
||||
clearance: 18.0,
|
||||
dive: 50.0,
|
||||
warhead: 2,
|
||||
scale: 1.15,
|
||||
engine: Engine::Jet(0.34, 1.55, 1.0),
|
||||
decoy: false,
|
||||
stealth: 1.1,
|
||||
sound: (sound::DRONE_GERAN3, 1.0, 40),
|
||||
class: Class::Jet,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Lancet => &DroneSpec {
|
||||
name: "Ланцет-3",
|
||||
key: "lancet",
|
||||
model: "airdef:lancet",
|
||||
speed: 1.1,
|
||||
turn: 0.07,
|
||||
cruise: 25.0,
|
||||
clearance: 10.0,
|
||||
dive: 38.0,
|
||||
warhead: 1,
|
||||
scale: 0.9,
|
||||
engine: Engine::Prop(0.93, 0.55),
|
||||
decoy: false,
|
||||
stealth: 1.4,
|
||||
sound: (sound::DRONE_LANCET, 1.0, 32),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Gerbera => &DroneSpec {
|
||||
name: "Гербера",
|
||||
key: "gerbera",
|
||||
model: "airdef:gerbera",
|
||||
speed: 0.5,
|
||||
turn: 0.04,
|
||||
cruise: 40.0,
|
||||
clearance: 15.0,
|
||||
dive: 30.0,
|
||||
warhead: 0,
|
||||
scale: 1.1,
|
||||
engine: Engine::Prop(1.03, 0.9),
|
||||
decoy: true,
|
||||
stealth: 0.9,
|
||||
sound: (sound::DRONE_SHAHED, 1.3, 40),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Kh101 => &DroneSpec {
|
||||
name: "Х-101",
|
||||
key: "kh101",
|
||||
model: "airdef:kh101",
|
||||
speed: 2.4,
|
||||
turn: 0.025,
|
||||
cruise: 0.0,
|
||||
clearance: 12.0,
|
||||
dive: 26.0,
|
||||
warhead: 6,
|
||||
scale: 1.6,
|
||||
engine: Engine::Jet(-0.27, 1.4, 0.6),
|
||||
decoy: false,
|
||||
stealth: 1.5,
|
||||
sound: (sound::DRONE_CRUISE, 1.0, 50),
|
||||
class: Class::Cruise,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Kalibr => &DroneSpec {
|
||||
name: "Калибр",
|
||||
key: "kalibr",
|
||||
model: "airdef:kalibr",
|
||||
speed: 2.1,
|
||||
turn: 0.025,
|
||||
cruise: 0.0,
|
||||
clearance: 15.0,
|
||||
dive: 26.0,
|
||||
warhead: 5,
|
||||
scale: 1.5,
|
||||
engine: Engine::Jet(-0.2, 1.5, 0.6),
|
||||
decoy: false,
|
||||
stealth: 1.3,
|
||||
sound: (sound::DRONE_CRUISE, 1.2, 50),
|
||||
class: Class::Cruise,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Kh22 => &DroneSpec {
|
||||
name: "Х-22",
|
||||
key: "kh22",
|
||||
model: "airdef:kh22",
|
||||
speed: 4.0,
|
||||
turn: 0.03,
|
||||
cruise: 140.0,
|
||||
clearance: 40.0,
|
||||
dive: 150.0,
|
||||
warhead: 7,
|
||||
scale: 1.8,
|
||||
engine: Engine::Rocket(1.5, 1.3),
|
||||
decoy: false,
|
||||
stealth: 1.4,
|
||||
sound: (sound::DRONE_ROCKET, 0.8, 46),
|
||||
class: Class::Supersonic,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Zircon => &DroneSpec {
|
||||
name: "Циркон",
|
||||
key: "zircon",
|
||||
model: "airdef:zircon",
|
||||
speed: 5.5,
|
||||
turn: 0.025,
|
||||
cruise: 90.0,
|
||||
clearance: 30.0,
|
||||
dive: 110.0,
|
||||
warhead: 6,
|
||||
scale: 1.4,
|
||||
engine: Engine::Rocket(1.5, 1.1),
|
||||
decoy: false,
|
||||
stealth: 2.0,
|
||||
sound: (sound::DRONE_ROCKET, 1.35, 46),
|
||||
class: Class::Supersonic,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::Iskander => &DroneSpec {
|
||||
name: "Искандер-М",
|
||||
key: "iskander",
|
||||
model: "airdef:iskander",
|
||||
speed: 5.0,
|
||||
turn: 0.04,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 8,
|
||||
scale: 1.8,
|
||||
engine: Engine::Rocket(1.5, 1.6),
|
||||
decoy: false,
|
||||
stealth: 1.6,
|
||||
sound: (sound::DRONE_ROCKET, 0.7, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(1000.0),
|
||||
},
|
||||
DroneKind::Kinzhal => &DroneSpec {
|
||||
name: "Кинжал",
|
||||
key: "kinzhal",
|
||||
model: "airdef:kinzhal",
|
||||
speed: 7.5,
|
||||
turn: 0.03,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 8,
|
||||
scale: 1.7,
|
||||
engine: Engine::Rocket(1.5, 1.5),
|
||||
decoy: false,
|
||||
stealth: 2.2,
|
||||
sound: (sound::DRONE_ROCKET, 0.6, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(1500.0),
|
||||
},
|
||||
DroneKind::Oreshnik => &DroneSpec {
|
||||
name: "Орешник",
|
||||
key: "oreshnik",
|
||||
model: "airdef:iskander",
|
||||
speed: 9.0,
|
||||
turn: 0.0,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 0,
|
||||
scale: 2.6,
|
||||
engine: Engine::Rocket(1.5, 2.4),
|
||||
decoy: false,
|
||||
stealth: 3.0,
|
||||
sound: (sound::DRONE_ROCKET, 0.5, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(2600.0),
|
||||
},
|
||||
DroneKind::OreshnikBlock => &DroneSpec {
|
||||
name: "Орешник: блок",
|
||||
key: "oreshnik_block",
|
||||
model: "airdef:reentry",
|
||||
speed: 9.0,
|
||||
turn: 0.0,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 4,
|
||||
scale: 1.6,
|
||||
engine: Engine::Rocket(1.0, 1.0),
|
||||
decoy: false,
|
||||
stealth: 3.5,
|
||||
sound: (sound::DRONE_BALLISTIC, 0.8, 9999),
|
||||
class: Class::Ballistic,
|
||||
ballistic: None,
|
||||
},
|
||||
DroneKind::OreshnikSub => &DroneSpec {
|
||||
name: "Орешник: суббоеприпас",
|
||||
key: "oreshnik_sub",
|
||||
model: "airdef:reentry",
|
||||
speed: 9.0,
|
||||
turn: 0.05,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 3,
|
||||
scale: 1.1,
|
||||
engine: Engine::Rocket(1.0, 1.0),
|
||||
decoy: false,
|
||||
stealth: 4.0,
|
||||
sound: (sound::DRONE_BALLISTIC, 1.0, 9999),
|
||||
class: Class::Ballistic,
|
||||
ballistic: None,
|
||||
},
|
||||
}
|
||||
self.spec().launch_sound
|
||||
}
|
||||
pub fn from_key(key: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|k| k.spec().key == key)
|
||||
}
|
||||
/// Its radar cross-section, a Shahed's 0.3: radars see it from as far as its fourth root allows.
|
||||
pub fn rcs(self) -> f64 {
|
||||
match self {
|
||||
DroneKind::Shahed => 0.3,
|
||||
DroneKind::Geran3 => 0.4,
|
||||
DroneKind::Lancet => 0.08,
|
||||
// Corner reflectors make the decoy look as big as a Shahed.
|
||||
DroneKind::Gerbera => 0.4,
|
||||
DroneKind::Kh101 => 0.05,
|
||||
DroneKind::Kalibr => 0.15,
|
||||
DroneKind::Kh22 => 1.0,
|
||||
DroneKind::Zircon => 0.4,
|
||||
DroneKind::Iskander => 0.6,
|
||||
DroneKind::Kinzhal => 0.5,
|
||||
DroneKind::Oreshnik => 2.0,
|
||||
DroneKind::OreshnikBlock => 0.4,
|
||||
DroneKind::OreshnikSub => 0.08,
|
||||
}
|
||||
self.spec().rcs
|
||||
}
|
||||
}
|
||||
|
||||
/// Height over the target where the Oreshnik's blocks release their submunitions.
|
||||
const BLOCK_SPLIT: f64 = 300.0;
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
enum Flight {
|
||||
Cruise,
|
||||
@@ -1066,7 +804,7 @@ impl Drone {
|
||||
|
||||
// ---------------------------------------------------------------- interceptors
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
pub enum MissileKind {
|
||||
Pac3,
|
||||
M57e6,
|
||||
@@ -1096,6 +834,12 @@ struct MissileSpec {
|
||||
flame: f64,
|
||||
/// The nozzle, model units behind the centre (z).
|
||||
rear: f64,
|
||||
/// How far its own seeker holds a target (blocks); 0: command guidance only.
|
||||
seeker: f64,
|
||||
/// The sound of its launch.
|
||||
launch_sound: &'static str,
|
||||
/// Its motor's voice: event, gain and pitch.
|
||||
voice: (&'static str, f32, f32),
|
||||
}
|
||||
|
||||
impl MissileKind {
|
||||
@@ -1106,149 +850,12 @@ impl MissileKind {
|
||||
}
|
||||
/// The sound of its launch.
|
||||
pub fn launch_sound(self) -> &'static str {
|
||||
match self {
|
||||
MissileKind::Pac3 => sound::LAUNCH_PATRIOT,
|
||||
MissileKind::M57e6 => sound::LAUNCH_PANTSIR,
|
||||
MissileKind::M48n6 => sound::LAUNCH_S400_EJECT,
|
||||
MissileKind::M77n6 => sound::LAUNCH_S500_EJECT,
|
||||
MissileKind::IrisT | MissileKind::M9m338 => sound::LAUNCH_S400_EJECT,
|
||||
MissileKind::Amraam | MissileKind::M9m317 => sound::LAUNCH_PATRIOT,
|
||||
MissileKind::P1sun => sound::DRONE_LANCET,
|
||||
}
|
||||
}
|
||||
fn spec(self) -> &'static MissileSpec {
|
||||
match self {
|
||||
MissileKind::Pac3 => &MissileSpec {
|
||||
model: "airdef:pac3",
|
||||
speed: 5.5,
|
||||
accel: 0.35,
|
||||
turn: 0.3,
|
||||
boost: 8,
|
||||
cold: 0,
|
||||
pk: 0.9,
|
||||
fuse: 4.0,
|
||||
life: 240,
|
||||
scale: 1.0,
|
||||
flame: 1.0,
|
||||
rear: 28.0,
|
||||
},
|
||||
MissileKind::M57e6 => &MissileSpec {
|
||||
model: "airdef:m57e6",
|
||||
speed: 5.0,
|
||||
accel: 0.5,
|
||||
turn: 0.32,
|
||||
boost: 5,
|
||||
cold: 0,
|
||||
pk: 0.82,
|
||||
fuse: 3.0,
|
||||
life: 160,
|
||||
scale: 0.85,
|
||||
flame: 0.65,
|
||||
rear: 28.0,
|
||||
},
|
||||
MissileKind::M48n6 => &MissileSpec {
|
||||
model: "airdef:m48n6",
|
||||
speed: 7.0,
|
||||
accel: 0.5,
|
||||
turn: 0.22,
|
||||
boost: 8,
|
||||
cold: 5,
|
||||
pk: 0.9,
|
||||
fuse: 5.0,
|
||||
life: 300,
|
||||
scale: 1.5,
|
||||
flame: 1.4,
|
||||
rear: 30.0,
|
||||
},
|
||||
MissileKind::M77n6 => &MissileSpec {
|
||||
model: "airdef:m77n6",
|
||||
speed: 9.0,
|
||||
accel: 0.6,
|
||||
turn: 0.22,
|
||||
boost: 8,
|
||||
cold: 5,
|
||||
pk: 0.95,
|
||||
fuse: 6.0,
|
||||
life: 320,
|
||||
scale: 1.7,
|
||||
flame: 1.6,
|
||||
rear: 32.0,
|
||||
},
|
||||
MissileKind::IrisT => &MissileSpec {
|
||||
model: "airdef:irist_missile",
|
||||
speed: 7.0,
|
||||
accel: 0.5,
|
||||
turn: 0.35,
|
||||
boost: 6,
|
||||
cold: 4,
|
||||
pk: 0.9,
|
||||
fuse: 4.0,
|
||||
life: 260,
|
||||
scale: 0.95,
|
||||
flame: 0.9,
|
||||
rear: 28.0,
|
||||
},
|
||||
MissileKind::Amraam => &MissileSpec {
|
||||
model: "airdef:amraam_missile",
|
||||
speed: 6.0,
|
||||
accel: 0.45,
|
||||
turn: 0.3,
|
||||
boost: 6,
|
||||
cold: 0,
|
||||
pk: 0.85,
|
||||
fuse: 4.0,
|
||||
life: 260,
|
||||
scale: 0.9,
|
||||
flame: 0.9,
|
||||
rear: 28.0,
|
||||
},
|
||||
MissileKind::M9m317 => &MissileSpec {
|
||||
model: "airdef:m48n6",
|
||||
speed: 6.5,
|
||||
accel: 0.5,
|
||||
turn: 0.25,
|
||||
boost: 8,
|
||||
cold: 0,
|
||||
pk: 0.85,
|
||||
fuse: 5.0,
|
||||
life: 300,
|
||||
scale: 1.1,
|
||||
flame: 1.2,
|
||||
rear: 32.0,
|
||||
},
|
||||
MissileKind::M9m338 => &MissileSpec {
|
||||
model: "airdef:m57e6",
|
||||
speed: 5.5,
|
||||
accel: 0.6,
|
||||
turn: 0.4,
|
||||
boost: 4,
|
||||
cold: 4,
|
||||
pk: 0.9,
|
||||
fuse: 3.0,
|
||||
life: 160,
|
||||
scale: 1.0,
|
||||
flame: 0.8,
|
||||
rear: 28.0,
|
||||
},
|
||||
MissileKind::P1sun => &MissileSpec {
|
||||
model: "airdef:p1sun",
|
||||
speed: 2.8,
|
||||
accel: 0.12,
|
||||
turn: 0.3,
|
||||
boost: 4,
|
||||
cold: 0,
|
||||
pk: 0.8,
|
||||
fuse: 3.0,
|
||||
life: 900,
|
||||
scale: 0.8,
|
||||
// Electric: no flame.
|
||||
flame: 0.0,
|
||||
rear: 10.0,
|
||||
},
|
||||
}
|
||||
self.spec().launch_sound
|
||||
}
|
||||
}
|
||||
|
||||
include!("specs_generated.rs");
|
||||
|
||||
pub struct Missile {
|
||||
pub id: u32,
|
||||
pub kind: MissileKind,
|
||||
@@ -1308,15 +915,7 @@ impl Missile {
|
||||
impl MissileKind {
|
||||
/// How far its own seeker holds a target of average size (blocks); 0: command guidance only.
|
||||
pub fn seeker(self) -> f64 {
|
||||
match self {
|
||||
MissileKind::M57e6 => 0.0,
|
||||
MissileKind::Pac3 => 110.0,
|
||||
MissileKind::M48n6 | MissileKind::M77n6 => 120.0,
|
||||
MissileKind::IrisT | MissileKind::M9m317 => 90.0,
|
||||
MissileKind::Amraam => 130.0,
|
||||
MissileKind::M9m338 => 70.0,
|
||||
MissileKind::P1sun => 60.0,
|
||||
}
|
||||
self.spec().seeker
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
// Generated by tools/gen_catalog.py from catalog/specs.json. Do not edit.
|
||||
// Game-scale values, not real weapon data; the catalog is the only copy.
|
||||
|
||||
/// Share of the old slew speed a launcher keeps when it has no rate of its own.
|
||||
const DRIVE_SCALE: f64 = 0.4;
|
||||
/// Ticks a drive takes to reach its top speed (and to stop from it).
|
||||
const DRIVE_RAMP: f64 = 8.0;
|
||||
const DRIVE_TRAINABLE: (f64, f64) = (0.12, 0.0);
|
||||
const DRIVE_VERTICAL: (f64, f64) = (0.15, 0.0);
|
||||
const DRIVE_OTHER: (f64, f64) = (0.05, 0.05);
|
||||
/// Ballistic flight's gravity, blocks a tick².
|
||||
const GRAVITY: f64 = 0.05;
|
||||
/// Height over the target where the Oreshnik's blocks release their submunitions.
|
||||
const BLOCK_SPLIT: f64 = 300.0;
|
||||
/// How near a depot a system reloads straight from it.
|
||||
const DEPOT_REACH: f64 = 60.0;
|
||||
/// How near a truck must come to hand a reload over, or to load up at a depot.
|
||||
const HANDOVER: f64 = 10.0;
|
||||
/// What a truck carries.
|
||||
const TRUCK_LOAD: u32 = 2;
|
||||
const STOCK_LIMIT: u32 = 1000000;
|
||||
|
||||
const DRONE_SPEC_SHAHED: DroneSpec = DroneSpec {
|
||||
name: "Шахед-136",
|
||||
key: "shahed",
|
||||
model: "airdef:shahed",
|
||||
speed: 0.6,
|
||||
turn: 0.035,
|
||||
cruise: 45.0,
|
||||
clearance: 18.0,
|
||||
dive: 32.0,
|
||||
warhead: 3,
|
||||
scale: 1.3,
|
||||
engine: Engine::Prop(1.34, 1.0),
|
||||
decoy: false,
|
||||
stealth: 1.0,
|
||||
rcs: 0.3,
|
||||
sound: (sound::DRONE_SHAHED, 1.0, 48),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_GERAN3: DroneSpec = DroneSpec {
|
||||
name: "Герань-3",
|
||||
key: "geran3",
|
||||
model: "airdef:geran3",
|
||||
speed: 1.6,
|
||||
turn: 0.03,
|
||||
cruise: 60.0,
|
||||
clearance: 18.0,
|
||||
dive: 50.0,
|
||||
warhead: 2,
|
||||
scale: 1.15,
|
||||
engine: Engine::Jet(0.34, 1.55, 1.0),
|
||||
decoy: false,
|
||||
stealth: 1.1,
|
||||
rcs: 0.4,
|
||||
sound: (sound::DRONE_GERAN3, 1.0, 40),
|
||||
class: Class::Jet,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_LANCET: DroneSpec = DroneSpec {
|
||||
name: "Ланцет-3",
|
||||
key: "lancet",
|
||||
model: "airdef:lancet",
|
||||
speed: 1.1,
|
||||
turn: 0.07,
|
||||
cruise: 25.0,
|
||||
clearance: 10.0,
|
||||
dive: 38.0,
|
||||
warhead: 1,
|
||||
scale: 0.9,
|
||||
engine: Engine::Prop(0.93, 0.55),
|
||||
decoy: false,
|
||||
stealth: 1.4,
|
||||
rcs: 0.08,
|
||||
sound: (sound::DRONE_LANCET, 1.0, 32),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_GERBERA: DroneSpec = DroneSpec {
|
||||
name: "Гербера",
|
||||
key: "gerbera",
|
||||
model: "airdef:gerbera",
|
||||
speed: 0.5,
|
||||
turn: 0.04,
|
||||
cruise: 40.0,
|
||||
clearance: 15.0,
|
||||
dive: 30.0,
|
||||
warhead: 0,
|
||||
scale: 1.1,
|
||||
engine: Engine::Prop(1.03, 0.9),
|
||||
decoy: true,
|
||||
stealth: 0.9,
|
||||
rcs: 0.4,
|
||||
sound: (sound::DRONE_SHAHED, 1.3, 40),
|
||||
class: Class::Drone,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_KH101: DroneSpec = DroneSpec {
|
||||
name: "Х-101",
|
||||
key: "kh101",
|
||||
model: "airdef:kh101",
|
||||
speed: 2.4,
|
||||
turn: 0.025,
|
||||
cruise: 0.0,
|
||||
clearance: 12.0,
|
||||
dive: 26.0,
|
||||
warhead: 6,
|
||||
scale: 1.6,
|
||||
engine: Engine::Jet(-0.27, 1.4, 0.6),
|
||||
decoy: false,
|
||||
stealth: 1.5,
|
||||
rcs: 0.05,
|
||||
sound: (sound::DRONE_CRUISE, 1.0, 50),
|
||||
class: Class::Cruise,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_KALIBR: DroneSpec = DroneSpec {
|
||||
name: "Калибр",
|
||||
key: "kalibr",
|
||||
model: "airdef:kalibr",
|
||||
speed: 2.1,
|
||||
turn: 0.025,
|
||||
cruise: 0.0,
|
||||
clearance: 15.0,
|
||||
dive: 26.0,
|
||||
warhead: 5,
|
||||
scale: 1.5,
|
||||
engine: Engine::Jet(-0.2, 1.5, 0.6),
|
||||
decoy: false,
|
||||
stealth: 1.3,
|
||||
rcs: 0.15,
|
||||
sound: (sound::DRONE_CRUISE, 1.2, 50),
|
||||
class: Class::Cruise,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_KH22: DroneSpec = DroneSpec {
|
||||
name: "Х-22",
|
||||
key: "kh22",
|
||||
model: "airdef:kh22",
|
||||
speed: 4.0,
|
||||
turn: 0.03,
|
||||
cruise: 140.0,
|
||||
clearance: 40.0,
|
||||
dive: 150.0,
|
||||
warhead: 7,
|
||||
scale: 1.8,
|
||||
engine: Engine::Rocket(1.5, 1.3),
|
||||
decoy: false,
|
||||
stealth: 1.4,
|
||||
rcs: 1.0,
|
||||
sound: (sound::DRONE_ROCKET, 0.8, 46),
|
||||
class: Class::Supersonic,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_ZIRCON: DroneSpec = DroneSpec {
|
||||
name: "Циркон",
|
||||
key: "zircon",
|
||||
model: "airdef:zircon",
|
||||
speed: 5.5,
|
||||
turn: 0.025,
|
||||
cruise: 90.0,
|
||||
clearance: 30.0,
|
||||
dive: 110.0,
|
||||
warhead: 6,
|
||||
scale: 1.4,
|
||||
engine: Engine::Rocket(1.5, 1.1),
|
||||
decoy: false,
|
||||
stealth: 2.0,
|
||||
rcs: 0.4,
|
||||
sound: (sound::DRONE_ROCKET, 1.35, 46),
|
||||
class: Class::Supersonic,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S400,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_ISKANDER: DroneSpec = DroneSpec {
|
||||
name: "Искандер-М",
|
||||
key: "iskander",
|
||||
model: "airdef:iskander",
|
||||
speed: 5.0,
|
||||
turn: 0.04,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 8,
|
||||
scale: 1.8,
|
||||
engine: Engine::Rocket(1.5, 1.6),
|
||||
decoy: false,
|
||||
stealth: 1.6,
|
||||
rcs: 0.6,
|
||||
sound: (sound::DRONE_ROCKET, 0.7, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(1000.0),
|
||||
launch_sound: sound::LAUNCH_S500,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_KINZHAL: DroneSpec = DroneSpec {
|
||||
name: "Кинжал",
|
||||
key: "kinzhal",
|
||||
model: "airdef:kinzhal",
|
||||
speed: 7.5,
|
||||
turn: 0.03,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 8,
|
||||
scale: 1.7,
|
||||
engine: Engine::Rocket(1.5, 1.5),
|
||||
decoy: false,
|
||||
stealth: 2.2,
|
||||
rcs: 0.5,
|
||||
sound: (sound::DRONE_ROCKET, 0.6, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(1500.0),
|
||||
launch_sound: sound::LAUNCH_S500,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_ORESHNIK: DroneSpec = DroneSpec {
|
||||
name: "Орешник",
|
||||
key: "oreshnik",
|
||||
model: "airdef:iskander",
|
||||
speed: 9.0,
|
||||
turn: 0.0,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 0,
|
||||
scale: 2.6,
|
||||
engine: Engine::Rocket(1.5, 2.4),
|
||||
decoy: false,
|
||||
stealth: 3.0,
|
||||
rcs: 2.0,
|
||||
sound: (sound::DRONE_ROCKET, 0.5, 46),
|
||||
class: Class::Ballistic,
|
||||
ballistic: Some(2600.0),
|
||||
launch_sound: sound::LAUNCH_S500,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_ORESHNIKBLOCK: DroneSpec = DroneSpec {
|
||||
name: "Орешник: блок",
|
||||
key: "oreshnik_block",
|
||||
model: "airdef:reentry",
|
||||
speed: 9.0,
|
||||
turn: 0.0,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 4,
|
||||
scale: 1.6,
|
||||
engine: Engine::Rocket(1.0, 1.0),
|
||||
decoy: false,
|
||||
stealth: 3.5,
|
||||
rcs: 0.4,
|
||||
sound: (sound::DRONE_BALLISTIC, 0.8, 9999),
|
||||
class: Class::Ballistic,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S500,
|
||||
};
|
||||
|
||||
const DRONE_SPEC_ORESHNIKSUB: DroneSpec = DroneSpec {
|
||||
name: "Орешник: суббоеприпас",
|
||||
key: "oreshnik_sub",
|
||||
model: "airdef:reentry",
|
||||
speed: 9.0,
|
||||
turn: 0.05,
|
||||
cruise: 0.0,
|
||||
clearance: 0.0,
|
||||
dive: 0.0,
|
||||
warhead: 3,
|
||||
scale: 1.1,
|
||||
engine: Engine::Rocket(1.0, 1.0),
|
||||
decoy: false,
|
||||
stealth: 4.0,
|
||||
rcs: 0.08,
|
||||
sound: (sound::DRONE_BALLISTIC, 1.0, 9999),
|
||||
class: Class::Ballistic,
|
||||
ballistic: None,
|
||||
launch_sound: sound::LAUNCH_S500,
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_PAC3: MissileSpec = MissileSpec {
|
||||
model: "airdef:pac3",
|
||||
speed: 5.5,
|
||||
accel: 0.35,
|
||||
turn: 0.3,
|
||||
boost: 8,
|
||||
cold: 0,
|
||||
pk: 0.9,
|
||||
fuse: 4.0,
|
||||
life: 240,
|
||||
scale: 1.0,
|
||||
flame: 1.0,
|
||||
rear: 28.0,
|
||||
seeker: 110.0,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 1.0),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_M57E6: MissileSpec = MissileSpec {
|
||||
model: "airdef:m57e6",
|
||||
speed: 5.0,
|
||||
accel: 0.5,
|
||||
turn: 0.32,
|
||||
boost: 5,
|
||||
cold: 0,
|
||||
pk: 0.82,
|
||||
fuse: 3.0,
|
||||
life: 160,
|
||||
scale: 0.85,
|
||||
flame: 0.65,
|
||||
rear: 28.0,
|
||||
seeker: 0.0,
|
||||
launch_sound: sound::LAUNCH_PANTSIR,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 1.12),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_M48N6: MissileSpec = MissileSpec {
|
||||
model: "airdef:m48n6",
|
||||
speed: 7.0,
|
||||
accel: 0.5,
|
||||
turn: 0.22,
|
||||
boost: 8,
|
||||
cold: 5,
|
||||
pk: 0.9,
|
||||
fuse: 5.0,
|
||||
life: 300,
|
||||
scale: 1.5,
|
||||
flame: 1.4,
|
||||
rear: 30.0,
|
||||
seeker: 120.0,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 0.88),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_M77N6: MissileSpec = MissileSpec {
|
||||
model: "airdef:m77n6",
|
||||
speed: 9.0,
|
||||
accel: 0.6,
|
||||
turn: 0.22,
|
||||
boost: 8,
|
||||
cold: 5,
|
||||
pk: 0.95,
|
||||
fuse: 6.0,
|
||||
life: 320,
|
||||
scale: 1.7,
|
||||
flame: 1.6,
|
||||
rear: 32.0,
|
||||
seeker: 120.0,
|
||||
launch_sound: sound::LAUNCH_S500_EJECT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 0.8),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_IRIST: MissileSpec = MissileSpec {
|
||||
model: "airdef:irist_missile",
|
||||
speed: 7.0,
|
||||
accel: 0.5,
|
||||
turn: 0.35,
|
||||
boost: 6,
|
||||
cold: 4,
|
||||
pk: 0.9,
|
||||
fuse: 4.0,
|
||||
life: 260,
|
||||
scale: 0.95,
|
||||
flame: 0.9,
|
||||
rear: 28.0,
|
||||
seeker: 90.0,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 1.05),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_AMRAAM: MissileSpec = MissileSpec {
|
||||
model: "airdef:amraam_missile",
|
||||
speed: 6.0,
|
||||
accel: 0.45,
|
||||
turn: 0.3,
|
||||
boost: 6,
|
||||
cold: 0,
|
||||
pk: 0.85,
|
||||
fuse: 4.0,
|
||||
life: 260,
|
||||
scale: 0.9,
|
||||
flame: 0.9,
|
||||
rear: 28.0,
|
||||
seeker: 130.0,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 1.05),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_M9M317: MissileSpec = MissileSpec {
|
||||
model: "airdef:m48n6",
|
||||
speed: 6.5,
|
||||
accel: 0.5,
|
||||
turn: 0.25,
|
||||
boost: 8,
|
||||
cold: 0,
|
||||
pk: 0.85,
|
||||
fuse: 5.0,
|
||||
life: 300,
|
||||
scale: 1.1,
|
||||
flame: 1.2,
|
||||
rear: 32.0,
|
||||
seeker: 90.0,
|
||||
launch_sound: sound::LAUNCH_PATRIOT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 0.9),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_M9M338: MissileSpec = MissileSpec {
|
||||
model: "airdef:m57e6",
|
||||
speed: 5.5,
|
||||
accel: 0.6,
|
||||
turn: 0.4,
|
||||
boost: 4,
|
||||
cold: 4,
|
||||
pk: 0.9,
|
||||
fuse: 3.0,
|
||||
life: 160,
|
||||
scale: 1.0,
|
||||
flame: 0.8,
|
||||
rear: 28.0,
|
||||
seeker: 70.0,
|
||||
launch_sound: sound::LAUNCH_S400_EJECT,
|
||||
voice: (sound::MISSILE_FLIGHT, 1.0, 1.15),
|
||||
};
|
||||
|
||||
const MISSILE_SPEC_P1SUN: MissileSpec = MissileSpec {
|
||||
model: "airdef:p1sun",
|
||||
speed: 2.8,
|
||||
accel: 0.12,
|
||||
turn: 0.3,
|
||||
boost: 4,
|
||||
cold: 0,
|
||||
pk: 0.8,
|
||||
fuse: 3.0,
|
||||
life: 900,
|
||||
scale: 0.8,
|
||||
flame: 0.0,
|
||||
rear: 10.0,
|
||||
seeker: 60.0,
|
||||
launch_sound: sound::DRONE_LANCET,
|
||||
voice: (sound::DRONE_LANCET, 0.8, 1.7),
|
||||
};
|
||||
|
||||
impl DroneKind {
|
||||
/// Its row of numbers from `catalog/specs.json`.
|
||||
pub fn spec(self) -> &'static DroneSpec {
|
||||
match self {
|
||||
DroneKind::Shahed => &DRONE_SPEC_SHAHED,
|
||||
DroneKind::Geran3 => &DRONE_SPEC_GERAN3,
|
||||
DroneKind::Lancet => &DRONE_SPEC_LANCET,
|
||||
DroneKind::Gerbera => &DRONE_SPEC_GERBERA,
|
||||
DroneKind::Kh101 => &DRONE_SPEC_KH101,
|
||||
DroneKind::Kalibr => &DRONE_SPEC_KALIBR,
|
||||
DroneKind::Kh22 => &DRONE_SPEC_KH22,
|
||||
DroneKind::Zircon => &DRONE_SPEC_ZIRCON,
|
||||
DroneKind::Iskander => &DRONE_SPEC_ISKANDER,
|
||||
DroneKind::Kinzhal => &DRONE_SPEC_KINZHAL,
|
||||
DroneKind::Oreshnik => &DRONE_SPEC_ORESHNIK,
|
||||
DroneKind::OreshnikBlock => &DRONE_SPEC_ORESHNIKBLOCK,
|
||||
DroneKind::OreshnikSub => &DRONE_SPEC_ORESHNIKSUB,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MissileKind {
|
||||
fn spec(self) -> &'static MissileSpec {
|
||||
match self {
|
||||
MissileKind::Pac3 => &MISSILE_SPEC_PAC3,
|
||||
MissileKind::M57e6 => &MISSILE_SPEC_M57E6,
|
||||
MissileKind::M48n6 => &MISSILE_SPEC_M48N6,
|
||||
MissileKind::M77n6 => &MISSILE_SPEC_M77N6,
|
||||
MissileKind::IrisT => &MISSILE_SPEC_IRIST,
|
||||
MissileKind::Amraam => &MISSILE_SPEC_AMRAAM,
|
||||
MissileKind::M9m317 => &MISSILE_SPEC_M9M317,
|
||||
MissileKind::M9m338 => &MISSILE_SPEC_M9M338,
|
||||
MissileKind::P1sun => &MISSILE_SPEC_P1SUN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save keys by kind; a rename needs an explicit migration.
|
||||
#[cfg(test)]
|
||||
pub(super) const DRONE_SPEC_KEYS: [(DroneKind, &str); 13] = [
|
||||
(DroneKind::Shahed, "shahed"),
|
||||
(DroneKind::Geran3, "geran3"),
|
||||
(DroneKind::Lancet, "lancet"),
|
||||
(DroneKind::Gerbera, "gerbera"),
|
||||
(DroneKind::Kh101, "kh101"),
|
||||
(DroneKind::Kalibr, "kalibr"),
|
||||
(DroneKind::Kh22, "kh22"),
|
||||
(DroneKind::Zircon, "zircon"),
|
||||
(DroneKind::Iskander, "iskander"),
|
||||
(DroneKind::Kinzhal, "kinzhal"),
|
||||
(DroneKind::Oreshnik, "oreshnik"),
|
||||
(DroneKind::OreshnikBlock, "oreshnik_block"),
|
||||
(DroneKind::OreshnikSub, "oreshnik_sub"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) const MISSILE_SPEC_MODELS: [(MissileKind, &str); 9] = [
|
||||
(MissileKind::Pac3, "airdef:pac3"),
|
||||
(MissileKind::M57e6, "airdef:m57e6"),
|
||||
(MissileKind::M48n6, "airdef:m48n6"),
|
||||
(MissileKind::M77n6, "airdef:m77n6"),
|
||||
(MissileKind::IrisT, "airdef:irist_missile"),
|
||||
(MissileKind::Amraam, "airdef:amraam_missile"),
|
||||
(MissileKind::M9m317, "airdef:m48n6"),
|
||||
(MissileKind::M9m338, "airdef:m57e6"),
|
||||
(MissileKind::P1sun, "airdef:p1sun"),
|
||||
];
|
||||
@@ -0,0 +1,424 @@
|
||||
// Generated by tools/gen_catalog.py from catalog/specs.json. Do not edit.
|
||||
// Game-scale values, not real weapon data; the catalog is the only copy.
|
||||
|
||||
const NAMED_BATTERY_RADAR: &[(&str, f64, f64)] = &[("airdef:radar_trailer", 0.0, 4.2)];
|
||||
const NAMED_BATTERY_SPINNER: Option<(&str, f64, f64, f64, f64)> = Some(("airdef:radar_panel", 0.9, 4.2, 0.9, -0.3));
|
||||
const NAMED_TUBE_PIVOT: DVec3 = DVec3::new(0.0, -0.5, 1.5);
|
||||
|
||||
const UNIT_SPEC_PATRIOT: SamSpec = SamSpec {
|
||||
name: "Patriot",
|
||||
key: "patriot",
|
||||
missile: Some(MissileLoad { kind: MissileKind::Pac3, magazine: 4, min: 10.0, max: 450.0, interval: 16 }),
|
||||
gun: None,
|
||||
reload: 220,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.66,
|
||||
roles: 30,
|
||||
family: Family::Trainable,
|
||||
parts: Parts {
|
||||
body: "airdef:patriot_trailer",
|
||||
launcher: Some("airdef:patriot_launcher"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[("airdef:patriot_truck", 0.0, -3.0), ("airdef:patriot_radar", 0.0, 3.6)],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.69, 1.2),
|
||||
pivot: DVec3::new(0.0, -0.5, 1.375),
|
||||
muzzle: Muzzle::Patriot,
|
||||
sensor: 585.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_GEPARD: SamSpec = SamSpec {
|
||||
name: "Гепард",
|
||||
key: "gepard",
|
||||
missile: None,
|
||||
gun: Some(GunSpec { range: 135.0, track: 195.0, p: 0.3, bursts: 160, shell: 10.0, slew: (0.22, 0.16) }),
|
||||
reload: 160,
|
||||
scale: 1.25,
|
||||
rest_pitch: 0.0,
|
||||
roles: 7,
|
||||
family: Family::Gun,
|
||||
parts: Parts {
|
||||
body: "airdef:gepard_hull",
|
||||
launcher: Some("airdef:gepard_turret"),
|
||||
pack: Some("airdef:gepard_guns"),
|
||||
dish: Some(("airdef:gepard_dish", 0.95, 0.6, 0.0)),
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (1.55, 0.25),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 255.0,
|
||||
mast: 3.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_PANTSIR: SamSpec = SamSpec {
|
||||
name: "Панцирь-С1",
|
||||
key: "pantsir",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M57e6, magazine: 12, min: 25.0, max: 225.0, interval: 14 }),
|
||||
gun: Some(GunSpec { range: 90.0, track: 135.0, p: 0.25, bursts: 140, shell: 9.0, slew: (0.25, 0.18) }),
|
||||
reload: 280,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 15,
|
||||
family: Family::Turret,
|
||||
parts: Parts {
|
||||
body: "airdef:pantsir_truck",
|
||||
launcher: Some("airdef:pantsir_turret"),
|
||||
pack: Some("airdef:pantsir_pack"),
|
||||
dish: Some(("airdef:pantsir_dish", 0.85, 0.3, 0.3)),
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (1.35, 0.69),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Pantsir,
|
||||
sensor: 292.0,
|
||||
mast: 4.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_S400: SamSpec = SamSpec {
|
||||
name: "С-400",
|
||||
key: "s400",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M48n6, magazine: 4, min: 30.0, max: 720.0, interval: 20 }),
|
||||
gun: None,
|
||||
reload: 420,
|
||||
scale: 1.5,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 30,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:s400_chassis",
|
||||
launcher: Some("airdef:s400_tubes"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: NAMED_BATTERY_RADAR,
|
||||
spinner: NAMED_BATTERY_SPINNER,
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: NAMED_TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(2, 2, 7.0),
|
||||
sensor: 936.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_S500: SamSpec = SamSpec {
|
||||
name: "С-500",
|
||||
key: "s500",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 2, min: 40.0, max: 1050.0, interval: 20 }),
|
||||
gun: None,
|
||||
reload: 520,
|
||||
scale: 1.6,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 24,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:s500_chassis",
|
||||
launcher: Some("airdef:s500_tubes"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: NAMED_BATTERY_RADAR,
|
||||
spinner: NAMED_BATTERY_SPINNER,
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: NAMED_TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(2, 1, 9.5),
|
||||
sensor: 1365.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_IRIST: SamSpec = SamSpec {
|
||||
name: "IRIS-T SLM",
|
||||
key: "irist",
|
||||
missile: Some(MissileLoad { kind: MissileKind::IrisT, magazine: 8, min: 15.0, max: 600.0, interval: 10 }),
|
||||
gun: None,
|
||||
reload: 300,
|
||||
scale: 1.4,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 15,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:irist_chassis",
|
||||
launcher: Some("airdef:irist_tubes"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: NAMED_BATTERY_RADAR,
|
||||
spinner: NAMED_BATTERY_SPINNER,
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: NAMED_TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(4, 2, 4.0),
|
||||
sensor: 780.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_NASAMS: SamSpec = SamSpec {
|
||||
name: "NASAMS",
|
||||
key: "nasams",
|
||||
missile: Some(MissileLoad { kind: MissileKind::Amraam, magazine: 6, min: 10.0, max: 450.0, interval: 14 }),
|
||||
gun: None,
|
||||
reload: 260,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.5,
|
||||
roles: 14,
|
||||
family: Family::Trainable,
|
||||
parts: Parts {
|
||||
body: "airdef:nasams_trailer",
|
||||
launcher: Some("airdef:nasams_launcher"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[("airdef:radar_trailer", 0.0, 3.4)],
|
||||
spinner: Some(("airdef:radar_panel", 0.9, 3.4, 0.6, -0.3)),
|
||||
},
|
||||
mount: (0.62, 0.4),
|
||||
pivot: NAMED_TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(3, 2, 4.0),
|
||||
sensor: 585.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_BUK: SamSpec = SamSpec {
|
||||
name: "Бук-М2",
|
||||
key: "buk",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M9m317, magazine: 4, min: 20.0, max: 675.0, interval: 18 }),
|
||||
gun: None,
|
||||
reload: 400,
|
||||
scale: 1.4,
|
||||
rest_pitch: 0.0,
|
||||
roles: 14,
|
||||
family: Family::Turret,
|
||||
parts: Parts {
|
||||
body: "airdef:buk_chassis",
|
||||
launcher: Some("airdef:buk_turret"),
|
||||
pack: Some("airdef:buk_rails"),
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (1.55, 0.2),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Rails(&[-9.0, -3.0, 13.0, 19.0]),
|
||||
sensor: 878.0,
|
||||
mast: 4.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_TOR: SamSpec = SamSpec {
|
||||
name: "Тор-М2",
|
||||
key: "tor",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M9m338, magazine: 8, min: 8.0, max: 240.0, interval: 8 }),
|
||||
gun: None,
|
||||
reload: 300,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 15,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:tor_chassis",
|
||||
launcher: Some("airdef:tor_turret"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (1.5, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Cells,
|
||||
sensor: 315.0,
|
||||
mast: 3.5,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_P1SUN: SamSpec = SamSpec {
|
||||
name: "P1-SUN",
|
||||
key: "p1sun",
|
||||
missile: Some(MissileLoad { kind: MissileKind::P1sun, magazine: 8, min: 5.0, max: 675.0, interval: 12 }),
|
||||
gun: None,
|
||||
reload: 240,
|
||||
scale: 1.1,
|
||||
rest_pitch: 0.35,
|
||||
roles: 1,
|
||||
family: Family::Trainable,
|
||||
parts: Parts {
|
||||
body: "airdef:nasams_trailer",
|
||||
launcher: Some("airdef:p1sun_launcher"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.6, 0.3),
|
||||
pivot: NAMED_TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(4, 2, 3.0),
|
||||
sensor: 0.0,
|
||||
mast: 2.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_RADAR: SamSpec = SamSpec {
|
||||
name: "РЛС дальнего обнаружения",
|
||||
key: "radar",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.5,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts {
|
||||
body: "airdef:radar_truck",
|
||||
launcher: None,
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: Some(("airdef:radar_panel", 1.2, 1.2, 1.5, -0.25)),
|
||||
},
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 2550.0,
|
||||
mast: 16.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_DEPOT: SamSpec = SamSpec {
|
||||
name: "Склад боеприпасов",
|
||||
key: "depot",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.5,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts {
|
||||
body: "airdef:depot",
|
||||
launcher: None,
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 1.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_TZM: SamSpec = SamSpec {
|
||||
name: "ТЗМ",
|
||||
key: "tzm",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Truck,
|
||||
parts: Parts {
|
||||
body: "airdef:tzm_truck",
|
||||
launcher: None,
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 2.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_EW: SamSpec = SamSpec {
|
||||
name: "Станция РЭБ",
|
||||
key: "ew",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts {
|
||||
body: "airdef:ew_truck",
|
||||
launcher: None,
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 3.0,
|
||||
};
|
||||
|
||||
const UNIT_SPEC_LIGHT: SamSpec = SamSpec {
|
||||
name: "Прожектор",
|
||||
key: "light",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.2,
|
||||
rest_pitch: 0.6,
|
||||
roles: 0,
|
||||
family: Family::Light,
|
||||
parts: Parts {
|
||||
body: "airdef:searchlight",
|
||||
launcher: Some("airdef:searchlight_head"),
|
||||
pack: None,
|
||||
dish: None,
|
||||
fixed: &[],
|
||||
spinner: None,
|
||||
},
|
||||
mount: (0.85, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 1.5,
|
||||
};
|
||||
|
||||
impl SamKind {
|
||||
/// Its row of numbers from `catalog/specs.json`.
|
||||
pub fn spec(self) -> &'static SamSpec {
|
||||
match self {
|
||||
SamKind::Patriot => &UNIT_SPEC_PATRIOT,
|
||||
SamKind::Gepard => &UNIT_SPEC_GEPARD,
|
||||
SamKind::Pantsir => &UNIT_SPEC_PANTSIR,
|
||||
SamKind::S400 => &UNIT_SPEC_S400,
|
||||
SamKind::S500 => &UNIT_SPEC_S500,
|
||||
SamKind::IrisT => &UNIT_SPEC_IRIST,
|
||||
SamKind::Nasams => &UNIT_SPEC_NASAMS,
|
||||
SamKind::Buk => &UNIT_SPEC_BUK,
|
||||
SamKind::Tor => &UNIT_SPEC_TOR,
|
||||
SamKind::P1sun => &UNIT_SPEC_P1SUN,
|
||||
SamKind::Radar => &UNIT_SPEC_RADAR,
|
||||
SamKind::Depot => &UNIT_SPEC_DEPOT,
|
||||
SamKind::Tzm => &UNIT_SPEC_TZM,
|
||||
SamKind::Ew => &UNIT_SPEC_EW,
|
||||
SamKind::Light => &UNIT_SPEC_LIGHT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save keys by kind; a rename needs an explicit migration.
|
||||
#[cfg(test)]
|
||||
pub(super) const UNIT_SPEC_KEYS: [(SamKind, &str); 15] = [
|
||||
(SamKind::Patriot, "patriot"),
|
||||
(SamKind::Gepard, "gepard"),
|
||||
(SamKind::Pantsir, "pantsir"),
|
||||
(SamKind::S400, "s400"),
|
||||
(SamKind::S500, "s500"),
|
||||
(SamKind::IrisT, "irist"),
|
||||
(SamKind::Nasams, "nasams"),
|
||||
(SamKind::Buk, "buk"),
|
||||
(SamKind::Tor, "tor"),
|
||||
(SamKind::P1sun, "p1sun"),
|
||||
(SamKind::Radar, "radar"),
|
||||
(SamKind::Depot, "depot"),
|
||||
(SamKind::Tzm, "tzm"),
|
||||
(SamKind::Ew, "ew"),
|
||||
(SamKind::Light, "light"),
|
||||
];
|
||||
+4
-368
@@ -107,11 +107,6 @@ pub(super) struct Drive {
|
||||
pub pitch: (f64, f64),
|
||||
}
|
||||
|
||||
/// The drives are this share of the speeds they had before they had a speed of their own.
|
||||
const DRIVE_SCALE: f64 = 0.4;
|
||||
/// Ticks a drive takes to reach its top speed (and to stop from it).
|
||||
const DRIVE_RAMP: f64 = 8.0;
|
||||
|
||||
pub struct SamSpec {
|
||||
pub name: &'static str,
|
||||
pub key: &'static str,
|
||||
@@ -140,20 +135,16 @@ impl SamSpec {
|
||||
pub(super) fn drive(&self) -> Drive {
|
||||
let (yaw, pitch) = match (&self.gun, self.family) {
|
||||
(Some(g), _) => g.slew,
|
||||
(None, Family::Trainable) => (0.12, 0.0),
|
||||
(None, Family::Vertical) => (0.15, 0.0),
|
||||
(None, _) => (0.05, 0.05),
|
||||
(None, Family::Trainable) => DRIVE_TRAINABLE,
|
||||
(None, Family::Vertical) => DRIVE_VERTICAL,
|
||||
(None, _) => DRIVE_OTHER,
|
||||
};
|
||||
let axis = |speed: f64| (speed * DRIVE_SCALE, speed * DRIVE_SCALE / DRIVE_RAMP);
|
||||
Drive { yaw: axis(yaw), pitch: axis(pitch) }
|
||||
}
|
||||
}
|
||||
|
||||
const NO_PARTS: Parts = Parts { body: "", launcher: None, pack: None, dish: None, fixed: &[], spinner: None };
|
||||
/// The battery radar trailer behind a vertical launcher.
|
||||
const BATTERY_RADAR: &[(&str, f64, f64)] = &[("airdef:radar_trailer", 0.0, 4.2)];
|
||||
const BATTERY_SPINNER: Option<(&str, f64, f64, f64, f64)> = Some(("airdef:radar_panel", 0.9, 4.2, 0.9, -0.3));
|
||||
const TUBE_PIVOT: DVec3 = DVec3::new(0.0, -0.5, 1.5);
|
||||
include!("unit_specs_generated.rs");
|
||||
|
||||
impl SamKind {
|
||||
pub const ALL: [SamKind; super::catalog::UNIT_COUNT] = super::catalog::UNIT_KINDS;
|
||||
@@ -170,361 +161,6 @@ impl SamKind {
|
||||
SamKind::Tor,
|
||||
SamKind::P1sun,
|
||||
];
|
||||
pub fn spec(self) -> &'static SamSpec {
|
||||
match self {
|
||||
SamKind::Patriot => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Patriot",
|
||||
key: "patriot",
|
||||
missile: Some(MissileLoad { kind: MissileKind::Pac3, magazine: 4, min: 10.0, max: 450.0, interval: 16 }),
|
||||
gun: None,
|
||||
reload: 220,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.66,
|
||||
roles: 30,
|
||||
family: Family::Trainable,
|
||||
parts: Parts {
|
||||
body: "airdef:patriot_trailer",
|
||||
launcher: Some("airdef:patriot_launcher"),
|
||||
fixed: &[("airdef:patriot_truck", 0.0, -3.0), ("airdef:patriot_radar", 0.0, 3.6)],
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.69, 1.2),
|
||||
pivot: DVec3::new(0.0, -0.5, 1.375),
|
||||
muzzle: Muzzle::Patriot,
|
||||
sensor: 585.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Gepard => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Гепард",
|
||||
key: "gepard",
|
||||
missile: None,
|
||||
gun: Some(GunSpec { range: 135.0, track: 195.0, p: 0.3, bursts: 160, shell: 10.0, slew: (0.22, 0.16) }),
|
||||
reload: 160,
|
||||
scale: 1.25,
|
||||
rest_pitch: 0.0,
|
||||
roles: 7,
|
||||
family: Family::Gun,
|
||||
parts: Parts {
|
||||
body: "airdef:gepard_hull",
|
||||
launcher: Some("airdef:gepard_turret"),
|
||||
pack: Some("airdef:gepard_guns"),
|
||||
dish: Some(("airdef:gepard_dish", 0.95, 0.6, 0.0)),
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (1.55, 0.25),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 255.0,
|
||||
mast: 3.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Pantsir => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Панцирь-С1",
|
||||
key: "pantsir",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M57e6, magazine: 12, min: 25.0, max: 225.0, interval: 14 }),
|
||||
gun: Some(GunSpec { range: 90.0, track: 135.0, p: 0.25, bursts: 140, shell: 9.0, slew: (0.25, 0.18) }),
|
||||
reload: 280,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 15,
|
||||
family: Family::Turret,
|
||||
parts: Parts {
|
||||
body: "airdef:pantsir_truck",
|
||||
launcher: Some("airdef:pantsir_turret"),
|
||||
pack: Some("airdef:pantsir_pack"),
|
||||
dish: Some(("airdef:pantsir_dish", 0.85, 0.3, 0.3)),
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (1.35, 0.69),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Pantsir,
|
||||
sensor: 292.0,
|
||||
mast: 4.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::S400 => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "С-400",
|
||||
key: "s400",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M48n6, magazine: 4, min: 30.0, max: 720.0, interval: 20 }),
|
||||
gun: None,
|
||||
reload: 420,
|
||||
scale: 1.5,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 30,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:s400_chassis",
|
||||
launcher: Some("airdef:s400_tubes"),
|
||||
fixed: BATTERY_RADAR,
|
||||
spinner: BATTERY_SPINNER,
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(2, 2, 7.0),
|
||||
sensor: 936.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::S500 => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "С-500",
|
||||
key: "s500",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M77n6, magazine: 2, min: 40.0, max: 1050.0, interval: 20 }),
|
||||
gun: None,
|
||||
reload: 520,
|
||||
scale: 1.6,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 24,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:s500_chassis",
|
||||
launcher: Some("airdef:s500_tubes"),
|
||||
fixed: BATTERY_RADAR,
|
||||
spinner: BATTERY_SPINNER,
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(2, 1, 9.5),
|
||||
sensor: 1365.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::IrisT => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "IRIS-T SLM",
|
||||
key: "irist",
|
||||
missile: Some(MissileLoad { kind: MissileKind::IrisT, magazine: 8, min: 15.0, max: 600.0, interval: 10 }),
|
||||
gun: None,
|
||||
reload: 300,
|
||||
scale: 1.4,
|
||||
rest_pitch: HALF_PI - 0.02,
|
||||
roles: 15,
|
||||
family: Family::Vertical,
|
||||
parts: Parts {
|
||||
body: "airdef:irist_chassis",
|
||||
launcher: Some("airdef:irist_tubes"),
|
||||
fixed: BATTERY_RADAR,
|
||||
spinner: BATTERY_SPINNER,
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.7, 1.45),
|
||||
pivot: TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(4, 2, 4.0),
|
||||
sensor: 780.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Nasams => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "NASAMS",
|
||||
key: "nasams",
|
||||
missile: Some(MissileLoad { kind: MissileKind::Amraam, magazine: 6, min: 10.0, max: 450.0, interval: 14 }),
|
||||
gun: None,
|
||||
reload: 260,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.5,
|
||||
roles: 14,
|
||||
family: Family::Trainable,
|
||||
parts: Parts {
|
||||
body: "airdef:nasams_trailer",
|
||||
launcher: Some("airdef:nasams_launcher"),
|
||||
fixed: &[("airdef:radar_trailer", 0.0, 3.4)],
|
||||
spinner: Some(("airdef:radar_panel", 0.9, 3.4, 0.6, -0.3)),
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.62, 0.4),
|
||||
pivot: TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(3, 2, 4.0),
|
||||
sensor: 585.0,
|
||||
mast: 5.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Buk => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Бук-М2",
|
||||
key: "buk",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M9m317, magazine: 4, min: 20.0, max: 675.0, interval: 18 }),
|
||||
gun: None,
|
||||
reload: 400,
|
||||
scale: 1.4,
|
||||
rest_pitch: 0.0,
|
||||
roles: 14,
|
||||
family: Family::Turret,
|
||||
parts: Parts {
|
||||
body: "airdef:buk_chassis",
|
||||
launcher: Some("airdef:buk_turret"),
|
||||
pack: Some("airdef:buk_rails"),
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (1.55, 0.2),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Rails(&[-9.0, -3.0, 13.0, 19.0]),
|
||||
sensor: 878.0,
|
||||
mast: 4.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Tor => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Тор-М2",
|
||||
key: "tor",
|
||||
missile: Some(MissileLoad { kind: MissileKind::M9m338, magazine: 8, min: 8.0, max: 240.0, interval: 8 }),
|
||||
gun: None,
|
||||
reload: 300,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 15,
|
||||
family: Family::Vertical,
|
||||
parts: Parts { body: "airdef:tor_chassis", launcher: Some("airdef:tor_turret"), ..NO_PARTS },
|
||||
mount: (1.5, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::Cells,
|
||||
sensor: 315.0,
|
||||
mast: 3.5,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::P1sun => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "P1-SUN",
|
||||
key: "p1sun",
|
||||
missile: Some(MissileLoad { kind: MissileKind::P1sun, magazine: 8, min: 5.0, max: 675.0, interval: 12 }),
|
||||
gun: None,
|
||||
reload: 240,
|
||||
scale: 1.1,
|
||||
rest_pitch: 0.35,
|
||||
roles: 1,
|
||||
family: Family::Trainable,
|
||||
parts: Parts { body: "airdef:nasams_trailer", launcher: Some("airdef:p1sun_launcher"), ..NO_PARTS },
|
||||
mount: (0.6, 0.3),
|
||||
pivot: TUBE_PIVOT,
|
||||
muzzle: Muzzle::Grid(4, 2, 3.0),
|
||||
// No radar of its own: it fires on the network's tracks.
|
||||
sensor: 0.0,
|
||||
mast: 2.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Radar => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "РЛС дальнего обнаружения",
|
||||
key: "radar",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.5,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts {
|
||||
body: "airdef:radar_truck",
|
||||
spinner: Some(("airdef:radar_panel", 1.2, 1.2, 1.5, -0.25)),
|
||||
..NO_PARTS
|
||||
},
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 2550.0,
|
||||
mast: 16.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Depot => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Склад боеприпасов",
|
||||
key: "depot",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.5,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts { body: "airdef:depot", ..NO_PARTS },
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 1.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Tzm => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "ТЗМ",
|
||||
key: "tzm",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Truck,
|
||||
parts: Parts { body: "airdef:tzm_truck", ..NO_PARTS },
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 2.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Ew => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Станция РЭБ",
|
||||
key: "ew",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.3,
|
||||
rest_pitch: 0.0,
|
||||
roles: 0,
|
||||
family: Family::Static,
|
||||
parts: Parts { body: "airdef:ew_truck", ..NO_PARTS },
|
||||
mount: (0.0, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 3.0,
|
||||
};
|
||||
&S
|
||||
}
|
||||
SamKind::Light => {
|
||||
const S: SamSpec = SamSpec {
|
||||
name: "Прожектор",
|
||||
key: "light",
|
||||
missile: None,
|
||||
gun: None,
|
||||
reload: 0,
|
||||
scale: 1.2,
|
||||
rest_pitch: 0.6,
|
||||
roles: 0,
|
||||
family: Family::Light,
|
||||
parts: Parts { body: "airdef:searchlight", launcher: Some("airdef:searchlight_head"), ..NO_PARTS },
|
||||
mount: (0.85, 0.0),
|
||||
pivot: DVec3::ZERO,
|
||||
muzzle: Muzzle::None,
|
||||
sensor: 0.0,
|
||||
mast: 1.5,
|
||||
};
|
||||
&S
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn name(self) -> &'static str {
|
||||
self.spec().name
|
||||
}
|
||||
|
||||
@@ -18,20 +18,7 @@ impl MissileKind {
|
||||
/// The sound of its motor, its gain and its pitch: a rocket's roar, or the electric whine of
|
||||
/// a motor without a flame.
|
||||
pub fn engine_voice(self) -> (&'static str, f32, f32) {
|
||||
if self.spec().flame == 0.0 {
|
||||
return (super::catalog::sound::DRONE_LANCET, 0.8, 1.6);
|
||||
}
|
||||
let pitch = match self {
|
||||
MissileKind::Pac3 => 1.0,
|
||||
MissileKind::M57e6 => 1.12,
|
||||
MissileKind::M48n6 => 0.88,
|
||||
MissileKind::M77n6 => 0.8,
|
||||
MissileKind::IrisT | MissileKind::Amraam => 1.05,
|
||||
MissileKind::M9m317 => 0.9,
|
||||
MissileKind::M9m338 => 1.15,
|
||||
MissileKind::P1sun => 1.7,
|
||||
};
|
||||
(super::catalog::sound::MISSILE_FLIGHT, 1.0, pitch)
|
||||
self.spec().voice
|
||||
}
|
||||
/// Ticks from the launch until the motor lights (a cold launch throws it out of the tube first).
|
||||
pub fn lit_after(self) -> u32 {
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"shared": {
|
||||
"mobility": {
|
||||
"drive_scale": 0.4,
|
||||
"drive_ramp": 8.0,
|
||||
"trainable": [0.12, 0.0],
|
||||
"vertical": [0.15, 0.0],
|
||||
"other": [0.05, 0.05]
|
||||
},
|
||||
"flight": {
|
||||
"gravity": 0.05,
|
||||
"block_split": 300.0
|
||||
},
|
||||
"logistics": {
|
||||
"depot_reach": 60.0,
|
||||
"handover": 10.0,
|
||||
"truck_load": 2,
|
||||
"stock_limit": 1000000
|
||||
}
|
||||
},
|
||||
"drones": [
|
||||
{
|
||||
"kind": "Shahed",
|
||||
"key": "shahed",
|
||||
"name": "Шахед-136",
|
||||
"model": "airdef:shahed",
|
||||
"speed": 0.6,
|
||||
"turn": 0.035,
|
||||
"cruise": 45.0,
|
||||
"clearance": 18.0,
|
||||
"dive": 32.0,
|
||||
"warhead": 3,
|
||||
"scale": 1.3,
|
||||
"engine": {"type": "prop", "back": 1.34, "size": 1.0},
|
||||
"decoy": false,
|
||||
"stealth": 1.0,
|
||||
"sound": {"event": "drone.shahed", "pitch": 1.0, "interval": 48},
|
||||
"class": "drone",
|
||||
"ballistic": null,
|
||||
"rcs": 0.3,
|
||||
"launch_sound": "launch.s400_eject"
|
||||
},
|
||||
{
|
||||
"kind": "Geran3",
|
||||
"key": "geran3",
|
||||
"name": "Герань-3",
|
||||
"model": "airdef:geran3",
|
||||
"speed": 1.6,
|
||||
"turn": 0.03,
|
||||
"cruise": 60.0,
|
||||
"clearance": 18.0,
|
||||
"dive": 50.0,
|
||||
"warhead": 2,
|
||||
"scale": 1.15,
|
||||
"engine": {"type": "jet", "up": 0.34, "back": 1.55, "size": 1.0},
|
||||
"decoy": false,
|
||||
"stealth": 1.1,
|
||||
"sound": {"event": "drone.geran3", "pitch": 1.0, "interval": 40},
|
||||
"class": "jet",
|
||||
"ballistic": null,
|
||||
"rcs": 0.4,
|
||||
"launch_sound": "launch.s400_eject"
|
||||
},
|
||||
{
|
||||
"kind": "Lancet",
|
||||
"key": "lancet",
|
||||
"name": "Ланцет-3",
|
||||
"model": "airdef:lancet",
|
||||
"speed": 1.1,
|
||||
"turn": 0.07,
|
||||
"cruise": 25.0,
|
||||
"clearance": 10.0,
|
||||
"dive": 38.0,
|
||||
"warhead": 1,
|
||||
"scale": 0.9,
|
||||
"engine": {"type": "prop", "back": 0.93, "size": 0.55},
|
||||
"decoy": false,
|
||||
"stealth": 1.4,
|
||||
"sound": {"event": "drone.lancet", "pitch": 1.0, "interval": 32},
|
||||
"class": "drone",
|
||||
"ballistic": null,
|
||||
"rcs": 0.08,
|
||||
"launch_sound": "launch.s400_eject"
|
||||
},
|
||||
{
|
||||
"kind": "Gerbera",
|
||||
"key": "gerbera",
|
||||
"name": "Гербера",
|
||||
"model": "airdef:gerbera",
|
||||
"speed": 0.5,
|
||||
"turn": 0.04,
|
||||
"cruise": 40.0,
|
||||
"clearance": 15.0,
|
||||
"dive": 30.0,
|
||||
"warhead": 0,
|
||||
"scale": 1.1,
|
||||
"engine": {"type": "prop", "back": 1.03, "size": 0.9},
|
||||
"decoy": true,
|
||||
"stealth": 0.9,
|
||||
"sound": {"event": "drone.shahed", "pitch": 1.3, "interval": 40},
|
||||
"class": "drone",
|
||||
"ballistic": null,
|
||||
"rcs": 0.4,
|
||||
"launch_sound": "launch.s400_eject"
|
||||
},
|
||||
{
|
||||
"kind": "Kh101",
|
||||
"key": "kh101",
|
||||
"name": "Х-101",
|
||||
"model": "airdef:kh101",
|
||||
"speed": 2.4,
|
||||
"turn": 0.025,
|
||||
"cruise": 0.0,
|
||||
"clearance": 12.0,
|
||||
"dive": 26.0,
|
||||
"warhead": 6,
|
||||
"scale": 1.6,
|
||||
"engine": {"type": "jet", "up": -0.27, "back": 1.4, "size": 0.6},
|
||||
"decoy": false,
|
||||
"stealth": 1.5,
|
||||
"sound": {"event": "drone.cruise", "pitch": 1.0, "interval": 50},
|
||||
"class": "cruise",
|
||||
"ballistic": null,
|
||||
"rcs": 0.05,
|
||||
"launch_sound": "launch.patriot"
|
||||
},
|
||||
{
|
||||
"kind": "Kalibr",
|
||||
"key": "kalibr",
|
||||
"name": "Калибр",
|
||||
"model": "airdef:kalibr",
|
||||
"speed": 2.1,
|
||||
"turn": 0.025,
|
||||
"cruise": 0.0,
|
||||
"clearance": 15.0,
|
||||
"dive": 26.0,
|
||||
"warhead": 5,
|
||||
"scale": 1.5,
|
||||
"engine": {"type": "jet", "up": -0.2, "back": 1.5, "size": 0.6},
|
||||
"decoy": false,
|
||||
"stealth": 1.3,
|
||||
"sound": {"event": "drone.cruise", "pitch": 1.2, "interval": 50},
|
||||
"class": "cruise",
|
||||
"ballistic": null,
|
||||
"rcs": 0.15,
|
||||
"launch_sound": "launch.patriot"
|
||||
},
|
||||
{
|
||||
"kind": "Kh22",
|
||||
"key": "kh22",
|
||||
"name": "Х-22",
|
||||
"model": "airdef:kh22",
|
||||
"speed": 4.0,
|
||||
"turn": 0.03,
|
||||
"cruise": 140.0,
|
||||
"clearance": 40.0,
|
||||
"dive": 150.0,
|
||||
"warhead": 7,
|
||||
"scale": 1.8,
|
||||
"engine": {"type": "rocket", "back": 1.5, "size": 1.3},
|
||||
"decoy": false,
|
||||
"stealth": 1.4,
|
||||
"sound": {"event": "drone.rocket", "pitch": 0.8, "interval": 46},
|
||||
"class": "supersonic",
|
||||
"ballistic": null,
|
||||
"rcs": 1.0,
|
||||
"launch_sound": "launch.patriot"
|
||||
},
|
||||
{
|
||||
"kind": "Zircon",
|
||||
"key": "zircon",
|
||||
"name": "Циркон",
|
||||
"model": "airdef:zircon",
|
||||
"speed": 5.5,
|
||||
"turn": 0.025,
|
||||
"cruise": 90.0,
|
||||
"clearance": 30.0,
|
||||
"dive": 110.0,
|
||||
"warhead": 6,
|
||||
"scale": 1.4,
|
||||
"engine": {"type": "rocket", "back": 1.5, "size": 1.1},
|
||||
"decoy": false,
|
||||
"stealth": 2.0,
|
||||
"sound": {"event": "drone.rocket", "pitch": 1.35, "interval": 46},
|
||||
"class": "supersonic",
|
||||
"ballistic": null,
|
||||
"rcs": 0.4,
|
||||
"launch_sound": "launch.s400"
|
||||
},
|
||||
{
|
||||
"kind": "Iskander",
|
||||
"key": "iskander",
|
||||
"name": "Искандер-М",
|
||||
"model": "airdef:iskander",
|
||||
"speed": 5.0,
|
||||
"turn": 0.04,
|
||||
"cruise": 0.0,
|
||||
"clearance": 0.0,
|
||||
"dive": 0.0,
|
||||
"warhead": 8,
|
||||
"scale": 1.8,
|
||||
"engine": {"type": "rocket", "back": 1.5, "size": 1.6},
|
||||
"decoy": false,
|
||||
"stealth": 1.6,
|
||||
"sound": {"event": "drone.rocket", "pitch": 0.7, "interval": 46},
|
||||
"class": "ballistic",
|
||||
"ballistic": 1000.0,
|
||||
"rcs": 0.6,
|
||||
"launch_sound": "launch.s500"
|
||||
},
|
||||
{
|
||||
"kind": "Kinzhal",
|
||||
"key": "kinzhal",
|
||||
"name": "Кинжал",
|
||||
"model": "airdef:kinzhal",
|
||||
"speed": 7.5,
|
||||
"turn": 0.03,
|
||||
"cruise": 0.0,
|
||||
"clearance": 0.0,
|
||||
"dive": 0.0,
|
||||
"warhead": 8,
|
||||
"scale": 1.7,
|
||||
"engine": {"type": "rocket", "back": 1.5, "size": 1.5},
|
||||
"decoy": false,
|
||||
"stealth": 2.2,
|
||||
"sound": {"event": "drone.rocket", "pitch": 0.6, "interval": 46},
|
||||
"class": "ballistic",
|
||||
"ballistic": 1500.0,
|
||||
"rcs": 0.5,
|
||||
"launch_sound": "launch.s500"
|
||||
},
|
||||
{
|
||||
"kind": "Oreshnik",
|
||||
"key": "oreshnik",
|
||||
"name": "Орешник",
|
||||
"model": "airdef:iskander",
|
||||
"speed": 9.0,
|
||||
"turn": 0.0,
|
||||
"cruise": 0.0,
|
||||
"clearance": 0.0,
|
||||
"dive": 0.0,
|
||||
"warhead": 0,
|
||||
"scale": 2.6,
|
||||
"engine": {"type": "rocket", "back": 1.5, "size": 2.4},
|
||||
"decoy": false,
|
||||
"stealth": 3.0,
|
||||
"sound": {"event": "drone.rocket", "pitch": 0.5, "interval": 46},
|
||||
"class": "ballistic",
|
||||
"ballistic": 2600.0,
|
||||
"rcs": 2.0,
|
||||
"launch_sound": "launch.s500"
|
||||
},
|
||||
{
|
||||
"kind": "OreshnikBlock",
|
||||
"key": "oreshnik_block",
|
||||
"name": "Орешник: блок",
|
||||
"model": "airdef:reentry",
|
||||
"speed": 9.0,
|
||||
"turn": 0.0,
|
||||
"cruise": 0.0,
|
||||
"clearance": 0.0,
|
||||
"dive": 0.0,
|
||||
"warhead": 4,
|
||||
"scale": 1.6,
|
||||
"engine": {"type": "rocket", "back": 1.0, "size": 1.0},
|
||||
"decoy": false,
|
||||
"stealth": 3.5,
|
||||
"sound": {"event": "drone.ballistic", "pitch": 0.8, "interval": 9999},
|
||||
"class": "ballistic",
|
||||
"ballistic": null,
|
||||
"rcs": 0.4,
|
||||
"launch_sound": "launch.s500"
|
||||
},
|
||||
{
|
||||
"kind": "OreshnikSub",
|
||||
"key": "oreshnik_sub",
|
||||
"name": "Орешник: суббоеприпас",
|
||||
"model": "airdef:reentry",
|
||||
"speed": 9.0,
|
||||
"turn": 0.05,
|
||||
"cruise": 0.0,
|
||||
"clearance": 0.0,
|
||||
"dive": 0.0,
|
||||
"warhead": 3,
|
||||
"scale": 1.1,
|
||||
"engine": {"type": "rocket", "back": 1.0, "size": 1.0},
|
||||
"decoy": false,
|
||||
"stealth": 4.0,
|
||||
"sound": {"event": "drone.ballistic", "pitch": 1.0, "interval": 9999},
|
||||
"class": "ballistic",
|
||||
"ballistic": null,
|
||||
"rcs": 0.08,
|
||||
"launch_sound": "launch.s500"
|
||||
}
|
||||
],
|
||||
"units": [
|
||||
{
|
||||
"kind": "Patriot",
|
||||
"key": "patriot",
|
||||
"name": "Patriot",
|
||||
"missile": {"kind": "Pac3", "magazine": 4, "min": 10.0, "max": 450.0, "interval": 16},
|
||||
"gun": null,
|
||||
"reload": 220,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.66,
|
||||
"roles": 30,
|
||||
"family": "trainable",
|
||||
"parts": {
|
||||
"body": "airdef:patriot_trailer",
|
||||
"launcher": "airdef:patriot_launcher",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [["airdef:patriot_truck", 0.0, -3.0], ["airdef:patriot_radar", 0.0, 3.6]],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.69, 1.2],
|
||||
"pivot": [0.0, -0.5, 1.375],
|
||||
"muzzle": {"type": "patriot"},
|
||||
"sensor": 585.0,
|
||||
"mast": 5.0
|
||||
},
|
||||
{
|
||||
"kind": "Gepard",
|
||||
"key": "gepard",
|
||||
"name": "Гепард",
|
||||
"missile": null,
|
||||
"gun": {"range": 135.0, "track": 195.0, "p": 0.3, "bursts": 160, "shell": 10.0, "slew": [0.22, 0.16]},
|
||||
"reload": 160,
|
||||
"scale": 1.25,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 7,
|
||||
"family": "gun",
|
||||
"parts": {
|
||||
"body": "airdef:gepard_hull",
|
||||
"launcher": "airdef:gepard_turret",
|
||||
"pack": "airdef:gepard_guns",
|
||||
"dish": ["airdef:gepard_dish", 0.95, 0.6, 0.0],
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [1.55, 0.25],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 255.0,
|
||||
"mast": 3.0
|
||||
},
|
||||
{
|
||||
"kind": "Pantsir",
|
||||
"key": "pantsir",
|
||||
"name": "Панцирь-С1",
|
||||
"missile": {"kind": "M57e6", "magazine": 12, "min": 25.0, "max": 225.0, "interval": 14},
|
||||
"gun": {"range": 90.0, "track": 135.0, "p": 0.25, "bursts": 140, "shell": 9.0, "slew": [0.25, 0.18]},
|
||||
"reload": 280,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 15,
|
||||
"family": "turret",
|
||||
"parts": {
|
||||
"body": "airdef:pantsir_truck",
|
||||
"launcher": "airdef:pantsir_turret",
|
||||
"pack": "airdef:pantsir_pack",
|
||||
"dish": ["airdef:pantsir_dish", 0.85, 0.3, 0.3],
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [1.35, 0.69],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "pantsir"},
|
||||
"sensor": 292.0,
|
||||
"mast": 4.0
|
||||
},
|
||||
{
|
||||
"kind": "S400",
|
||||
"key": "s400",
|
||||
"name": "С-400",
|
||||
"missile": {"kind": "M48n6", "magazine": 4, "min": 30.0, "max": 720.0, "interval": 20},
|
||||
"gun": null,
|
||||
"reload": 420,
|
||||
"scale": 1.5,
|
||||
"rest_pitch": {"half_pi": -0.02},
|
||||
"roles": 30,
|
||||
"family": "vertical",
|
||||
"parts": {
|
||||
"body": "airdef:s400_chassis",
|
||||
"launcher": "airdef:s400_tubes",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": {"ref": "battery_radar"},
|
||||
"spinner": {"ref": "battery_spinner"}
|
||||
},
|
||||
"mount": [0.7, 1.45],
|
||||
"pivot": {"ref": "tube_pivot"},
|
||||
"muzzle": {"type": "grid", "cols": 2, "rows": 2, "size": 7.0},
|
||||
"sensor": 936.0,
|
||||
"mast": 5.0
|
||||
},
|
||||
{
|
||||
"kind": "S500",
|
||||
"key": "s500",
|
||||
"name": "С-500",
|
||||
"missile": {"kind": "M77n6", "magazine": 2, "min": 40.0, "max": 1050.0, "interval": 20},
|
||||
"gun": null,
|
||||
"reload": 520,
|
||||
"scale": 1.6,
|
||||
"rest_pitch": {"half_pi": -0.02},
|
||||
"roles": 24,
|
||||
"family": "vertical",
|
||||
"parts": {
|
||||
"body": "airdef:s500_chassis",
|
||||
"launcher": "airdef:s500_tubes",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": {"ref": "battery_radar"},
|
||||
"spinner": {"ref": "battery_spinner"}
|
||||
},
|
||||
"mount": [0.7, 1.45],
|
||||
"pivot": {"ref": "tube_pivot"},
|
||||
"muzzle": {"type": "grid", "cols": 2, "rows": 1, "size": 9.5},
|
||||
"sensor": 1365.0,
|
||||
"mast": 5.0
|
||||
},
|
||||
{
|
||||
"kind": "IrisT",
|
||||
"key": "irist",
|
||||
"name": "IRIS-T SLM",
|
||||
"missile": {"kind": "IrisT", "magazine": 8, "min": 15.0, "max": 600.0, "interval": 10},
|
||||
"gun": null,
|
||||
"reload": 300,
|
||||
"scale": 1.4,
|
||||
"rest_pitch": {"half_pi": -0.02},
|
||||
"roles": 15,
|
||||
"family": "vertical",
|
||||
"parts": {
|
||||
"body": "airdef:irist_chassis",
|
||||
"launcher": "airdef:irist_tubes",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": {"ref": "battery_radar"},
|
||||
"spinner": {"ref": "battery_spinner"}
|
||||
},
|
||||
"mount": [0.7, 1.45],
|
||||
"pivot": {"ref": "tube_pivot"},
|
||||
"muzzle": {"type": "grid", "cols": 4, "rows": 2, "size": 4.0},
|
||||
"sensor": 780.0,
|
||||
"mast": 5.0
|
||||
},
|
||||
{
|
||||
"kind": "Nasams",
|
||||
"key": "nasams",
|
||||
"name": "NASAMS",
|
||||
"missile": {"kind": "Amraam", "magazine": 6, "min": 10.0, "max": 450.0, "interval": 14},
|
||||
"gun": null,
|
||||
"reload": 260,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.5,
|
||||
"roles": 14,
|
||||
"family": "trainable",
|
||||
"parts": {
|
||||
"body": "airdef:nasams_trailer",
|
||||
"launcher": "airdef:nasams_launcher",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [["airdef:radar_trailer", 0.0, 3.4]],
|
||||
"spinner": ["airdef:radar_panel", 0.9, 3.4, 0.6, -0.3]
|
||||
},
|
||||
"mount": [0.62, 0.4],
|
||||
"pivot": {"ref": "tube_pivot"},
|
||||
"muzzle": {"type": "grid", "cols": 3, "rows": 2, "size": 4.0},
|
||||
"sensor": 585.0,
|
||||
"mast": 5.0
|
||||
},
|
||||
{
|
||||
"kind": "Buk",
|
||||
"key": "buk",
|
||||
"name": "Бук-М2",
|
||||
"missile": {"kind": "M9m317", "magazine": 4, "min": 20.0, "max": 675.0, "interval": 18},
|
||||
"gun": null,
|
||||
"reload": 400,
|
||||
"scale": 1.4,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 14,
|
||||
"family": "turret",
|
||||
"parts": {
|
||||
"body": "airdef:buk_chassis",
|
||||
"launcher": "airdef:buk_turret",
|
||||
"pack": "airdef:buk_rails",
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [1.55, 0.2],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "rails", "xs": [-9.0, -3.0, 13.0, 19.0]},
|
||||
"sensor": 878.0,
|
||||
"mast": 4.0
|
||||
},
|
||||
{
|
||||
"kind": "Tor",
|
||||
"key": "tor",
|
||||
"name": "Тор-М2",
|
||||
"missile": {"kind": "M9m338", "magazine": 8, "min": 8.0, "max": 240.0, "interval": 8},
|
||||
"gun": null,
|
||||
"reload": 300,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 15,
|
||||
"family": "vertical",
|
||||
"parts": {
|
||||
"body": "airdef:tor_chassis",
|
||||
"launcher": "airdef:tor_turret",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [1.5, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "cells"},
|
||||
"sensor": 315.0,
|
||||
"mast": 3.5
|
||||
},
|
||||
{
|
||||
"kind": "P1sun",
|
||||
"key": "p1sun",
|
||||
"name": "P1-SUN",
|
||||
"missile": {"kind": "P1sun", "magazine": 8, "min": 5.0, "max": 675.0, "interval": 12},
|
||||
"gun": null,
|
||||
"reload": 240,
|
||||
"scale": 1.1,
|
||||
"rest_pitch": 0.35,
|
||||
"roles": 1,
|
||||
"family": "trainable",
|
||||
"parts": {
|
||||
"body": "airdef:nasams_trailer",
|
||||
"launcher": "airdef:p1sun_launcher",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.6, 0.3],
|
||||
"pivot": {"ref": "tube_pivot"},
|
||||
"muzzle": {"type": "grid", "cols": 4, "rows": 2, "size": 3.0},
|
||||
"sensor": 0.0,
|
||||
"mast": 2.0
|
||||
},
|
||||
{
|
||||
"kind": "Radar",
|
||||
"key": "radar",
|
||||
"name": "РЛС дальнего обнаружения",
|
||||
"missile": null,
|
||||
"gun": null,
|
||||
"reload": 0,
|
||||
"scale": 1.5,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 0,
|
||||
"family": "static",
|
||||
"parts": {
|
||||
"body": "airdef:radar_truck",
|
||||
"launcher": null,
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": ["airdef:radar_panel", 1.2, 1.2, 1.5, -0.25]
|
||||
},
|
||||
"mount": [0.0, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 2550.0,
|
||||
"mast": 16.0
|
||||
},
|
||||
{
|
||||
"kind": "Depot",
|
||||
"key": "depot",
|
||||
"name": "Склад боеприпасов",
|
||||
"missile": null,
|
||||
"gun": null,
|
||||
"reload": 0,
|
||||
"scale": 1.5,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 0,
|
||||
"family": "static",
|
||||
"parts": {
|
||||
"body": "airdef:depot",
|
||||
"launcher": null,
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.0, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 0.0,
|
||||
"mast": 1.0
|
||||
},
|
||||
{
|
||||
"kind": "Tzm",
|
||||
"key": "tzm",
|
||||
"name": "ТЗМ",
|
||||
"missile": null,
|
||||
"gun": null,
|
||||
"reload": 0,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 0,
|
||||
"family": "truck",
|
||||
"parts": {
|
||||
"body": "airdef:tzm_truck",
|
||||
"launcher": null,
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.0, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 0.0,
|
||||
"mast": 2.0
|
||||
},
|
||||
{
|
||||
"kind": "Ew",
|
||||
"key": "ew",
|
||||
"name": "Станция РЭБ",
|
||||
"missile": null,
|
||||
"gun": null,
|
||||
"reload": 0,
|
||||
"scale": 1.3,
|
||||
"rest_pitch": 0.0,
|
||||
"roles": 0,
|
||||
"family": "static",
|
||||
"parts": {
|
||||
"body": "airdef:ew_truck",
|
||||
"launcher": null,
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.0, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 0.0,
|
||||
"mast": 3.0
|
||||
},
|
||||
{
|
||||
"kind": "Light",
|
||||
"key": "light",
|
||||
"name": "Прожектор",
|
||||
"missile": null,
|
||||
"gun": null,
|
||||
"reload": 0,
|
||||
"scale": 1.2,
|
||||
"rest_pitch": 0.6,
|
||||
"roles": 0,
|
||||
"family": "light",
|
||||
"parts": {
|
||||
"body": "airdef:searchlight",
|
||||
"launcher": "airdef:searchlight_head",
|
||||
"pack": null,
|
||||
"dish": null,
|
||||
"fixed": [],
|
||||
"spinner": null
|
||||
},
|
||||
"mount": [0.85, 0.0],
|
||||
"pivot": [0.0, 0.0, 0.0],
|
||||
"muzzle": {"type": "none"},
|
||||
"sensor": 0.0,
|
||||
"mast": 1.5
|
||||
}
|
||||
],
|
||||
"missiles": [
|
||||
{
|
||||
"kind": "Pac3",
|
||||
"model": "airdef:pac3",
|
||||
"speed": 5.5,
|
||||
"accel": 0.35,
|
||||
"turn": 0.3,
|
||||
"boost": 8,
|
||||
"cold": 0,
|
||||
"pk": 0.9,
|
||||
"fuse": 4.0,
|
||||
"life": 240,
|
||||
"scale": 1.0,
|
||||
"flame": 1.0,
|
||||
"rear": 28.0,
|
||||
"seeker": 110.0,
|
||||
"launch_sound": "launch.patriot",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 1.0}
|
||||
},
|
||||
{
|
||||
"kind": "M57e6",
|
||||
"model": "airdef:m57e6",
|
||||
"speed": 5.0,
|
||||
"accel": 0.5,
|
||||
"turn": 0.32,
|
||||
"boost": 5,
|
||||
"cold": 0,
|
||||
"pk": 0.82,
|
||||
"fuse": 3.0,
|
||||
"life": 160,
|
||||
"scale": 0.85,
|
||||
"flame": 0.65,
|
||||
"rear": 28.0,
|
||||
"seeker": 0.0,
|
||||
"launch_sound": "launch.pantsir",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 1.12}
|
||||
},
|
||||
{
|
||||
"kind": "M48n6",
|
||||
"model": "airdef:m48n6",
|
||||
"speed": 7.0,
|
||||
"accel": 0.5,
|
||||
"turn": 0.22,
|
||||
"boost": 8,
|
||||
"cold": 5,
|
||||
"pk": 0.9,
|
||||
"fuse": 5.0,
|
||||
"life": 300,
|
||||
"scale": 1.5,
|
||||
"flame": 1.4,
|
||||
"rear": 30.0,
|
||||
"seeker": 120.0,
|
||||
"launch_sound": "launch.s400_eject",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 0.88}
|
||||
},
|
||||
{
|
||||
"kind": "M77n6",
|
||||
"model": "airdef:m77n6",
|
||||
"speed": 9.0,
|
||||
"accel": 0.6,
|
||||
"turn": 0.22,
|
||||
"boost": 8,
|
||||
"cold": 5,
|
||||
"pk": 0.95,
|
||||
"fuse": 6.0,
|
||||
"life": 320,
|
||||
"scale": 1.7,
|
||||
"flame": 1.6,
|
||||
"rear": 32.0,
|
||||
"seeker": 120.0,
|
||||
"launch_sound": "launch.s500_eject",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 0.8}
|
||||
},
|
||||
{
|
||||
"kind": "IrisT",
|
||||
"model": "airdef:irist_missile",
|
||||
"speed": 7.0,
|
||||
"accel": 0.5,
|
||||
"turn": 0.35,
|
||||
"boost": 6,
|
||||
"cold": 4,
|
||||
"pk": 0.9,
|
||||
"fuse": 4.0,
|
||||
"life": 260,
|
||||
"scale": 0.95,
|
||||
"flame": 0.9,
|
||||
"rear": 28.0,
|
||||
"seeker": 90.0,
|
||||
"launch_sound": "launch.s400_eject",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 1.05}
|
||||
},
|
||||
{
|
||||
"kind": "Amraam",
|
||||
"model": "airdef:amraam_missile",
|
||||
"speed": 6.0,
|
||||
"accel": 0.45,
|
||||
"turn": 0.3,
|
||||
"boost": 6,
|
||||
"cold": 0,
|
||||
"pk": 0.85,
|
||||
"fuse": 4.0,
|
||||
"life": 260,
|
||||
"scale": 0.9,
|
||||
"flame": 0.9,
|
||||
"rear": 28.0,
|
||||
"seeker": 130.0,
|
||||
"launch_sound": "launch.patriot",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 1.05}
|
||||
},
|
||||
{
|
||||
"kind": "M9m317",
|
||||
"model": "airdef:m48n6",
|
||||
"speed": 6.5,
|
||||
"accel": 0.5,
|
||||
"turn": 0.25,
|
||||
"boost": 8,
|
||||
"cold": 0,
|
||||
"pk": 0.85,
|
||||
"fuse": 5.0,
|
||||
"life": 300,
|
||||
"scale": 1.1,
|
||||
"flame": 1.2,
|
||||
"rear": 32.0,
|
||||
"seeker": 90.0,
|
||||
"launch_sound": "launch.patriot",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 0.9}
|
||||
},
|
||||
{
|
||||
"kind": "M9m338",
|
||||
"model": "airdef:m57e6",
|
||||
"speed": 5.5,
|
||||
"accel": 0.6,
|
||||
"turn": 0.4,
|
||||
"boost": 4,
|
||||
"cold": 4,
|
||||
"pk": 0.9,
|
||||
"fuse": 3.0,
|
||||
"life": 160,
|
||||
"scale": 1.0,
|
||||
"flame": 0.8,
|
||||
"rear": 28.0,
|
||||
"seeker": 70.0,
|
||||
"launch_sound": "launch.s400_eject",
|
||||
"voice": {"event": "missile.flight", "gain": 1.0, "pitch": 1.15}
|
||||
},
|
||||
{
|
||||
"kind": "P1sun",
|
||||
"model": "airdef:p1sun",
|
||||
"speed": 2.8,
|
||||
"accel": 0.12,
|
||||
"turn": 0.3,
|
||||
"boost": 4,
|
||||
"cold": 0,
|
||||
"pk": 0.8,
|
||||
"fuse": 3.0,
|
||||
"life": 900,
|
||||
"scale": 0.8,
|
||||
"flame": 0.0,
|
||||
"rear": 10.0,
|
||||
"seeker": 60.0,
|
||||
"launch_sound": "drone.lancet",
|
||||
"voice": {"event": "drone.lancet", "gain": 0.8, "pitch": 1.7}
|
||||
}
|
||||
],
|
||||
"named": {
|
||||
"battery_radar": {
|
||||
"kind": "fixed",
|
||||
"value": [["airdef:radar_trailer", 0.0, 4.2]]
|
||||
},
|
||||
"battery_spinner": {
|
||||
"kind": "spinner",
|
||||
"value": ["airdef:radar_panel", 0.9, 4.2, 0.9, -0.3]
|
||||
},
|
||||
"tube_pivot": {
|
||||
"kind": "pivot",
|
||||
"value": [0.0, -0.5, 1.5]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,12 @@
|
||||
Движок `57f8b2853c4443f7e3ff5701600cbb262b53118f`, API29. В этом файле ниже сохранён
|
||||
исторический журнал предыдущих серий; старые SHA и числа тестов не заменяют новую контрольную точку.
|
||||
|
||||
След #91 продолжен: `catalog/specs.json` — единственная копия числовых строк дронов, ракет и
|
||||
комплексов; `python3 tools/gen_catalog.py` генерирует `specs_generated.rs` и
|
||||
`unit_specs_generated.rs`, `--check` (CI) валит расхождение. Числа не менялись; при сверке с
|
||||
литералами найден и исправлен только голос P1-SUN в JSON (1.6 → 1.7). Остаток: варианты
|
||||
комплексов и игровая приёмка.
|
||||
|
||||
### Контрольная точка 3 ноября
|
||||
|
||||
Код предыдущих четырёх задач интегрирован до `6e2d661`. Продолжение #102/обратного вывоза
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
CI проверяет актуальность. Сохранены порядок позиционных сейвов, все 35 звуковых событий,
|
||||
OGG и порядок общего RNG синтезатора. Числовые `DroneSpec`/`SamSpec` остаются типизированным
|
||||
источником в Rust; полного переноса всех характеристик в JSON не было.
|
||||
Продолжение переноса чисел — ниже.
|
||||
- **#102:** `63aefc7`: `test-budgets` включает настоящий `--fuel-profile` и сохраняет
|
||||
`measured_fuel.callbacks`, включая ошибки. Отсутствие запрошенных счётчиков запрещает success.
|
||||
В движке `6230ec703`/`d8fa77b00` добавлены API28 point-lights-status и API29 sound-sources-status.
|
||||
@@ -41,6 +42,32 @@
|
||||
[Отчёт и SHA дорожки](measurements/mass-acoustics-2026-11-04.json).
|
||||
Это не замер полного игрового CPU/GPU, не Windows FPS и не готовый MP4.
|
||||
|
||||
## Продолжение #91: один источник чисел
|
||||
|
||||
`catalog/specs.json` теперь единственная копия числовых строк симуляции: 13 дронов
|
||||
(`DroneKind::spec`), 9 ракет (включая `seeker`, звук пуска и голос мотора), 15 комплексов
|
||||
(`SamKind::spec`, включая части моделей и именованные наборы), общие константы гравитации,
|
||||
высоты раскрытия «Орешника», приводов и логистики. `tools/gen_catalog.py` генерирует
|
||||
`airdef-mod/src/sim/specs_generated.rs` и `unit_specs_generated.rs`; `include!` встраивает их в
|
||||
`mod.rs` и `units.rs`, поэтому мод и `airdef-core` компилируют одни и те же строки. Прежние
|
||||
`match`-литералы удалены; `DroneKind::spec`, `MissileKind::spec`, `SamKind::spec` и их производные
|
||||
(RCS, seeker, звуки, голос) читают сгенерированные константы.
|
||||
|
||||
`load_specs` проверяет схему, звуковые ссылки, совпадение набора kinds с `content.json`, ссылки на
|
||||
именованные части и то, что каждая строка кем-то используется; `gen_catalog.py --check` уже в
|
||||
CI-шаге «Проверка generated assets», поэтому расхождение JSON и Rust валит сборку.
|
||||
|
||||
Сверка перед переносом: JSON сравнивался с литералами `632061d` по всем полям. Найдено одно
|
||||
расхождение — голос P1-SUN был записан с pitch 1.6 вместо фактического 1.7 (ветка `flame == 0`
|
||||
в `voices.rs`); JSON исправлен, поведение не менялось. После генерации строки сверены с прежними
|
||||
литералами поле-в-поле (включая части моделей и именованные refs) — расхождений нет. Порядок
|
||||
raid/save, ключи `sam`/дронов, RNG и тексты не менялись.
|
||||
|
||||
Проверки этого этапа: `cargo test --release` ядра — **194 passed, 7 ignored** + **8 сценариев**;
|
||||
адаптер — **64 unit**, **1 boundary**, **5 сценариев**; `gen_catalog.py --check`, `gen_models.py --check`,
|
||||
тесты генераторов/звука/границ моделей; настоящий WASM `test-wasm` — **27 шагов**, 1 скрипт.
|
||||
GUI/MP4 и варианты комплексов (разные ракеты/профили по батареям) в этот этап не входили.
|
||||
|
||||
## Проверки и продолжение
|
||||
|
||||
Объединённый core release: **193 unit**, 7 ignored, **8 сценариев**; адаптер: **64 unit**,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| #87 | Каноническое SDK-free ядро `airdef-core`, используется модом и сценариями | Дальнейшее отделение текстового/визуального вывода домена от адаптера |
|
||||
| #88 | Типизированный журнал измеренных переходов; read-only Snapshot для Smoke/Shown; записанные пожары, позы, пуски и зажигание | Перевести оставшиеся model/audio adapters и режиссёра на общий снимок; замер полной 10-минутной записи |
|
||||
| #89 | Session владеет Live/Replay/Export; явные часы/поколения; единый cleanup; задержанный звук следует часам показа; выход в заголовок завершает показ одним cleanup (хук `frame`, `Session::world_left`); движок освобождает temp/лог ошибки энкодера и не трогает готовый MP4 | Реальный короткий MP4 проверен 4 ноября (звук/видео, отмена, ошибка SDK и энкодера, повторный запуск, совпадение кадров и хвоста очереди, сохранность прежнего MP4). Остаётся: 10-минутный экспорт; приёмка Windows/Linux |
|
||||
| #91 | Общий каталог доступности и потребители меню/инвентаря; явный RM48U-профиль; корректные подписи очередей | Единый источник всех чисел, звуков и Python-регистраций; проверка вариантов комплексов |
|
||||
| #91 | Общий каталог доступности и потребители меню/инвентаря; явный RM48U-профиль; корректные подписи очередей; `catalog/specs.json` — единственный источник числовых строк, из него генерируются Rust-таблицы для мода и `airdef-core`, CI проверяет свежесть | Варианты комплексов (разные ракеты/профили по батареям) и игровая приёмка; каталог остаётся статичными данными, не динамическим ECS |
|
||||
| #92 | Authored overrides, проверка ссылок/PNG/границ, manifest, безопасная транзакционная генерация; registry/texture/icon modules | Дальнейшее ручное улучшение контента относится к #115 |
|
||||
| #96 | Акустические пути; измеренные синтетические occlusion/echo/reverb fixtures; лицензии и исходные notices в архивах; настоящий MP4 с акустикой: пары on/off, статусы ready, окклюзия/поглощение и диффузный wet-остаток в замкнутой геометрии | Прослушивание, игровой массовый звук/CPU, чистый хвост/RT60 в MP4; точность RT60 пока по допуску 35%, не 15% |
|
||||
| #102 | Pinned engine/API29, packer/build identity; smoke/acoustics/capture-owner/light/source milestones; fallback/владение; WASM27 и flat stress271; измеренный callback fuel; JSON provenance/failure tests; Metal/no-audio smoke; URL/Git-регрессия; owner-scoped camera targets | Старое69-шаговое утверждение о перекрытии отозвано; статус камер, полные query/voice/peak budgets, GPU/готовый MP4 и Windows-приёмка остаются |
|
||||
|
||||
+3
-1
@@ -34,10 +34,12 @@
|
||||
Команды:
|
||||
|
||||
- `python3 -m pip install -r tools/requirements.txt` — зависимости генератора (закреплён Pillow).
|
||||
- `python3 tools/gen_catalog.py` — обновить Rust-константы после изменений `catalog/content.json`; `--check` проверяет актуальность.
|
||||
- `python3 tools/gen_catalog.py` — обновить Rust-константы после изменений `catalog/content.json`, `catalog/specs.json` или `catalog/sounds.json`; `--check` проверяет актуальность.
|
||||
- `python3 tools/gen_models.py` — перегенерировать.
|
||||
- `python3 tools/gen_models.py --check` — только сравнить, выход 1 при различиях.
|
||||
|
||||
`catalog/specs.json` — единственный источник числовых профилей симуляции: строки `DroneSpec`, `MissileSpec`/`MissileKind` (включая RCS, seeker, звук пуска и голос мотора), `SamSpec` (оружие, части моделей, приводы, датчик), общие константы (гравитация, высота раскрытия «Орешника», логистика) и именованные наборы частей. `tools/gen_catalog.py` создаёт из него `airdef-mod/src/sim/specs_generated.rs` (включается в `mod.rs`; тот же файл компилирует `airdef-core`) и `airdef-mod/src/sim/unit_specs_generated.rs` (включается в `units.rs`). Эти два файла не редактировать вручную. `tools/airdef_assets/catalog.py::load_specs` проверяет схему, звуковые ссылки, наборы kinds против `content.json`, ссылки на именованные части и заполненность всех строк; `gen_catalog.py --check` в CI ловит расхождение. Значения — игровой масштаб, не реальные характеристики техники.
|
||||
|
||||
`catalog/sounds.json` хранит звуковые ID, дальности и наличие дальнего варианта; `tools/gen_catalog.py` экспортирует ID для Rust, а `tools/gen_sounds.py` создаёт `airdef-mod/assets/airdef/sounds/*.ogg` и `sounds.json`. Этот генератор не использует manifest и не входит в проверку `gen_models.py`.
|
||||
|
||||
## Архив: `legacy/`
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
"""Shared content identities and item presentation used by runtime and assets."""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PATH = Path(__file__).resolve().parents[2] / "catalog" / "content.json"
|
||||
SPECS = PATH.parent / "specs.json"
|
||||
|
||||
FAMILIES = {"trainable", "gun", "turret", "vertical", "static", "truck", "light"}
|
||||
CLASSES = {"drone", "jet", "cruise", "supersonic", "ballistic"}
|
||||
MUZZLES = {"none", "patriot", "pantsir", "grid", "rails", "cells"}
|
||||
ENGINES = {"prop": {"type", "back", "size"}, "jet": {"type", "up", "back", "size"},
|
||||
"rocket": {"type", "back", "size"}}
|
||||
MODEL = r"airdef:[a-z0-9_]+"
|
||||
|
||||
|
||||
def load():
|
||||
@@ -27,8 +37,6 @@ def load():
|
||||
|
||||
def load_sounds():
|
||||
"""Load stable event IDs and engine attenuation metadata."""
|
||||
import re
|
||||
|
||||
entries = json.loads((PATH.parent / "sounds.json").read_text(encoding="utf-8"))
|
||||
seen = set()
|
||||
for entry in entries:
|
||||
@@ -47,6 +55,317 @@ def load_sounds():
|
||||
return entries
|
||||
|
||||
|
||||
def _exact(entry, keys, where):
|
||||
if not isinstance(entry, dict) or set(entry) != set(keys):
|
||||
got = sorted(entry) if isinstance(entry, dict) else type(entry).__name__
|
||||
raise ValueError(f"{where}: expected keys {sorted(keys)}, got {got}")
|
||||
|
||||
|
||||
def _number(value, where, minimum=None):
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
||||
raise ValueError(f"{where}: expected a finite number, got {value!r}")
|
||||
if minimum is not None and value < minimum:
|
||||
raise ValueError(f"{where}: {value!r} is below {minimum}")
|
||||
return value
|
||||
|
||||
|
||||
def _integer(value, where, minimum=0):
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
||||
raise ValueError(f"{where}: expected an integer >= {minimum}, got {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value, where, pattern=None):
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"{where}: expected a non-empty string")
|
||||
if pattern and not re.fullmatch(pattern, value):
|
||||
raise ValueError(f"{where}: {value!r} does not match {pattern}")
|
||||
return value
|
||||
|
||||
|
||||
def _sound(value, where, events):
|
||||
_text(value, where, r"[a-z0-9_]+\.[a-z0-9_]+")
|
||||
if value not in events:
|
||||
raise ValueError(f"{where}: sound event {value!r} is not registered in sounds.json")
|
||||
|
||||
|
||||
def _named_ref(value, where, named, kind):
|
||||
_exact(value, {"ref"}, where)
|
||||
name = value["ref"]
|
||||
if name not in named or named[name]["kind"] != kind:
|
||||
raise ValueError(f"{where}: unknown {kind} reference {name!r}")
|
||||
return name
|
||||
|
||||
|
||||
def _model_ref(value, where, named, kind):
|
||||
return f"NAMED_{_named_ref(value, where, named, kind).upper()}"
|
||||
|
||||
|
||||
def _parts(entry, where, named):
|
||||
_exact(entry, {"body", "launcher", "pack", "dish", "fixed", "spinner"}, where)
|
||||
_text(entry["body"], f"{where}.body", MODEL)
|
||||
for slot in ("launcher", "pack"):
|
||||
if entry[slot] is not None:
|
||||
_text(entry[slot], f"{where}.{slot}", MODEL)
|
||||
dish = entry["dish"]
|
||||
if dish is not None:
|
||||
if not isinstance(dish, list) or len(dish) != 4:
|
||||
raise ValueError(f"{where}.dish: expected [model, height, forward, tilt]")
|
||||
_text(dish[0], f"{where}.dish.model", MODEL)
|
||||
for i, v in enumerate(dish[1:], 1):
|
||||
_number(v, f"{where}.dish[{i}]")
|
||||
fixed = entry["fixed"]
|
||||
if isinstance(fixed, dict):
|
||||
_named_ref(fixed, f"{where}.fixed", named, "fixed")
|
||||
elif not isinstance(fixed, list):
|
||||
raise ValueError(f"{where}.fixed: expected a list or a named reference")
|
||||
else:
|
||||
for i, piece in enumerate(fixed):
|
||||
if not isinstance(piece, list) or len(piece) != 3:
|
||||
raise ValueError(f"{where}.fixed[{i}]: expected [model, height, back]")
|
||||
_text(piece[0], f"{where}.fixed[{i}].model", MODEL)
|
||||
_number(piece[1], f"{where}.fixed[{i}].height")
|
||||
_number(piece[2], f"{where}.fixed[{i}].back")
|
||||
spinner = entry["spinner"]
|
||||
if isinstance(spinner, dict):
|
||||
_named_ref(spinner, f"{where}.spinner", named, "spinner")
|
||||
elif spinner is not None:
|
||||
if not isinstance(spinner, list) or len(spinner) != 5:
|
||||
raise ValueError(f"{where}.spinner: expected [model, height, back, size, tilt]")
|
||||
_text(spinner[0], f"{where}.spinner.model", MODEL)
|
||||
for i, v in enumerate(spinner[1:], 1):
|
||||
_number(v, f"{where}.spinner[{i}]")
|
||||
|
||||
|
||||
def _muzzle(entry, where):
|
||||
if not isinstance(entry, dict) or entry.get("type") not in MUZZLES:
|
||||
raise ValueError(f"{where}: expected one of {sorted(MUZZLES)}")
|
||||
kind = entry["type"]
|
||||
keys = {"grid": {"type", "cols", "rows", "size"}, "rails": {"type", "xs"}}.get(kind, {"type"})
|
||||
_exact(entry, keys, where)
|
||||
if kind == "grid":
|
||||
_integer(entry["cols"], f"{where}.cols", 1)
|
||||
_integer(entry["rows"], f"{where}.rows", 1)
|
||||
_number(entry["size"], f"{where}.size", 0)
|
||||
elif kind == "rails":
|
||||
xs = entry["xs"]
|
||||
if not isinstance(xs, list) or not xs:
|
||||
raise ValueError(f"{where}.xs: expected a non-empty list")
|
||||
for i, v in enumerate(xs):
|
||||
_number(v, f"{where}.xs[{i}]")
|
||||
|
||||
|
||||
def _rest_pitch(entry, where):
|
||||
if isinstance(entry, dict):
|
||||
_exact(entry, {"half_pi"}, where)
|
||||
_number(entry["half_pi"], f"{where}.half_pi")
|
||||
else:
|
||||
_number(entry, where)
|
||||
|
||||
|
||||
def load_specs():
|
||||
"""Load the numeric profiles shared by Rust and Python, validating every reference."""
|
||||
data = json.loads(SPECS.read_text(encoding="utf-8"))
|
||||
_exact(data, {"shared", "drones", "units", "missiles", "named"}, "specs.json")
|
||||
events = {entry["event"] for entry in load_sounds()}
|
||||
content = load()
|
||||
|
||||
shared = data["shared"]
|
||||
_exact(shared, {"mobility", "flight", "logistics"}, "shared")
|
||||
_exact(shared["mobility"], {"drive_scale", "drive_ramp", "trainable", "vertical", "other"}, "shared.mobility")
|
||||
_number(shared["mobility"]["drive_scale"], "shared.mobility.drive_scale", 0)
|
||||
_number(shared["mobility"]["drive_ramp"], "shared.mobility.drive_ramp", 0)
|
||||
for family in ("trainable", "vertical", "other"):
|
||||
axes = shared["mobility"][family]
|
||||
if not isinstance(axes, list) or len(axes) != 2:
|
||||
raise ValueError(f"shared.mobility.{family}: expected [yaw, pitch]")
|
||||
for i, v in enumerate(axes):
|
||||
_number(v, f"shared.mobility.{family}[{i}]")
|
||||
_exact(shared["flight"], {"gravity", "block_split"}, "shared.flight")
|
||||
_number(shared["flight"]["gravity"], "shared.flight.gravity", 0)
|
||||
_number(shared["flight"]["block_split"], "shared.flight.block_split", 0)
|
||||
_exact(shared["logistics"], {"depot_reach", "handover", "truck_load", "stock_limit"}, "shared.logistics")
|
||||
_number(shared["logistics"]["depot_reach"], "shared.logistics.depot_reach", 0)
|
||||
_number(shared["logistics"]["handover"], "shared.logistics.handover", 0)
|
||||
_integer(shared["logistics"]["truck_load"], "shared.logistics.truck_load", 1)
|
||||
_integer(shared["logistics"]["stock_limit"], "shared.logistics.stock_limit", 1)
|
||||
|
||||
named = data["named"]
|
||||
if not isinstance(named, dict):
|
||||
raise ValueError("named: expected an object")
|
||||
referenced = set()
|
||||
for name, entry in named.items():
|
||||
where = f"named.{name}"
|
||||
_exact(entry, {"kind", "value"}, where)
|
||||
value = entry["value"]
|
||||
if not isinstance(value, list):
|
||||
raise ValueError(f"{where}.value: expected a list")
|
||||
if entry["kind"] == "fixed":
|
||||
for i, piece in enumerate(value):
|
||||
if not isinstance(piece, list) or len(piece) != 3:
|
||||
raise ValueError(f"{where}.value[{i}]: expected [model, height, back]")
|
||||
_text(piece[0], f"{where}.value[{i}].model", MODEL)
|
||||
_number(piece[1], f"{where}.value[{i}].height")
|
||||
_number(piece[2], f"{where}.value[{i}].back")
|
||||
elif entry["kind"] == "spinner":
|
||||
if len(value) != 5:
|
||||
raise ValueError(f"{where}: expected [model, height, back, size, tilt]")
|
||||
_text(value[0], f"{where}.model", MODEL)
|
||||
for i, v in enumerate(value[1:], 1):
|
||||
_number(v, f"{where}[{i}]")
|
||||
elif entry["kind"] == "pivot":
|
||||
if len(value) != 3:
|
||||
raise ValueError(f"{where}: expected [x, y, z]")
|
||||
for i, v in enumerate(value):
|
||||
_number(v, f"{where}[{i}]")
|
||||
else:
|
||||
raise ValueError(f"{where}.kind: expected fixed, spinner or pivot")
|
||||
|
||||
missile_kinds = set()
|
||||
for i, entry in enumerate(data["missiles"]):
|
||||
where = f"missiles[{i}]"
|
||||
_exact(entry, {"kind", "model", "speed", "accel", "turn", "boost", "cold", "pk", "fuse",
|
||||
"life", "scale", "flame", "rear", "seeker", "launch_sound", "voice"}, where)
|
||||
kind = _text(entry["kind"], f"{where}.kind", r"[A-Z][A-Za-z0-9]*")
|
||||
if kind in missile_kinds:
|
||||
raise ValueError(f"{where}: duplicate missile kind {kind}")
|
||||
missile_kinds.add(kind)
|
||||
_text(entry["model"], f"{where}.model", MODEL)
|
||||
for field in ("speed", "accel", "turn", "pk", "fuse", "scale", "flame", "rear", "seeker"):
|
||||
_number(entry[field], f"{where}.{field}", 0)
|
||||
_integer(entry["boost"], f"{where}.boost")
|
||||
_integer(entry["cold"], f"{where}.cold")
|
||||
_integer(entry["life"], f"{where}.life", 1)
|
||||
_sound(entry["launch_sound"], f"{where}.launch_sound", events)
|
||||
_exact(entry["voice"], {"event", "gain", "pitch"}, f"{where}.voice")
|
||||
_sound(entry["voice"]["event"], f"{where}.voice.event", events)
|
||||
_number(entry["voice"]["gain"], f"{where}.voice.gain", 0)
|
||||
_number(entry["voice"]["pitch"], f"{where}.voice.pitch", 0)
|
||||
|
||||
drone_kinds = []
|
||||
keys = set()
|
||||
for i, entry in enumerate(data["drones"]):
|
||||
where = f"drones[{i}]"
|
||||
_exact(entry, {"kind", "key", "name", "model", "speed", "turn", "cruise", "clearance", "dive",
|
||||
"warhead", "scale", "engine", "decoy", "stealth", "sound", "class", "ballistic",
|
||||
"rcs", "launch_sound"}, where)
|
||||
kind = _text(entry["kind"], f"{where}.kind", r"[A-Z][A-Za-z0-9]*")
|
||||
drone_kinds.append(kind)
|
||||
key = _text(entry["key"], f"{where}.key", r"[a-z0-9_]+")
|
||||
if key in keys:
|
||||
raise ValueError(f"{where}: duplicate key {key}")
|
||||
keys.add(key)
|
||||
_text(entry["name"], f"{where}.name")
|
||||
_text(entry["model"], f"{where}.model", MODEL)
|
||||
for field in ("speed", "turn", "cruise", "clearance", "dive", "scale", "stealth", "rcs"):
|
||||
_number(entry[field], f"{where}.{field}", 0)
|
||||
_integer(entry["warhead"], f"{where}.warhead")
|
||||
if not isinstance(entry["decoy"], bool):
|
||||
raise ValueError(f"{where}.decoy: expected a boolean")
|
||||
engine = entry["engine"]
|
||||
if not isinstance(engine, dict) or engine.get("type") not in ENGINES:
|
||||
raise ValueError(f"{where}.engine: expected one of {sorted(ENGINES)}")
|
||||
_exact(engine, ENGINES[engine["type"]], f"{where}.engine")
|
||||
for field in ENGINES[engine["type"]] - {"type"}:
|
||||
_number(engine[field], f"{where}.engine.{field}")
|
||||
_exact(entry["sound"], {"event", "pitch", "interval"}, f"{where}.sound")
|
||||
_sound(entry["sound"]["event"], f"{where}.sound.event", events)
|
||||
_number(entry["sound"]["pitch"], f"{where}.sound.pitch", 0)
|
||||
_integer(entry["sound"]["interval"], f"{where}.sound.interval", 1)
|
||||
if entry["class"] not in CLASSES:
|
||||
raise ValueError(f"{where}.class: expected one of {sorted(CLASSES)}")
|
||||
if entry["ballistic"] is not None:
|
||||
_number(entry["ballistic"], f"{where}.ballistic", 0)
|
||||
_sound(entry["launch_sound"], f"{where}.launch_sound", events)
|
||||
|
||||
unit_kinds = []
|
||||
used_missiles = set()
|
||||
armed = {"Patriot", "Gepard", "Pantsir", "S400", "S500", "IrisT", "Nasams", "Buk", "Tor", "P1sun"}
|
||||
for i, entry in enumerate(data["units"]):
|
||||
where = f"units[{i}]"
|
||||
_exact(entry, {"kind", "key", "name", "missile", "gun", "reload", "scale", "rest_pitch",
|
||||
"roles", "family", "parts", "mount", "pivot", "muzzle", "sensor", "mast"}, where)
|
||||
kind = _text(entry["kind"], f"{where}.kind", r"[A-Z][A-Za-z0-9]*")
|
||||
unit_kinds.append(kind)
|
||||
key = _text(entry["key"], f"{where}.key", r"[a-z0-9_]+")
|
||||
if key in keys:
|
||||
raise ValueError(f"{where}: duplicate key {key}")
|
||||
keys.add(key)
|
||||
_text(entry["name"], f"{where}.name")
|
||||
_integer(entry["reload"], f"{where}.reload")
|
||||
_number(entry["scale"], f"{where}.scale", 0)
|
||||
_rest_pitch(entry["rest_pitch"], f"{where}.rest_pitch")
|
||||
_integer(entry["roles"], f"{where}.roles")
|
||||
if entry["roles"] > 0x1F:
|
||||
raise ValueError(f"{where}.roles: mask out of range")
|
||||
if entry["family"] not in FAMILIES:
|
||||
raise ValueError(f"{where}.family: expected one of {sorted(FAMILIES)}")
|
||||
_parts(entry["parts"], f"{where}.parts", named)
|
||||
if not isinstance(entry["mount"], list) or len(entry["mount"]) != 2:
|
||||
raise ValueError(f"{where}.mount: expected [up, forward]")
|
||||
for j, v in enumerate(entry["mount"]):
|
||||
_number(v, f"{where}.mount[{j}]")
|
||||
pivot = entry["pivot"]
|
||||
if isinstance(pivot, dict):
|
||||
_model_ref(pivot, f"{where}.pivot", named, "pivot")
|
||||
elif not isinstance(pivot, list) or len(pivot) != 3:
|
||||
raise ValueError(f"{where}.pivot: expected [x, y, z] or a named reference")
|
||||
else:
|
||||
for j, v in enumerate(pivot):
|
||||
_number(v, f"{where}.pivot[{j}]")
|
||||
_muzzle(entry["muzzle"], f"{where}.muzzle")
|
||||
_number(entry["sensor"], f"{where}.sensor", 0)
|
||||
_number(entry["mast"], f"{where}.mast", 0)
|
||||
missile = entry["missile"]
|
||||
if missile is not None:
|
||||
_exact(missile, {"kind", "magazine", "min", "max", "interval"}, f"{where}.missile")
|
||||
used_missiles.add(_text(missile["kind"], f"{where}.missile.kind", r"[A-Z][A-Za-z0-9]*"))
|
||||
_integer(missile["magazine"], f"{where}.missile.magazine", 1)
|
||||
_number(missile["min"], f"{where}.missile.min", 0)
|
||||
_number(missile["max"], f"{where}.missile.max", 0)
|
||||
_integer(missile["interval"], f"{where}.missile.interval", 1)
|
||||
gun = entry["gun"]
|
||||
if gun is not None:
|
||||
_exact(gun, {"range", "track", "p", "bursts", "shell", "slew"}, f"{where}.gun")
|
||||
for field in ("range", "track", "p", "shell"):
|
||||
_number(gun[field], f"{where}.gun.{field}", 0)
|
||||
_integer(gun["bursts"], f"{where}.gun.bursts", 1)
|
||||
if not isinstance(gun["slew"], list) or len(gun["slew"]) != 2:
|
||||
raise ValueError(f"{where}.gun.slew: expected [yaw, pitch]")
|
||||
for j, v in enumerate(gun["slew"]):
|
||||
_number(v, f"{where}.gun.slew[{j}]", 0)
|
||||
if kind in armed and missile is None and gun is None:
|
||||
raise ValueError(f"{where}: an armed unit needs missiles or a gun")
|
||||
if kind not in armed and (missile is not None or gun is not None):
|
||||
raise ValueError(f"{where}: a support unit has no weapons")
|
||||
|
||||
if set(drone_kinds) != {entry["kind"] for entry in content["aircraft"]}:
|
||||
raise ValueError("specs.json drones and content.json aircraft disagree")
|
||||
if len(drone_kinds) != len(content["aircraft"]):
|
||||
raise ValueError("specs.json drones and content.json aircraft differ in count")
|
||||
if set(unit_kinds) != {entry["kind"] for entry in content["units"]}:
|
||||
raise ValueError("specs.json units and content.json units disagree")
|
||||
if len(unit_kinds) != len(content["units"]):
|
||||
raise ValueError("specs.json units and content.json units differ in count")
|
||||
missing = used_missiles - missile_kinds
|
||||
if missing:
|
||||
raise ValueError(f"units reference missiles without a profile: {sorted(missing)}")
|
||||
unused = missile_kinds - used_missiles
|
||||
if unused:
|
||||
raise ValueError(f"missile profiles are not carried by any unit: {sorted(unused)}")
|
||||
for entry in data["units"]:
|
||||
parts = entry["parts"]
|
||||
for slot in ("fixed", "spinner"):
|
||||
if isinstance(parts[slot], dict):
|
||||
referenced.add(parts[slot]["ref"])
|
||||
if isinstance(entry["pivot"], dict):
|
||||
referenced.add(entry["pivot"]["ref"])
|
||||
idle = set(named) - referenced
|
||||
if idle:
|
||||
raise ValueError(f"named part profiles are not referenced: {sorted(idle)}")
|
||||
return data
|
||||
|
||||
|
||||
def sound_events(entries):
|
||||
"""Build the Minecraft sound event registry without generating audio."""
|
||||
events = {}
|
||||
@@ -56,4 +375,4 @@ def sound_events(entries):
|
||||
events[event] = {"sounds": [{"name": f"airdef:{name}", "attenuation_distance": distance}]}
|
||||
if entry["far"]:
|
||||
events[event + "_far"] = {"sounds": [{"name": f"airdef:{name}_far", "attenuation_distance": int(distance * 1.6)}]}
|
||||
return events
|
||||
return events
|
||||
+268
-2
@@ -4,9 +4,17 @@ import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from airdef_assets.catalog import load, load_sounds
|
||||
from airdef_assets.catalog import load, load_specs, load_sounds
|
||||
|
||||
OUTPUT = Path(__file__).resolve().parents[1] / "airdef-mod/src/sim/catalog_entries.rs"
|
||||
SPEC_OUTPUT = OUTPUT.with_name("specs_generated.rs")
|
||||
UNIT_SPEC_OUTPUT = OUTPUT.with_name("unit_specs_generated.rs")
|
||||
|
||||
CLASSES = {"drone": "Drone", "jet": "Jet", "cruise": "Cruise", "supersonic": "Supersonic",
|
||||
"ballistic": "Ballistic"}
|
||||
FAMILIES = {"trainable": "Trainable", "gun": "Gun", "turret": "Turret", "vertical": "Vertical",
|
||||
"static": "Static", "truck": "Truck", "light": "Light"}
|
||||
SOUND_PREFIX = "sound::"
|
||||
|
||||
|
||||
def generate(data):
|
||||
@@ -40,12 +48,270 @@ def generate_sound_ids(entries):
|
||||
return "\n".join(lines + [""])
|
||||
|
||||
|
||||
def num(value):
|
||||
"""A Rust float literal that round-trips the catalog value exactly."""
|
||||
value = float(value)
|
||||
text = repr(value)
|
||||
return text.replace("e+", "e")
|
||||
|
||||
|
||||
def integer(value):
|
||||
return str(int(value))
|
||||
|
||||
|
||||
def text(value):
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def sound_ref(event):
|
||||
return SOUND_PREFIX + event.replace(".", "_").upper()
|
||||
|
||||
|
||||
def parts_value(parts, indent):
|
||||
pad = " " * indent
|
||||
lines = [pad + "parts: Parts {"]
|
||||
inner = " " * (indent + 4)
|
||||
lines.append(f"{inner}body: {text(parts['body'])},")
|
||||
for slot in ("launcher", "pack"):
|
||||
value = parts[slot]
|
||||
lines.append(f"{inner}{slot}: " + (f"Some({text(value)})" if value is not None else "None") + ",")
|
||||
dish = parts["dish"]
|
||||
if dish is None:
|
||||
lines.append(f"{inner}dish: None,")
|
||||
else:
|
||||
lines.append(f"{inner}dish: Some(({text(dish[0])}, {', '.join(num(v) for v in dish[1:])})),")
|
||||
fixed = parts["fixed"]
|
||||
if isinstance(fixed, dict):
|
||||
lines.append(f"{inner}fixed: NAMED_{fixed['ref'].upper()},")
|
||||
else:
|
||||
pieces = ", ".join(f"({text(piece[0])}, {num(piece[1])}, {num(piece[2])})" for piece in fixed)
|
||||
lines.append(f"{inner}fixed: &[{pieces}],")
|
||||
spinner = parts["spinner"]
|
||||
if isinstance(spinner, dict):
|
||||
lines.append(f"{inner}spinner: NAMED_{spinner['ref'].upper()},")
|
||||
elif spinner is None:
|
||||
lines.append(f"{inner}spinner: None,")
|
||||
else:
|
||||
lines.append(f"{inner}spinner: Some(({text(spinner[0])}, {', '.join(num(v) for v in spinner[1:])})),")
|
||||
lines.append(pad + "},")
|
||||
return lines
|
||||
|
||||
|
||||
def pivot_value(pivot):
|
||||
if isinstance(pivot, dict):
|
||||
return f"NAMED_{pivot['ref'].upper()}"
|
||||
if all(float(v) == 0.0 for v in pivot):
|
||||
return "DVec3::ZERO"
|
||||
return f"DVec3::new({', '.join(num(v) for v in pivot)})"
|
||||
|
||||
|
||||
def muzzle_value(muzzle):
|
||||
kind = muzzle["type"]
|
||||
if kind == "grid":
|
||||
return f"Muzzle::Grid({muzzle['cols']}, {muzzle['rows']}, {num(muzzle['size'])})"
|
||||
if kind == "rails":
|
||||
return f"Muzzle::Rails(&[{', '.join(num(v) for v in muzzle['xs'])}])"
|
||||
return "Muzzle::" + {"none": "None", "patriot": "Patriot", "pantsir": "Pantsir", "cells": "Cells"}[kind]
|
||||
|
||||
|
||||
def rest_pitch_value(pitch):
|
||||
if isinstance(pitch, dict):
|
||||
offset = float(pitch["half_pi"])
|
||||
if offset == 0.0:
|
||||
return "HALF_PI"
|
||||
if offset < 0.0:
|
||||
return f"HALF_PI - {num(-offset)}"
|
||||
return f"HALF_PI + {num(offset)}"
|
||||
return num(pitch)
|
||||
|
||||
|
||||
def drone_profile(entry):
|
||||
engine = entry["engine"]
|
||||
if engine["type"] == "prop":
|
||||
motor = f"Engine::Prop({num(engine['back'])}, {num(engine['size'])})"
|
||||
elif engine["type"] == "jet":
|
||||
motor = f"Engine::Jet({num(engine['up'])}, {num(engine['back'])}, {num(engine['size'])})"
|
||||
else:
|
||||
motor = f"Engine::Rocket({num(engine['back'])}, {num(engine['size'])})"
|
||||
sound = entry["sound"]
|
||||
ballistic = "None" if entry["ballistic"] is None else f"Some({num(entry['ballistic'])})"
|
||||
lines = [f"const DRONE_SPEC_{entry['kind'].upper()}: DroneSpec = DroneSpec {{",
|
||||
f" name: {text(entry['name'])},",
|
||||
f" key: {text(entry['key'])},",
|
||||
f" model: {text(entry['model'])},",
|
||||
f" speed: {num(entry['speed'])},",
|
||||
f" turn: {num(entry['turn'])},",
|
||||
f" cruise: {num(entry['cruise'])},",
|
||||
f" clearance: {num(entry['clearance'])},",
|
||||
f" dive: {num(entry['dive'])},",
|
||||
f" warhead: {integer(entry['warhead'])},",
|
||||
f" scale: {num(entry['scale'])},",
|
||||
f" engine: {motor},",
|
||||
f" decoy: {str(entry['decoy']).lower()},",
|
||||
f" stealth: {num(entry['stealth'])},",
|
||||
f" rcs: {num(entry['rcs'])},",
|
||||
f" sound: ({sound_ref(sound['event'])}, {num(sound['pitch'])}, {integer(sound['interval'])}),",
|
||||
f" class: Class::{CLASSES[entry['class']]},",
|
||||
f" ballistic: {ballistic},",
|
||||
f" launch_sound: {sound_ref(entry['launch_sound'])},",
|
||||
"};"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def missile_profile(entry):
|
||||
voice = entry["voice"]
|
||||
lines = [f"const MISSILE_SPEC_{entry['kind'].upper()}: MissileSpec = MissileSpec {{",
|
||||
f" model: {text(entry['model'])},",
|
||||
f" speed: {num(entry['speed'])},",
|
||||
f" accel: {num(entry['accel'])},",
|
||||
f" turn: {num(entry['turn'])},",
|
||||
f" boost: {integer(entry['boost'])},",
|
||||
f" cold: {integer(entry['cold'])},",
|
||||
f" pk: {num(entry['pk'])},",
|
||||
f" fuse: {num(entry['fuse'])},",
|
||||
f" life: {integer(entry['life'])},",
|
||||
f" scale: {num(entry['scale'])},",
|
||||
f" flame: {num(entry['flame'])},",
|
||||
f" rear: {num(entry['rear'])},",
|
||||
f" seeker: {num(entry['seeker'])},",
|
||||
f" launch_sound: {sound_ref(entry['launch_sound'])},",
|
||||
f" voice: ({sound_ref(voice['event'])}, {num(voice['gain'])}, {num(voice['pitch'])}),",
|
||||
"};"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def unit_profile(entry):
|
||||
missile = entry["missile"]
|
||||
gun = entry["gun"]
|
||||
if missile is None:
|
||||
missile_expr = "None"
|
||||
else:
|
||||
missile_expr = (f"Some(MissileLoad {{ kind: MissileKind::{missile['kind']}, magazine: {integer(missile['magazine'])}, "
|
||||
f"min: {num(missile['min'])}, max: {num(missile['max'])}, interval: {integer(missile['interval'])} }})")
|
||||
if gun is None:
|
||||
gun_expr = "None"
|
||||
else:
|
||||
gun_expr = (f"Some(GunSpec {{ range: {num(gun['range'])}, track: {num(gun['track'])}, p: {num(gun['p'])}, "
|
||||
f"bursts: {integer(gun['bursts'])}, shell: {num(gun['shell'])}, "
|
||||
f"slew: ({num(gun['slew'][0])}, {num(gun['slew'][1])}) }})")
|
||||
lines = [f"const UNIT_SPEC_{entry['kind'].upper()}: SamSpec = SamSpec {{",
|
||||
f" name: {text(entry['name'])},",
|
||||
f" key: {text(entry['key'])},",
|
||||
f" missile: {missile_expr},",
|
||||
f" gun: {gun_expr},",
|
||||
f" reload: {integer(entry['reload'])},",
|
||||
f" scale: {num(entry['scale'])},",
|
||||
f" rest_pitch: {rest_pitch_value(entry['rest_pitch'])},",
|
||||
f" roles: {integer(entry['roles'])},",
|
||||
f" family: Family::{FAMILIES[entry['family']]},"]
|
||||
lines += parts_value(entry["parts"], 4)
|
||||
lines += [f" mount: ({num(entry['mount'][0])}, {num(entry['mount'][1])}),",
|
||||
f" pivot: {pivot_value(entry['pivot'])},",
|
||||
f" muzzle: {muzzle_value(entry['muzzle'])},",
|
||||
f" sensor: {num(entry['sensor'])},",
|
||||
f" mast: {num(entry['mast'])},",
|
||||
"};"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_specs(data):
|
||||
"""The simulation's numeric rows, generated for both the mod and airdef-core."""
|
||||
shared = data["shared"]
|
||||
lines = ["// Generated by tools/gen_catalog.py from catalog/specs.json. Do not edit.",
|
||||
"// Game-scale values, not real weapon data; the catalog is the only copy.",
|
||||
"",
|
||||
"/// Share of the old slew speed a launcher keeps when it has no rate of its own.",
|
||||
f"const DRIVE_SCALE: f64 = {num(shared['mobility']['drive_scale'])};",
|
||||
"/// Ticks a drive takes to reach its top speed (and to stop from it).",
|
||||
f"const DRIVE_RAMP: f64 = {num(shared['mobility']['drive_ramp'])};"]
|
||||
for family in ("trainable", "vertical", "other"):
|
||||
axes = shared["mobility"][family]
|
||||
lines.append(f"const DRIVE_{family.upper()}: (f64, f64) = ({num(axes[0])}, {num(axes[1])});")
|
||||
lines += ["/// Ballistic flight's gravity, blocks a tick².",
|
||||
f"const GRAVITY: f64 = {num(shared['flight']['gravity'])};",
|
||||
"/// Height over the target where the Oreshnik's blocks release their submunitions.",
|
||||
f"const BLOCK_SPLIT: f64 = {num(shared['flight']['block_split'])};",
|
||||
"/// How near a depot a system reloads straight from it.",
|
||||
f"const DEPOT_REACH: f64 = {num(shared['logistics']['depot_reach'])};",
|
||||
"/// How near a truck must come to hand a reload over, or to load up at a depot.",
|
||||
f"const HANDOVER: f64 = {num(shared['logistics']['handover'])};",
|
||||
"/// What a truck carries.",
|
||||
f"const TRUCK_LOAD: u32 = {integer(shared['logistics']['truck_load'])};",
|
||||
f"const STOCK_LIMIT: u32 = {integer(shared['logistics']['stock_limit'])};",
|
||||
""]
|
||||
for entry in data["drones"]:
|
||||
lines += [drone_profile(entry), ""]
|
||||
for entry in data["missiles"]:
|
||||
lines += [missile_profile(entry), ""]
|
||||
lines += ["impl DroneKind {",
|
||||
" /// Its row of numbers from `catalog/specs.json`.",
|
||||
" pub fn spec(self) -> &'static DroneSpec {",
|
||||
" match self {"]
|
||||
for entry in data["drones"]:
|
||||
lines.append(f" DroneKind::{entry['kind']} => &DRONE_SPEC_{entry['kind'].upper()},")
|
||||
lines += [" }", " }", "}", "",
|
||||
"impl MissileKind {",
|
||||
" fn spec(self) -> &'static MissileSpec {",
|
||||
" match self {"]
|
||||
for entry in data["missiles"]:
|
||||
lines.append(f" MissileKind::{entry['kind']} => &MISSILE_SPEC_{entry['kind'].upper()},")
|
||||
lines += [" }", " }", "}", "",
|
||||
"/// Save keys by kind; a rename needs an explicit migration.",
|
||||
"#[cfg(test)]",
|
||||
f"pub(super) const DRONE_SPEC_KEYS: [(DroneKind, &str); {len(data['drones'])}] = ["]
|
||||
for entry in data["drones"]:
|
||||
lines.append(f" (DroneKind::{entry['kind']}, {text(entry['key'])}),")
|
||||
lines += ["];", "",
|
||||
"#[cfg(test)]",
|
||||
f"pub(super) const MISSILE_SPEC_MODELS: [(MissileKind, &str); {len(data['missiles'])}] = ["]
|
||||
for entry in data["missiles"]:
|
||||
lines.append(f" (MissileKind::{entry['kind']}, {text(entry['model'])}),")
|
||||
return "\n".join(lines + ["];", ""])
|
||||
|
||||
|
||||
def generate_unit_specs(data):
|
||||
"""The deployed units' rows, generated where the unit types live."""
|
||||
lines = ["// Generated by tools/gen_catalog.py from catalog/specs.json. Do not edit.",
|
||||
"// Game-scale values, not real weapon data; the catalog is the only copy.",
|
||||
""]
|
||||
for name, entry in data["named"].items():
|
||||
value = entry["value"]
|
||||
if entry["kind"] == "fixed":
|
||||
pieces = ", ".join(f"({text(piece[0])}, {num(piece[1])}, {num(piece[2])})" for piece in value)
|
||||
lines.append(f"const NAMED_{name.upper()}: &[(&str, f64, f64)] = &[{pieces}];")
|
||||
elif entry["kind"] == "spinner":
|
||||
lines.append(f"const NAMED_{name.upper()}: Option<(&str, f64, f64, f64, f64)> = "
|
||||
f"Some(({text(value[0])}, {', '.join(num(v) for v in value[1:])}));")
|
||||
else:
|
||||
lines.append(f"const NAMED_{name.upper()}: DVec3 = DVec3::new({', '.join(num(v) for v in value)});")
|
||||
lines.append("")
|
||||
for entry in data["units"]:
|
||||
lines += [unit_profile(entry), ""]
|
||||
lines += ["impl SamKind {",
|
||||
" /// Its row of numbers from `catalog/specs.json`.",
|
||||
" pub fn spec(self) -> &'static SamSpec {",
|
||||
" match self {"]
|
||||
for entry in data["units"]:
|
||||
lines.append(f" SamKind::{entry['kind']} => &UNIT_SPEC_{entry['kind'].upper()},")
|
||||
lines += [" }", " }", "}", "",
|
||||
"/// Save keys by kind; a rename needs an explicit migration.",
|
||||
"#[cfg(test)]",
|
||||
f"pub(super) const UNIT_SPEC_KEYS: [(SamKind, &str); {len(data['units'])}] = ["]
|
||||
for entry in data["units"]:
|
||||
lines.append(f" (SamKind::{entry['kind']}, {text(entry['key'])}),")
|
||||
return "\n".join(lines + ["];", ""])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
sound_output = OUTPUT.with_name("catalog_sound_ids.rs")
|
||||
outputs = ((OUTPUT, generate(load())), (sound_output, generate_sound_ids(load_sounds())))
|
||||
specs = load_specs()
|
||||
outputs = ((OUTPUT, generate(load())),
|
||||
(sound_output, generate_sound_ids(load_sounds())),
|
||||
(SPEC_OUTPUT, generate_specs(specs)),
|
||||
(UNIT_SPEC_OUTPUT, generate_unit_specs(specs)))
|
||||
for path, generated in outputs:
|
||||
if args.check:
|
||||
if not path.exists() or path.read_text(encoding="utf-8") != generated:
|
||||
|
||||
Reference in New Issue
Block a user