fix: explain empty strike stock and ground attack prerequisites
build / mod (push) Failing after 19s

Show typed depot cargo separately, identify an empty nearby depot, and document loading, reconnaissance, two ID namespaces and reverse pickup.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
2026-11-03 11:26:52 +03:00
parent cc48302b36
commit cbd6067e8f
5 changed files with 96 additions and 4 deletions
+2 -1
View File
@@ -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 <on|off>` — «последний шанс»: комплекс стреляет и по классу вне ролей, если цель вот-вот попадёт (по умолчанию вкл; сохраняется);
- `/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 <patriot|gepard|pantsir|s400|s500|all> <классы|default>` — задать (классы через запятую: `drone`, `jet`, `cruise`, `supersonic`, `ballistic` или `all`), например `/airdef roles s400 cruise,ballistic`;
- `/airdef doctrine` — доктрины комплексов (#47; в меню N: «Доктрины…»): `/airdef doctrine <номер|all> reserve <K>` — держать K ракет для срочных целей (баллистика или до удара меньше 12 с); `salvo eco|normal|heavy` — сколько перехватчиков на цель (только базовые / как обычно / на один больше); `sector <центр°> <полуширина°>` или `sector off` — сектор ответственности (0° — север); `guard <объект№>[,<№>]` или `guard off` — стрелять только по тому, что идёт на охраняемые объекты (отметка: `/airdef target here`). По умолчанию ограничений нет; последний шанс сильнее доктрины; `/airdef why <номер>` называет, какая настройка остановила или разрешила пуск; сохраняется с миром и сценариями. Карта охраны и пробелов в ней — вместе с картой (#48).
+7
View File
@@ -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));
+16
View File
@@ -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);
+3 -3
View File
@@ -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<String, String> {
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
}
}
+68
View File
@@ -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` отменяет ещё ожидающую доставку/вывоз. Уже забранная ракета находится
в ТЗМ и продолжает путь на склад; команда не телепортирует её обратно в ПУ.