diff --git a/README.md b/README.md index f02ffac..3faaf5d 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ sandbox-сценария, `/airdef ground-load <С-400>` загружает од `/airdef ground-cancel <С-400>` отменяет ещё ожидающую доставку или вывоз. После забора ракета находится в машине и едет на склад. Отмена заявки не возвращает уже вывезенный груз в ПУ. Без склада этот ударный боеприпас сам не появляется. Бук и остальные комплексы такого профиля пока не имеют. +Порядок подготовки, разведка и номера двух списков: [как ударить по ПУ](docs/GROUND-STRIKES.md). **Перемещение.** **Пульт перемещения**: навестись на технику (выбрать), затем на точку на земле — она сворачивается (С-400 и другие с вертикальным пуском дольше, Гепард и Панцирь быстро), едет по рельефу и @@ -204,7 +205,7 @@ API мода `render.set-lights` (до 512, плавно интерполиру - `/airdef lastchance ` — «последний шанс»: комплекс стреляет и по классу вне ролей, если цель вот-вот попадёт (по умолчанию вкл; сохраняется); - `/airdef sites` — известные пусковые площадки (ПУ). РЛС с прямой видимостью может заметить сам пуск и получить неточную позицию; несколько наблюдённых пусков уточняют её. Обнаруженный позже дрон даёт только слабое предположение об обратном курсе: скрытые координаты площадки и время невидимого старта не используются. Повторные треки одного полёта не создают ложную точность. - `/airdef strike <номер ПУ>` — ручной удар по разведанной площадке из готового физического С-400 с отдельно загруженной RM48U (игровой профиль, #118). Четыре ячейки делятся с боезапасом ПВО; приказ резервирует носитель и ячейку, открывает крышку и списывает одну ударную ракету при выбросе. Выбирается готовый комплекс с наибольшим остатком RM48U, затем ближайший к цели. Есть открытие ТПК, выброс, отложенное включение двигателя и разгон, ограниченная игровая траектория, столкновения и запись пуска. Несуществующее «ПВО площадки» не перехватывает ракету по броску вероятности. -- `/airdef ground-stock <номер склада> <количество>` — задать начальный запас RM48U для sandbox-сценария (по умолчанию 0, сам не пополняется). `/airdef ground-load <номер С-400>` — у живого склада в пределах 60 блоков загрузить одну RM48U, сохранив снятую ракету ПВО на складе; `/airdef ground-unload <номер С-400>` — вернуть её и восстановить снятую ракету ПВО, если она осталась. Запас сохраняется с миром. Автоматическая доставка RM48U через ТЗМ пока не реализована. Совместимость профиля основана на [публикации ГУР](https://gur.gov.ua/content/warsanctions-hur-mo-rozkryvaie-budovu-komponentnu-bazu-ta-pidpryiemstva-kooperatsii-z-vyrobnytstva-udarnykh-raket-rm48u.html); это сообщение ведомства, не независимая сертификация. Дальность, форма полёта и оформление условны для Minecraft. +- `/airdef ground-stock <номер склада> <количество>` — задать остаток RM48U для sandbox-сценария (по умолчанию 0). `/airdef ground-load <номер С-400>` загружает одну ракету у склада либо заказывает ТЗМ; `ground-unload` возвращает её у склада либо заказывает вывоз; `ground-cancel` отменяет ожидающую заявку. Доставка и обратный вывоз через ТЗМ реализованы, запас и груз сохраняются. [Пошаговый пример](docs/GROUND-STRIKES.md). Совместимость профиля основана на [публикации ГУР](https://gur.gov.ua/content/warsanctions-hur-mo-rozkryvaie-budovu-komponentnu-bazu-ta-pidpryiemstva-kooperatsii-z-vyrobnytstva-udarnykh-raket-rm48u.html); это сообщение ведомства, не независимая сертификация. Дальность, форма полёта и оформление условны для Minecraft. - `/airdef deploy s400_launcher` и `s500_launcher` — отдельные пусковые без своей РЛС; `radar_search`, `radar_track`, `radar_height` — обзорная, сопровождающая и мачтовая игровые РЛС. Они также доступны через меню N → «Разместить элементы батареи…». `/airdef battery` показывает состав и связь; `/airdef battery <номер> integrated|launcher` меняет конфигурацию, `/airdef radar <номер> multi|search|engagement|height` — роль отдельной РЛС. Обзорная РЛС передаёт подсказки; станция сопровождения должна сама получить свежее измерение, чтобы разрешить пуск. Старые С-400/С-500 сохраняют упрощённую сборную конфигурацию. [Устройство игровой батареи и пуска](docs/BATTERIES-AND-COLD-LAUNCH.md). - `/airdef roles` — показать, по каким классам целей работает каждый комплекс; `/airdef roles <классы|default>` — задать (классы через запятую: `drone`, `jet`, `cruise`, `supersonic`, `ballistic` или `all`), например `/airdef roles s400 cruise,ballistic`; - `/airdef doctrine` — доктрины комплексов (#47; в меню N: «Доктрины…»): `/airdef doctrine <номер|all> reserve ` — держать K ракет для срочных целей (баллистика или до удара меньше 12 с); `salvo eco|normal|heavy` — сколько перехватчиков на цель (только базовые / как обычно / на один больше); `sector <центр°> <полуширина°>` или `sector off` — сектор ответственности (0° — север); `guard <объект№>[,<№>]` или `guard off` — стрелять только по тому, что идёт на охраняемые объекты (отметка: `/airdef target here`). По умолчанию ограничений нет; последний шанс сильнее доктрины; `/airdef why <номер>` называет, какая настройка остановила или разрешила пуск; сохраняется с миром и сценариями. Карта охраны и пробелов в ней — вместе с картой (#48). diff --git a/airdef-mod/src/map.rs b/airdef-mod/src/map.rs index 5edcb9f..899ee09 100644 --- a/airdef-mod/src/map.rs +++ b/airdef-mod/src/map.rs @@ -2340,6 +2340,13 @@ impl AirDef { } lines.push((sam.status(), WHITE)); lines.push((format!("Исправен {:.0}% · {:.0} {:.0}", sam.health * 100.0, sam.pos.x, sam.pos.z), GREY)); + if k == SamKind::Depot { + lines.push((format!("Ударные RM48U: {} · снятые 48Н6: {}", sam.ground_stock, sam.air_48n6_stock), WHITE)); + lines.push((format!("Обычный БК ПВО: {} перезарядок", sam.cargo), WHITE)); + if sam.ground_stock == 0 { lines.push((format!("Начальный запас: /airdef ground-stock {} <число>", n + 1), GREY)); } + } else if k == SamKind::Tzm { + lines.push((format!("Груз RM48U: {} · 48Н6: {}{}", sam.ground_stock, sam.air_48n6_stock, if sam.return_cargo { " · возврат на склад" } else { "" }), WHITE)); + } if k.armed() { if sam.kind == SamKind::S400 { lines.push((format!("Ударных RM48U: {} (общие ячейки с ПВО){}", sam.ground_rounds, if sam.ground_return { " · ожидает вывоза" } else if sam.ground_request { " · ожидает доставки" } else { "" }), WHITE)); } lines.push((format!("Боезапас: {} ракет, {} очередей{}", sam.missiles, sam.bursts, if sam.reloading > 0 { format!(" · перезарядка {} с", sam.reloading / 20) } else { String::new() }), WHITE)); diff --git a/airdef-mod/src/sim/logistics.rs b/airdef-mod/src/sim/logistics.rs index 1b25733..dd7a82b 100644 --- a/airdef-mod/src/sim/logistics.rs +++ b/airdef-mod/src/sim/logistics.rs @@ -35,6 +35,9 @@ impl Sim { let Some(depot) = depot else { if s.ground_request { return Err("Заявка RM48U уже ожидает ТЗМ".into()); } if !self.sams.iter().any(|d| d.alive() && ((d.kind == SamKind::Depot && d.ground_stock > 0) || (d.kind == SamKind::Tzm && d.ground_stock > 0))) { + if let Some((j, _)) = self.sams.iter().enumerate().find(|(_, d)| d.kind == SamKind::Depot && d.alive() && d.pos.distance(s.pos) < DEPOT_REACH) { + return Err(format!("Склад №{} рядом, но RM48U: 0. Задайте запас: /airdef ground-stock {} <количество>", j + 1, j + 1)); + } return Err("Нет живого склада с RM48U рядом (60 блоков)".into()); } if !self.sams.iter().any(|t| t.kind == SamKind::Tzm && t.alive()) { return Err("Для доставки RM48U нужна исправная ТЗМ".into()); } @@ -288,6 +291,19 @@ impl Sim { #[cfg(test)] mod tests { use super::*; + #[test] + fn an_empty_nearby_depot_explains_the_missing_typed_stock() { + let mut sim = Sim::new(1); + for (kind, x) in [(SamKind::Depot, 0.0), (SamKind::S400, 1.0)] { + let id = sim.id(); + sim.sams.push(Sam::new(id, kind, DVec3::new(x, 70.0, 0.0), 0.0)); + } + let error = sim.load_ground_round(1).unwrap_err(); + assert!(error.contains("RM48U: 0")); + assert!(error.contains("/airdef ground-stock 1")); + assert_eq!((sim.sams[0].ground_stock, sim.sams[1].ground_rounds, sim.sams[1].missiles), (0, 0, 4)); + } + #[test] fn return_waits_for_capacity_and_ready_carrier_and_can_be_cancelled() { let mut sim = Sim::new(3); diff --git a/airdef-mod/src/sim/strike.rs b/airdef-mod/src/sim/strike.rs index 32f4e63..2c1ae76 100644 --- a/airdef-mod/src/sim/strike.rs +++ b/airdef-mod/src/sim/strike.rs @@ -694,7 +694,7 @@ impl Sim { /// Reserve a physical carrier and its cell; ammunition is spent only on ejection. pub fn order_strike(&mut self, id: u32, _from: DVec3, _out: &mut Out) -> Result { let k = self.strikes.get(id).ok_or_else(|| format!("ПУ №{id} не найдена"))?; - let est = k.est.filter(|e| e.fix).ok_or("Место ПУ неизвестно: нужна разведка")?; + let est = k.est.filter(|e| e.fix).ok_or("Место ПУ неизвестно: РЛС должна увидеть сам пуск. /airdef sites — состояние разведки")?; if matches!(k.state, SiteState::Destroyed(_)) { return Err("ПУ уже уничтожена".into()); } if self.strikes.flights.iter().any(|f| f.site == id) || self.strikes.pending.iter().any(|p| p.site == id) { return Err("По этой ПУ ракета уже готовится или в полёте".into()); @@ -954,9 +954,9 @@ impl Sim { s }).collect(); if v.is_empty() { - v.push("Известных ПУ нет: их находят РЛС по пускам и трекам вылетевших целей".into()); + v.push("Известных ПУ нет: РЛС должна заметить пуск или вылетевшую цель; для удара нужно определить место пуска".into()); } - v.push("Удар: /airdef strike <ПУ>; подготовка RM48U: /airdef ground-load <номер С-400> у склада".into()); + v.push("Удар: /airdef strike <вражеская ПУ>; RM48U: /airdef ground-stock <склад> <число>, затем ground-load <С-400>".into()); v } } diff --git a/docs/GROUND-STRIKES.md b/docs/GROUND-STRIKES.md new file mode 100644 index 0000000..92bd331 --- /dev/null +++ b/docs/GROUND-STRIKES.md @@ -0,0 +1,68 @@ +# Как подготовить удар по вражеской ПУ + +Номера своей техники и вражеских ПУ — разные списки. На карте **M** щёлкните по своей машине: +заголовок карточки показывает её номер. `/airdef sites` выводит номера разведанных вражеских площадок. + +## Боеприпас + +У обычного склада по умолчанию **0 RM48U**. Его обычные перезарядки ПВО — другой запас. +Профиль наземного удара пока есть только у С-400; другие ЗРК таким способом не заряжаются. + +Пример: склад №3, С-400 №2: + +```text +/airdef ground-stock 3 4 +/airdef ground-load 2 +``` + +Первая команда **задаёт остаток** склада равным четырём RM48U, а не прибавляет четыре. +Это подготовка sandbox-сценария; производства этих ракет пока нет. +Вторая загружает одну ракету. Каждый повтор загружает ещё одну, пока хватает запаса и ячеек. +Четыре ТПК общие: например, после первой загрузки будет 3 ракеты ПВО + 1 RM48U. +Вытесненная ракета ПВО остаётся на складе либо возвращается туда грузом ТЗМ. + +Рядом с исправным развёрнутым С-400 и наполненным складом (менее 60 блоков) передача прямая. +Вдали тот же приказ создаёт заявку, если есть запас и живая ТЗМ. Погрузка запрещена во время +движения, перезарядки, подготовки пуска или при отказе пусковой. Нулевой запас не исправляется +перестановкой склада ближе. Карточка склада показывает RM48U отдельно от обычного БК. + +## Откуда берётся разведка + +Она автоматическая. Рабочая включённая РЛС может заметить **сам пуск** с наземной площадки, +если он попадает в её игровой обзор и не закрыт рельефом. Это даёт оценку позиции; несколько +наблюдений могут её уточнить. Обнаруженная позже летящая цель даёт лишь слабое предположение +о направлении старта — оно не превращается в координаты от ожидания или от количества радаров. + +Команда `/airdef sites` должна показать, что место ПУ определено. Если там только направление +или «место неизвестно», удар не разрешён. Специальной команды, выдающей скрытые координаты, +и отдельного ручного режима разведки сейчас нет. У запусков «за картой» сам пуск не наблюдается. +Чтобы попробовать механику, используйте близкий наземный налёт и открытую местность; далёкая +траектория сама по себе не гарантирует обнаружения площадки. + +## Приказ на удар + +После определения места, например вражеской ПУ №7: + +```text +/airdef sites +/airdef strike 7 +``` + +Или **M → карточка вражеской ПУ → «Нанести удар»**. Кнопка появляется после определения позиции. +Указывать номер своего С-400 в `strike` не надо: мод сам выбирает готовый совместимый носитель +с RM48U в игровой дальности. Сначала проверяется разведка, поэтому её ошибка может появиться +раньше сообщения о незагруженной ракете. + +На носителе резервируется ячейка, открывается крышка, ракета выбрасывается; боеприпас списывается +при выбросе. Пуск остаётся игровой стилизацией, а не моделью реального регламента. + +## Возврат и отмена + +```text +/airdef ground-unload 2 +/airdef ground-cancel 2 +``` + +`ground-unload` у склада выгружает одну RM48U сразу; вдали резервирует её и вызывает ТЗМ для +вывоза. `ground-cancel` отменяет ещё ожидающую доставку/вывоз. Уже забранная ракета находится +в ТЗМ и продолжает путь на склад; команда не телепортирует её обратно в ПУ.