2026-10-23 18:33:04 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
|
|
|
|
// МОДУЛЬ АРМИИ: единственное место, где хранятся войска. Документ: Docs/Modules/Army.md
|
|
|
|
|
|
// Без Unity: правила те же, что будут на сервере.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Один солдат -- в одном месте: либо в городе (troops), либо в походе (marches).
|
|
|
|
|
|
// Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам.
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// Гарнизон (garrison) -- часть воинов города: они в troops, но в поход не уходят. garrison[i] <= troops[i] всегда.
|
2026-10-23 18:33:04 +02:00
|
|
|
|
public sealed class ArmyService : IArmy, ISaveParticipant
|
|
|
|
|
|
{
|
|
|
|
|
|
public const int Types = 4; // пехота, лучники, кавалерия, осадные -- порядок как в GameData.Troops
|
|
|
|
|
|
|
|
|
|
|
|
private class MarchUnits
|
|
|
|
|
|
{
|
|
|
|
|
|
public int marchId;
|
|
|
|
|
|
public int[] units = new int[Types];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private readonly int[] troops = new int[Types];
|
2026-10-27 16:54:49 +01:00
|
|
|
|
private readonly int[] garrison = new int[Types];
|
2026-10-23 18:33:04 +02:00
|
|
|
|
private readonly List<MarchUnits> marches = new List<MarchUnits>();
|
|
|
|
|
|
|
|
|
|
|
|
// Секция "army" была в сохранении -- старые войска из progress.json переносить не нужно
|
|
|
|
|
|
public bool RestoredFromSave { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Контракт IArmy ----------------
|
|
|
|
|
|
|
|
|
|
|
|
public int TypeCount => Types;
|
|
|
|
|
|
|
|
|
|
|
|
public int GetTroops(int type)
|
|
|
|
|
|
{
|
|
|
|
|
|
return type >= 0 && type < Types ? troops[type] : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int SoldiersAtHome
|
|
|
|
|
|
{
|
|
|
|
|
|
get { int n = 0; foreach (var t in troops) n += t; return n; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int TroopsInMarches
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
foreach (var m in marches) foreach (var u in m.units) n += u;
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
public int GarrisonTotal
|
|
|
|
|
|
{
|
|
|
|
|
|
get { int n = 0; foreach (var g in garrison) n += g; return n; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int GetGarrison(int type)
|
|
|
|
|
|
{
|
|
|
|
|
|
return type >= 0 && type < Types ? garrison[type] : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int AvailableForMarch => SoldiersAtHome - GarrisonTotal;
|
|
|
|
|
|
|
|
|
|
|
|
// Гарнизон набирается по составу армии в городе: 30 пехоты и 10 лучников, гарнизон 8 -> 6 пехоты и 2 лучника
|
|
|
|
|
|
public int SetGarrison(int count)
|
|
|
|
|
|
{
|
|
|
|
|
|
count = Math.Max(0, Math.Min(count, SoldiersAtHome));
|
|
|
|
|
|
int[] units = count > 0 ? ArmyMath.TakeProportional(troops, count) : null;
|
|
|
|
|
|
for (int i = 0; i < Types; i++) garrison[i] = units != null ? units[i] : 0;
|
|
|
|
|
|
Changed();
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 18:33:04 +02:00
|
|
|
|
public void AddTroops(int type, int count)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (type < 0 || type >= Types || count <= 0) return;
|
|
|
|
|
|
troops[type] += count;
|
|
|
|
|
|
Changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int LoseFraction(double fraction)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (fraction <= 0) return 0;
|
|
|
|
|
|
if (fraction > 1) fraction = 1;
|
|
|
|
|
|
|
|
|
|
|
|
int lost = 0;
|
|
|
|
|
|
for (int i = 0; i < Types; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int l = (int)Math.Floor(troops[i] * fraction);
|
|
|
|
|
|
troops[i] -= l;
|
|
|
|
|
|
lost += l;
|
|
|
|
|
|
}
|
2026-10-27 16:54:49 +01:00
|
|
|
|
ClampGarrison();
|
2026-10-23 18:33:04 +02:00
|
|
|
|
if (lost > 0) Changed();
|
|
|
|
|
|
return lost;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TakeForMarch(int marchId, int count)
|
|
|
|
|
|
{
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Поход с таким номером уже есть: перезапись потеряла бы его воинов
|
|
|
|
|
|
if (marches.Exists(m => m.marchId == marchId)) return false;
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// В поход -- только свободные воины, гарнизон остаётся в городе
|
|
|
|
|
|
var free = new int[Types];
|
|
|
|
|
|
for (int i = 0; i < Types; i++) free[i] = troops[i] - garrison[i];
|
|
|
|
|
|
int[] taken = ArmyMath.TakeProportional(free, count);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
if (taken == null) return false;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < Types; i++) troops[i] -= taken[i];
|
|
|
|
|
|
marches.Add(new MarchUnits { marchId = marchId, units = taken });
|
|
|
|
|
|
Changed();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Воины возвращаются только из известного похода, и только один раз: запись похода удаляется.
|
|
|
|
|
|
// Неизвестный поход (повторное событие, испорченное сохранение) не добавляет никого -- иначе солдаты
|
|
|
|
|
|
// появлялись бы из ниоткуда. Перенос старых сохранений -- отдельно, в ImportLegacy.
|
|
|
|
|
|
public bool ReturnFromMarch(int marchId, int survivors)
|
2026-10-23 18:33:04 +02:00
|
|
|
|
{
|
|
|
|
|
|
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
2026-10-28 18:38:01 +01:00
|
|
|
|
if (entry == null) return false;
|
|
|
|
|
|
|
|
|
|
|
|
int sent = 0;
|
|
|
|
|
|
foreach (var u in entry.units) sent += u;
|
|
|
|
|
|
int[] back = ArmyMath.SplitSurvivors(entry.units, Math.Min(survivors, sent)); // больше, чем ушло, не вернётся
|
|
|
|
|
|
for (int i = 0; i < Types; i++) troops[i] += back[i];
|
|
|
|
|
|
marches.Remove(entry);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
Changed();
|
2026-10-28 18:38:01 +01:00
|
|
|
|
return true;
|
2026-10-23 18:33:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:34:58 +01:00
|
|
|
|
public int[] GetMarchUnits(int marchId)
|
|
|
|
|
|
{
|
|
|
|
|
|
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
|
|
|
|
|
return entry != null ? (int[])entry.units.Clone() : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Выжившие точно по типам: бой посчитал потери каждого типа. Больше, чем ушло, вернуться не может.
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Правила те же, что у ReturnFromMarch: только известный поход, только один раз.
|
|
|
|
|
|
public bool ReturnUnits(int marchId, int[] survivorUnits)
|
2026-10-27 16:34:58 +01:00
|
|
|
|
{
|
|
|
|
|
|
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
2026-10-28 18:38:01 +01:00
|
|
|
|
if (entry == null) return false;
|
|
|
|
|
|
|
2026-10-27 16:34:58 +01:00
|
|
|
|
for (int i = 0; i < Types && survivorUnits != null && i < survivorUnits.Length; i++)
|
2026-10-28 18:38:01 +01:00
|
|
|
|
troops[i] += Math.Max(0, Math.Min(survivorUnits[i], entry.units[i]));
|
|
|
|
|
|
marches.Remove(entry);
|
2026-10-27 16:34:58 +01:00
|
|
|
|
Changed();
|
2026-10-28 18:38:01 +01:00
|
|
|
|
return true;
|
2026-10-27 16:34:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
void ClampGarrison()
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < Types; i++) garrison[i] = Math.Max(0, Math.Min(garrison[i], troops[i]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-23 18:33:04 +02:00
|
|
|
|
void Changed()
|
|
|
|
|
|
{
|
2026-10-27 16:54:49 +01:00
|
|
|
|
EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches, Garrison = GarrisonTotal });
|
2026-10-23 18:33:04 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Перенос из старого progress.json ----------------
|
|
|
|
|
|
|
|
|
|
|
|
// Войска раньше хранил GameProgress (troops) и мост ArmyBridge (sashaMarchUnits).
|
|
|
|
|
|
// Вызывается один раз, если в save.json ещё нет секции "army".
|
|
|
|
|
|
public void ImportLegacy(int[] legacyTroops, IEnumerable<KeyValuePair<int, int[]>> legacyMarches)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (legacyTroops != null)
|
|
|
|
|
|
for (int i = 0; i < Types && i < legacyTroops.Length; i++) troops[i] = Math.Max(0, legacyTroops[i]);
|
2026-10-27 16:54:49 +01:00
|
|
|
|
Array.Clear(garrison, 0, Types);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
|
|
|
|
|
|
marches.Clear();
|
|
|
|
|
|
if (legacyMarches != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var m in legacyMarches)
|
|
|
|
|
|
{
|
2026-10-28 18:38:01 +01:00
|
|
|
|
if (marches.Exists(x => x.marchId == m.Key)) continue; // один поход -- одна запись
|
2026-10-23 18:33:04 +02:00
|
|
|
|
var units = new int[Types];
|
|
|
|
|
|
for (int i = 0; i < Types && m.Value != null && i < m.Value.Length; i++) units[i] = Math.Max(0, m.Value[i]);
|
|
|
|
|
|
marches.Add(new MarchUnits { marchId = m.Key, units = units });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Сохранение (секция "army") ----------------
|
|
|
|
|
|
// Модуль без Unity, поэтому формат -- простые строки:
|
|
|
|
|
|
// troops=<пехота>,<лучники>,<кавалерия>,<осадные>
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (из troops; нет строки -- гарнизона нет)
|
2026-10-23 18:33:04 +02:00
|
|
|
|
// march=<id похода>:<пехота>,<лучники>,<кавалерия>,<осадные>
|
|
|
|
|
|
|
|
|
|
|
|
public string SaveKey => "army";
|
|
|
|
|
|
|
|
|
|
|
|
public string Capture()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
sb.Append("troops=").Append(Join(troops)).Append('\n');
|
2026-10-27 16:54:49 +01:00
|
|
|
|
sb.Append("garrison=").Append(Join(garrison)).Append('\n');
|
2026-10-23 18:33:04 +02:00
|
|
|
|
foreach (var m in marches)
|
|
|
|
|
|
sb.Append("march=").Append(m.marchId.ToString(CultureInfo.InvariantCulture)).Append(':').Append(Join(m.units)).Append('\n');
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Restore(string data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Array.Clear(troops, 0, Types);
|
2026-10-27 16:54:49 +01:00
|
|
|
|
Array.Clear(garrison, 0, Types);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
marches.Clear();
|
|
|
|
|
|
RestoredFromSave = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(data))
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var raw in data.Split('\n'))
|
|
|
|
|
|
{
|
|
|
|
|
|
string line = raw.Trim();
|
|
|
|
|
|
if (line.StartsWith("troops="))
|
|
|
|
|
|
{
|
|
|
|
|
|
int[] t = Parse(line.Substring("troops=".Length));
|
|
|
|
|
|
Array.Copy(t, troops, Types);
|
|
|
|
|
|
}
|
2026-10-27 16:54:49 +01:00
|
|
|
|
else if (line.StartsWith("garrison="))
|
|
|
|
|
|
{
|
|
|
|
|
|
int[] g = Parse(line.Substring("garrison=".Length));
|
|
|
|
|
|
Array.Copy(g, garrison, Types);
|
|
|
|
|
|
}
|
2026-10-23 18:33:04 +02:00
|
|
|
|
else if (line.StartsWith("march="))
|
|
|
|
|
|
{
|
|
|
|
|
|
string[] parts = line.Substring("march=".Length).Split(':');
|
2026-10-28 18:38:01 +01:00
|
|
|
|
if (parts.Length == 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int id)
|
|
|
|
|
|
&& !marches.Exists(x => x.marchId == id)) // один поход -- одна запись
|
2026-10-23 18:33:04 +02:00
|
|
|
|
marches.Add(new MarchUnits { marchId = id, units = Parse(parts[1]) });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-27 16:54:49 +01:00
|
|
|
|
ClampGarrison(); // гарнизон не больше войск, даже если файл правили руками
|
2026-10-23 18:33:04 +02:00
|
|
|
|
Changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// Новая игра: войск нет, гарнизона и походов нет
|
2026-10-23 18:33:04 +02:00
|
|
|
|
public void ResetState()
|
|
|
|
|
|
{
|
|
|
|
|
|
Array.Clear(troops, 0, Types);
|
2026-10-27 16:54:49 +01:00
|
|
|
|
Array.Clear(garrison, 0, Types);
|
2026-10-23 18:33:04 +02:00
|
|
|
|
marches.Clear();
|
|
|
|
|
|
Changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string Join(int[] values)
|
|
|
|
|
|
{
|
|
|
|
|
|
var parts = new string[values.Length];
|
|
|
|
|
|
for (int i = 0; i < values.Length; i++) parts[i] = values[i].ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
return string.Join(",", parts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int[] Parse(string csv)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new int[Types];
|
|
|
|
|
|
string[] parts = csv.Split(',');
|
|
|
|
|
|
for (int i = 0; i < Types && i < parts.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (int.TryParse(parts[i].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int v))
|
|
|
|
|
|
result[i] = Math.Max(0, v);
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|