Армия (п. 2.1): - ReturnFromMarch / ReturnUnits возвращают bool и зачисляют воинов только по известному походу и только один раз (раньше неизвестный поход добавлял выживших пехотой, ReturnUnits — без ограничения) - вернуться может не больше, чем ушло - TakeForMarch отклоняет повторный номер похода (раньше перезапись теряла воинов первого) - загрузка сохранения и перенос старых данных пропускают дубликаты походов Мир (п. 2.2): - TryStartMarch проверяет TakeForMarch; при отказе поход откатывается (WorldSimulation.CancelMarch) - если Армия не знает возвращающийся поход — ни воинов, ни добычи, предупреждение в консоли Тесты (п. 2.1, 2.2, 2.4): - неизвестный поход, повторное возвращение, повторный номер, откат похода - крайние случаи боя: 1×1, 1×10, 10×1, 0×0, пустая сторона, бонусы −100% / −500% / +100000%, бой числом Формула потерь не менялась. Найдено и записано в Battle.md как открытый вопрос: одиночный проигравший не гибнет (потери округляются вниз) — правило округления решает команда. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
11 KiB
Markdown
104 lines
11 KiB
Markdown
# Армия
|
||
|
||
Войска и их характеристики: сколько воинов каждого типа в городе, сколько из них в гарнизоне, сколько в походах, обучение, потери, уход в поход и возвращение. **Единственное место, где хранятся войска.**
|
||
|
||
- **Сборка:** `Bogatyri.Army` (без Unity).
|
||
- **Папка:** `Assets/Scripts/Modules/Army`.
|
||
- **Зависит от:** ядра.
|
||
|
||
## Контракт `IArmy`
|
||
|
||
| Член | Что делает |
|
||
| --- | --- |
|
||
| `TypeCount` | Типов войск — 4 (пехота, лучники, кавалерия, осадные; порядок как в `GameData.Troops`). |
|
||
| `GetTroops(type)` | Воинов этого типа в городе. |
|
||
| `SoldiersAtHome` | Всего воинов в городе (вместе с гарнизоном). |
|
||
| `TroopsInMarches` | Воинов в походах. |
|
||
| `GarrisonTotal`, `GetGarrison(type)` | Сколько воинов в гарнизоне — всего и по типам. |
|
||
| `AvailableForMarch` | Свободные воины: в городе минус гарнизон. Только их можно отправить в поход. |
|
||
| `SetGarrison(count)` | Назначить гарнизон: набирается по составу армии в городе (30 пехоты и 10 лучников, гарнизон 8 → 6 пехоты и 2 лучника). Не больше, чем воинов в городе; возвращает, сколько назначено. |
|
||
| `AddTroops(type, count)` | Добавить воинов (обучение в Казармах). |
|
||
| `LoseFraction(fraction)` | Потери в бою: от каждого типа погибает доля (с округлением вниз); возвращает, сколько погибло. |
|
||
| `TakeForMarch(marchId, count)` | Забрать свободных воинов (без гарнизона) в поход пропорционально составу и запомнить состав. `false` — ничего не взято: не хватает воинов или поход с таким номером уже есть. |
|
||
| `ReturnFromMarch(marchId, survivors)` | Вернуть выживших по тем же типам (пропорционально). `false` — поход неизвестен, никто не добавлен. |
|
||
| `GetMarchUnits(marchId)` | Состав похода по типам — для боя с типами войск. |
|
||
| `ReturnUnits(marchId, survivorUnits)` | Вернуть выживших точно по типам, как посчитал бой. Больше, чем ушло, не вернётся. `false` — поход неизвестен. |
|
||
|
||
Получить: `GameServices.Get<IArmy>()`. Регистрирует `App/GameModules`. `GameProgress` (обучение, кампания, арена, сила армии) и панели работают с войсками только через этот контракт.
|
||
|
||
## Правило «один солдат в одном месте»
|
||
|
||
Воин либо в городе, либо в походе. Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам. Тест `ArmyService_MarchNeverCreatesOrLosesSoldiers` проверяет, что «дома + в походах» всегда равно общему числу.
|
||
|
||
**Возвращение — только по известному походу и только один раз** (ревью 27.10.2026, п. 2.1):
|
||
|
||
- неизвестный поход (повторное событие, испорченное сохранение) не добавляет никого — раньше такие выжившие зачислялись пехотой;
|
||
- после возвращения запись похода удаляется, второе возвращение того же похода возвращает `false`;
|
||
- вернуться может не больше, чем ушло;
|
||
- два похода с одним номером невозможны: `TakeForMarch` отклоняет повтор, загрузка и перенос старых данных пропускают дубликаты;
|
||
- перенос старых сохранений — отдельно, в `ImportLegacy`.
|
||
|
||
**Гарнизон** — часть воинов города. Они по-прежнему в городе (`SoldiersAtHome`), но в поход не уходят:
|
||
|
||
- 30 воинов, 10 в гарнизоне → 20 свободны, всего по-прежнему 30 (тест `ArmyService_Garrison_OneSoldierInOnePlace`);
|
||
- гарнизон по каждому типу никогда не больше воинов этого типа: после потерь (`LoseFraction`) он обрезается;
|
||
- новые воины из Казарм становятся свободными, гарнизон не меняется;
|
||
- назначается в окне «Войска» в городе, карточка «Гарнизон города»: кнопки 0, -10, -1, +1, +10, «Все».
|
||
|
||
Пока гарнизон только держит воинов дома: на город игрока никто не нападает. Защита города, еда гарнизона и дебаффы при голоде — по плану ниже.
|
||
|
||
## События
|
||
|
||
- **Публикует:** `TroopsChangedEvent` (воинов в городе, из них в гарнизоне, и в походах) — при обучении, потерях, походе, возвращении, загрузке, сбросе. По нему `App/GameModules` перерисовывает панели HUD.
|
||
- **Слушает:** ничего.
|
||
|
||
## Сохранение
|
||
|
||
Секция `"army"`. Модуль без Unity, поэтому формат — простые строки:
|
||
|
||
```
|
||
troops=<пехота>,<лучники>,<кавалерия>,<осадные>
|
||
garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (часть troops)
|
||
march=<id похода>:<пехота>,<лучники>,<кавалерия>,<осадные>
|
||
```
|
||
|
||
Старое сохранение без строки `garrison` читается: гарнизона нет.
|
||
|
||
Сброс (`ResetState`): войск, гарнизона и походов нет.
|
||
|
||
**Перенос из старых сохранений:**
|
||
- Раньше войска лежали в `progress.json`: `troops` хранил `GameProgress`, `sashaMarchUnits` — мост `ArmyBridge`.
|
||
- Если в `save.json` нет секции `"army"`, после загрузки `App/GameModules` переносит их в модуль (`ImportLegacy`).
|
||
- Из `progress.json` они удаляются только после записи `save.json`. Флаг `armyInModule` отмечает, что перенос сделан.
|
||
|
||
## Файлы
|
||
|
||
| Файл | Что это |
|
||
| --- | --- |
|
||
| `ArmyService.cs` | Хранилище войск, реализует `IArmy` и `ISaveParticipant`. |
|
||
| `ArmyMath.cs` | `TakeProportional` — кого взять в поход; `SplitSurvivors` — как вернуть выживших. |
|
||
|
||
## Решения и план
|
||
|
||
- **Этап 5 (план разработки):** солдат стоит 1 человека, 15 еды, 5 металла, 5 золота; содержание 0,2 еды в час; погибшие уменьшают население. Делается вместе с модулем «Население».
|
||
- **Решение команды от 19.10.2026:**
|
||
- войска набираются из населения с сохранением гражданского минимума;
|
||
- гарнизон поселения: часть солдат назначается защищать город;
|
||
- общая армия = гарнизон + доступные отряды + войска в походах, на точках и в гарнизоне Круга, **один солдат не может быть в двух местах**;
|
||
- гарнизон ест еду и участвует в защите;
|
||
- при голоде дебаффы действуют и на гарнизон, и на походы.
|
||
- **Сделано 27.10.2026:** гарнизон города (`SetGarrison`, `AvailableForMarch`).
|
||
- **Дальше по гарнизону:** участие в защите города (когда на него смогут напасть — PvP), еда гарнизона (сейчас все воины едят одинаково, считает модуль Населения), дебаффы при голоде. Выбор состава по типам — если понадобится.
|
||
- Описания типов войск (`GameData.Troops`: цена, сила, время обучения) пока в слое приложения; цены перейдут в `EconomyConfig`.
|
||
|
||
## Журнал изменений
|
||
|
||
| Дата | Кто | Что |
|
||
| --- | --- | --- |
|
||
| 28.10.2026 | Claude | По ревью 27.10.2026 (п. 2.1–2.2): `ReturnFromMarch` и `ReturnUnits` возвращают `bool` и зачисляют только по известному походу и только один раз; `TakeForMarch` отклоняет повторный номер похода; дубликаты походов в сохранении пропускаются. Тесты: неизвестный поход, повторное возвращение, повторный номер. |
|
||
| 27.10.2026 | Claude | Гарнизон города: `GarrisonTotal`, `GetGarrison`, `AvailableForMarch`, `SetGarrison`; в поход уходят только свободные; строка `garrison=` в сохранении; поле `Garrison` в `TroopsChangedEvent`; карточка «Гарнизон города» в окне «Войска». Тест «один солдат в одном месте» с гарнизоном. |
|
||
| 27.10.2026 | Claude | `GetMarchUnits` и `ReturnUnits`: бой с типами войск получает состав похода, выжившие возвращаются точно по типам. |
|
||
| 23.10.2026 | Claude | Войска переехали в модуль: `ArmyService` — единственное хранилище войск (секция `"army"`), `IArmy` расширен (типы, обучение, потери, войска в походах), событие `TroopsChangedEvent`. Перенос из `progress.json` с удалением старых данных только после записи `save.json`. `ArmyBridge` удалён — контракты и перенос сохранений проверены тестами. |
|
||
| 19.10.2026 | Claude | `ArmyBridge.TroopsInMarches` для панели «Войска». После удаления маленькой карты войска в походах учитываются только здесь (`sashaMarchUnits`). |
|
||
| 19.10.2026 | Claude | Модуль создан: `ArmyMath` (правила деления войск, без Unity, тесты на отсутствие двойного учёта). `ArmyBridge` сохранён и теперь реализует `IArmy` через `ArmyMath`; формат `sashaMarchUnits` не изменён. |
|