using System; using System.Collections.Generic; using System.Globalization; using System.Text; // МОДУЛЬ АРМИИ: единственное место, где хранятся войска. Документ: Docs/Modules/Army.md // Без Unity: правила те же, что будут на сервере. // // Один солдат -- в одном месте: либо в городе (troops), либо в походе (marches). // Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам. 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]; private readonly List marches = new List(); // Секция "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; } } 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; } if (lost > 0) Changed(); return lost; } public bool TakeForMarch(int marchId, int count) { int[] taken = ArmyMath.TakeProportional(troops, count); if (taken == null) return false; for (int i = 0; i < Types; i++) troops[i] -= taken[i]; marches.RemoveAll(m => m.marchId == marchId); marches.Add(new MarchUnits { marchId = marchId, units = taken }); Changed(); return true; } public void ReturnFromMarch(int marchId, int survivors) { MarchUnits entry = marches.Find(m => m.marchId == marchId); if (entry == null) { troops[0] += Math.Max(0, survivors); // состав неизвестен (очень старое сохранение) -- считаем пехотой } else { int[] back = ArmyMath.SplitSurvivors(entry.units, survivors); for (int i = 0; i < Types; i++) troops[i] += back[i]; marches.Remove(entry); } Changed(); } void Changed() { EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches }); } // ---------------- Перенос из старого progress.json ---------------- // Войска раньше хранил GameProgress (troops) и мост ArmyBridge (sashaMarchUnits). // Вызывается один раз, если в save.json ещё нет секции "army". public void ImportLegacy(int[] legacyTroops, IEnumerable> legacyMarches) { if (legacyTroops != null) for (int i = 0; i < Types && i < legacyTroops.Length; i++) troops[i] = Math.Max(0, legacyTroops[i]); marches.Clear(); if (legacyMarches != null) { foreach (var m in legacyMarches) { 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=<пехота>,<лучники>,<кавалерия>,<осадные> // march=:<пехота>,<лучники>,<кавалерия>,<осадные> public string SaveKey => "army"; public string Capture() { var sb = new StringBuilder(); sb.Append("troops=").Append(Join(troops)).Append('\n'); 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); 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); } else if (line.StartsWith("march=")) { string[] parts = line.Substring("march=".Length).Split(':'); if (parts.Length == 2 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int id)) marches.Add(new MarchUnits { marchId = id, units = Parse(parts[1]) }); } } } Changed(); } // Новая игра: войск нет, походов нет public void ResetState() { Array.Clear(troops, 0, Types); 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; } }