2026-10-27 16:34:58 +01:00
|
|
|
using System;
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
// МОДУЛЬ БОЕВОЙ СИСТЕМЫ: урон, потери, исход. Документ: Docs/Modules/Battle.md
|
|
|
|
|
// Без Unity и без float: этот же код без изменений сможет считать бои на сервере.
|
|
|
|
|
//
|
2026-10-27 16:34:58 +01:00
|
|
|
// Правила (предложение, утверждает команда -- открытый вопрос этапа 6 плана):
|
|
|
|
|
// - сила стороны = сумма по типам: воины x сила типа x (100% + контры + осадный бонус) x (100% + бонусы стороны);
|
|
|
|
|
// - контры: пехота сильнее кавалерии, кавалерия сильнее лучников, лучники сильнее пехоты (+25%);
|
|
|
|
|
// бонус типа против врага взвешивается по составу врага;
|
|
|
|
|
// - осадные: против стен +50%, в поле -25%; стены дают защитникам +20%;
|
|
|
|
|
// - побеждает тот, у кого сила больше; ничья -- защитнику;
|
|
|
|
|
// - проигравший теряет половину каждого типа; победитель -- до половины, тем больше, чем ровнее был бой.
|
2026-10-19 18:39:28 +02:00
|
|
|
public sealed class BattleSystem : IBattleSystem
|
|
|
|
|
{
|
2026-10-27 16:34:58 +01:00
|
|
|
// Сила одного воина каждого типа. Берётся из 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
public BattleOutcome Resolve(BattleRequest request)
|
|
|
|
|
{
|
2026-10-27 16:34:58 +01:00
|
|
|
int[] att = Units(request.AttackerUnits, request.Attackers);
|
|
|
|
|
int[] def = Units(request.DefenderUnits, request.Defenders);
|
2026-10-19 18:39:28 +02:00
|
|
|
|
2026-10-27 16:34:58 +01:00
|
|
|
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];
|
2026-10-19 18:39:28 +02:00
|
|
|
|
|
|
|
|
return new BattleOutcome
|
|
|
|
|
{
|
|
|
|
|
Victory = victory,
|
2026-10-27 16:34:58 +01:00
|
|
|
AttackerLosses = Sum(attLost),
|
|
|
|
|
DefenderLosses = Sum(defLost),
|
|
|
|
|
Survivors = Sum(survivors),
|
|
|
|
|
AttackerPower = attPower,
|
|
|
|
|
DefenderPower = defPower,
|
|
|
|
|
AttackerSurvivorUnits = survivors,
|
|
|
|
|
DefenderLossUnits = defLost
|
2026-10-19 18:39:28 +02:00
|
|
|
};
|
|
|
|
|
}
|
2026-10-27 16:34:58 +01:00
|
|
|
|
|
|
|
|
// Сила стороны в сотых долях (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;
|
|
|
|
|
}
|
2026-10-19 18:39:28 +02:00
|
|
|
}
|