Files
MVP1/Assets/Scripts/Modules/Battle/BattleSystem.cs
T
AlexanderandClaude Opus 5.5 460d1db2de Боевая система с типами войск
Модуль Боя (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>
2026-10-27 16:34:58 +01:00

134 lines
6.3 KiB
C#

using System;
// МОДУЛЬ БОЕВОЙ СИСТЕМЫ: урон, потери, исход. Документ: Docs/Modules/Battle.md
// Без Unity и без float: этот же код без изменений сможет считать бои на сервере.
//
// Правила (предложение, утверждает команда -- открытый вопрос этапа 6 плана):
// - сила стороны = сумма по типам: воины x сила типа x (100% + контры + осадный бонус) x (100% + бонусы стороны);
// - контры: пехота сильнее кавалерии, кавалерия сильнее лучников, лучники сильнее пехоты (+25%);
// бонус типа против врага взвешивается по составу врага;
// - осадные: против стен +50%, в поле -25%; стены дают защитникам +20%;
// - побеждает тот, у кого сила больше; ничья -- защитнику;
// - проигравший теряет половину каждого типа; победитель -- до половины, тем больше, чем ровнее был бой.
public sealed class BattleSystem : IBattleSystem
{
// Сила одного воина каждого типа. Берётся из GameData.Troops (App/GameModules), здесь -- запасные значения.
public static readonly int[] DefaultPower = { 4, 5, 9, 16 };
public const int CounterPercent = 25;
public const int SiegeVsWallsPercent = 50;
public const int SiegeInFieldPercent = -25;
public const int WallsPercent = 20;
public const int LoserLossPercent = 50;
private readonly int[] power;
public BattleSystem() : this(DefaultPower) { }
public BattleSystem(int[] unitPower)
{
power = new int[TroopType.Count];
for (int i = 0; i < TroopType.Count; i++)
power[i] = unitPower != null && i < unitPower.Length && unitPower[i] > 0 ? unitPower[i] : DefaultPower[i];
}
// Контра: насколько тип attackerType сильнее против defenderType, в процентах
public static int Counter(int attackerType, int defenderType)
{
if (attackerType == TroopType.Infantry && defenderType == TroopType.Cavalry) return CounterPercent;
if (attackerType == TroopType.Cavalry && defenderType == TroopType.Archers) return CounterPercent;
if (attackerType == TroopType.Archers && defenderType == TroopType.Infantry) return CounterPercent;
return 0;
}
public BattleOutcome Resolve(BattleRequest request)
{
int[] att = Units(request.AttackerUnits, request.Attackers);
int[] def = Units(request.DefenderUnits, request.Defenders);
long attPower = SidePower(att, def, attacking: true, request.DefenderFortified, request.AttackerBonusPercent);
long defPower = SidePower(def, att, attacking: false, request.DefenderFortified,
request.DefenderBonusPercent + (request.DefenderFortified ? WallsPercent : 0));
bool victory = attPower > defPower; // ничья -- в пользу защитника
// Потери в процентах: проигравший -- половина; победитель -- половина x (сила проигравшего / сила победителя)
long stronger = Math.Max(attPower, defPower);
long weaker = Math.Min(attPower, defPower);
int[] attLost = Losses(att, weaker, stronger, isLoser: !victory);
int[] defLost = Losses(def, weaker, stronger, isLoser: victory);
var survivors = new int[TroopType.Count];
for (int i = 0; i < TroopType.Count; i++) survivors[i] = att[i] - attLost[i];
return new BattleOutcome
{
Victory = victory,
AttackerLosses = Sum(attLost),
DefenderLosses = Sum(defLost),
Survivors = Sum(survivors),
AttackerPower = attPower,
DefenderPower = defPower,
AttackerSurvivorUnits = survivors,
DefenderLossUnits = defLost
};
}
// Сила стороны в сотых долях (x100), чтобы проценты считались в целых числах
long SidePower(int[] units, int[] enemy, bool attacking, bool fortified, int bonusPercent)
{
int enemyTotal = Sum(enemy);
long total = 0;
for (int i = 0; i < TroopType.Count; i++)
{
if (units[i] <= 0) continue;
long counterSum = 0;
for (int j = 0; j < TroopType.Count; j++) counterSum += (long)Counter(i, j) * enemy[j];
long counterPercent = enemyTotal > 0 ? counterSum / enemyTotal : 0;
int siegePercent = 0;
if (i == TroopType.Siege) siegePercent = attacking && fortified ? SiegeVsWallsPercent : SiegeInFieldPercent;
total += (long)units[i] * power[i] * (100 + counterPercent + siegePercent);
}
return Math.Max(0, total * (100 + bonusPercent) / 100);
}
// Потери каждого типа: проигравший -- LoserLossPercent; победитель -- LoserLossPercent x ratioNum / ratioDen
static int[] Losses(int[] units, long ratioNum, long ratioDen, bool isLoser)
{
var lost = new int[TroopType.Count];
for (int i = 0; i < TroopType.Count; i++)
{
if (units[i] <= 0) continue;
long l = isLoser
? (long)units[i] * LoserLossPercent / 100
: ratioDen > 0 ? (long)units[i] * LoserLossPercent * ratioNum / (100 * ratioDen) : 0;
lost[i] = (int)Math.Min(units[i], l);
}
return lost;
}
static int[] Units(int[] units, int count)
{
var result = new int[TroopType.Count];
if (units != null)
{
for (int i = 0; i < TroopType.Count && i < units.Length; i++) result[i] = Math.Max(0, units[i]);
}
else
{
result[TroopType.Infantry] = Math.Max(0, count); // состав неизвестен -- считаем пехотой
}
return result;
}
static int Sum(int[] values)
{
int n = 0;
foreach (var v in values) n += v;
return n;
}
}