Голод в бою (−50%) и правило усталости (−20%, вместе 40%)

План от 27.10.2026, раздел «Армии и восстановление»:
- BattleCondition (флаги Famine, Fatigue); BattleRequest.AttackerConditions / DefenderConditions
- голод −50%, усталость −20%, применяются последовательно: вместе 40% силы
- порядок: контры и осадные → бонусы → состояния → случайность
- числа в BattleConfig (famineStrengthPercent, fatigueStrengthPercent) и BattleRules, версия правил 2
- BattleOutcome.AttackerConditionPercent / DefenderConditionPercent

Мир:
- голод проверяется на момент боя: WorldSimulation.AttackerConditions спрашивает MarchManager,
  тот берёт IPopulation.IsStarving
- бой в офлайне получает голод на момент входа в игру (истории голода у Населения нет) — записано в Battle.md

Интерфейс карты:
- окно атаки: «Голод: войско слабее, сила 50%. Накормите город до боя», прогноз учитывает голод
- окно итога боя: «голод: сила 50%»

Усталость пока никто не выставляет: не утверждено, как смешивать свежие и уставшие войска.

Тесты: штрафы и их сложение, голод переворачивает бой у нападающего и у защитника, прогноз с голодом,
голод в момент боя у похода. Все 39 тестов ModuleTests проходят вне Unity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-10-28 19:16:25 +01:00
co-authored by Claude Opus 5.5
parent 2e87b10a58
commit 5581b4d2c9
13 changed files with 164 additions and 14 deletions
@@ -432,6 +432,63 @@ public class ModuleTests
Assert.AreEqual(2, o.RulesVersion, "итог помнит версию правил");
}
// Голод -50%, усталость -20%, вместе -- 40% (план от 27.10.2026)
[Test]
public void Battle_Famine_HalvesPower_AndStacksWithFatigue()
{
var b = new BattleSystem(BattleRules.NoRandom);
Assert.AreEqual(100, b.ConditionPercent(BattleCondition.None));
Assert.AreEqual(50, b.ConditionPercent(BattleCondition.Famine));
Assert.AreEqual(80, b.ConditionPercent(BattleCondition.Fatigue));
Assert.AreEqual(40, b.ConditionPercent(BattleCondition.Famine | BattleCondition.Fatigue), "последовательно: 50% x 80%, а не 30%");
// 40 пехоты берут замок Змея Горыныча, голодные -- нет
var req = new BattleRequest { AttackerUnits = new[] { 40, 0, 0, 0 }, DefenderUnits = new[] { 8, 6, 4, 2 }, DefenderFortified = true };
BattleOutcome fed = b.Resolve(req);
req.AttackerConditions = BattleCondition.Famine;
BattleOutcome hungry = b.Resolve(req);
Assert.IsTrue(fed.Victory);
Assert.IsFalse(hungry.Victory, "голодное войско слабее вдвое");
Assert.AreEqual(fed.AttackerPower / 2, hungry.AttackerPower);
Assert.AreEqual(fed.DefenderPower, hungry.DefenderPower, "голод нападающего не трогает защитника");
Assert.AreEqual(50, hungry.AttackerConditionPercent);
Assert.AreEqual(100, hungry.DefenderConditionPercent);
// Голод защитника работает так же
var defHungry = new BattleRequest { AttackerUnits = new[] { 22, 0, 0, 0 }, DefenderUnits = new[] { 20, 0, 0, 0 }, DefenderFortified = true };
Assert.IsFalse(b.Resolve(defHungry).Victory);
defHungry.DefenderConditions = BattleCondition.Famine;
Assert.IsTrue(b.Resolve(defHungry).Victory, "голодный гарнизон не удержал стены");
// Прогноз учитывает голод
Assert.AreEqual(BattleChance.HighRisk, new BattleSystem().Forecast(req).Chance);
}
[Test]
public void World_FamineAtBattleTime_WeakensMarch()
{
MarchData Run(BattleCondition conditions)
{
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem(BattleRules.NoRandom));
world.AttackerConditions = _ => conditions;
MarchData m = world.StartMarch("p", WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 40, 0, out _);
world.Advance(m.arriveMs, new List<WorldEvent>());
return m;
}
MarchData fed = Run(BattleCondition.None), hungry = Run(BattleCondition.Famine);
Assert.IsTrue(fed.victory);
Assert.AreEqual(100, fed.attackerConditionPercent);
Assert.IsFalse(hungry.victory, "голод в момент боя -- поражение");
Assert.AreEqual(50, hungry.attackerConditionPercent);
Assert.AreEqual((int)BattleCondition.Famine, hungry.attackerConditions);
// Не задано, кто голодает (сервер ещё не подключён, старый код) -- без штрафов
var plain = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem(BattleRules.NoRandom));
MarchData p = plain.StartMarch("p", WorldMapLayout.PlayerCityId, WorldMapLayout.BossCastleId, 40, 0, out _);
plain.Advance(p.arriveMs, new List<WorldEvent>());
Assert.AreEqual(100, p.attackerConditionPercent);
}
[Test]
public void World_MarchBattle_IsReproducible()
{