Боевая система с типами войск

Модуль Боя (Bogatyri.Battle, без Unity и float):
- сила стороны по типам войск, сила воина — из GameData.Troops (4/5/9/16)
- контры +25%: пехота > кавалерия > лучники > пехота (по составу врага)
- стены +20% защитникам; осадные +50% против стен, −25% в поле
- потери по типам: проигравший — половина, победитель — до половины (чем ровнее бой, тем больше)
- BattleRequest/BattleOutcome расширены (состав, стены, бонусы, сила сторон); бой «числом» считается пехотой
- формула — предложение, числа константами в BattleSystem.cs, ждёт утверждения командой

Мир и Армия:
- состав гарнизона у каждой цели (WorldMapLayout.GarrisonUnits, из id — карта не изменилась)
- поход хранит состав по типам; выжившие возвращаются точно по типам (IArmy.GetMarchUnits/ReturnUnits)
- время походов по GameClock: перемотка в DEV-панели проматывает и походы

Интерфейс карты:
- окно атаки: состав гарнизона и ожидаемый исход (сила сторон, победа/поражение, потери)
- верхняя панель: воины в походах (а не число отрядов)

Тесты: контры, стены, осадные, потери в пределах состава, состав гарнизонов.
Документы Battle, World, Army, Core, README обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-10-27 16:34:58 +01:00
co-authored by Claude Opus 5.5
parent f188c3a8eb
commit 460d1db2de
14 changed files with 406 additions and 35 deletions
@@ -157,6 +157,46 @@ namespace Bogatyri.World
return false;
}
// Состав гарнизона по типам войск (TroopType). Вычисляется из id цели, а не из генератора карты:
// так карта (координаты, гарнизоны) осталась прежней, а состав всегда одинаков у клиента и сервера.
public static int[] GarrisonUnits(MapNode node)
{
var units = new int[TroopType.Count];
if (node == null || node.garrison <= 0) return units;
int[] percent;
if (node.type == MapNodeType.BossCastle)
{
percent = new[] { 40, 30, 20, 10 }; // Змей Горыныч держит всех понемногу
}
else
{
uint h = StableHash(node.id);
int infantry = 30 + (int)(h % 31); // 30..60%
int archers = 10 + (int)(h / 31 % 26); // 10..35%
int cavalry = 5 + (int)(h / 806 % 21); // 5..25%
int siege = Math.Max(0, 100 - infantry - archers - cavalry);
percent = new[] { infantry, archers, cavalry, siege };
}
int given = 0;
for (int i = 0; i < TroopType.Count; i++)
{
units[i] = node.garrison * percent[i] / 100;
given += units[i];
}
units[TroopType.Infantry] += node.garrison - given; // остаток округления -- пехоте
return units;
}
// FNV-1a: одинаков на любой платформе (string.GetHashCode в .NET меняется между запусками)
static uint StableHash(string s)
{
uint hash = 2166136261;
foreach (char c in s ?? "") hash = (hash ^ c) * 16777619;
return hash;
}
public static MapNode Find(List<MapNode> nodes, string id)
{
foreach (var n in nodes)