Бой делит выбывших на погибших и раненых по соотношению сил стороны (70/45/30/20%), версия правил 6. Раненые идут домой с войском; Армия кладёт их в госпиталь, лечит 4/4/6/8 игровых часов, лишние ждут места. Места -- от IHospital (здания пока нет -- HospitalConfig.temporaryBeds), цена -- через Экономику (не утверждена). Население считает раненых людьми. Здание госпиталя -- следующий шаг. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
597 lines
28 KiB
C#
597 lines
28 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Text;
|
||
|
||
// МОДУЛЬ АРМИИ: единственное место, где хранятся войска. Документ: Docs/Modules/Army.md
|
||
// Без Unity: правила те же, что будут на сервере.
|
||
//
|
||
// Один солдат -- в одном месте: либо в городе (troops), либо в походе (marches).
|
||
// Поход забирает воинов из города и запоминает состав, возвращение раздаёт выживших по тем же типам.
|
||
// Гарнизон (garrison) -- часть воинов города: они в troops, но в поход не уходят. garrison[i] <= troops[i] всегда.
|
||
// Очередь обучения (training) -- тоже здесь (решения по 8 вопросам Саши, 31.10, п. 8): заказы, сроки, завершение.
|
||
// Раненые (решения 31.10, п. 7): ждут места (woundedWaiting) или лечатся (healing) -- не в troops и не в походах.
|
||
// Числа госпиталя (регламент 31.10, ТЕСТ). В игре -- из таблицы HospitalConfig.
|
||
public sealed class HospitalRules
|
||
{
|
||
public int TemporaryBeds = 20; // мест, пока здания госпиталя нет (как у госпиталя I уровня)
|
||
public int[] HealGameHours = { 4, 4, 6, 8 }; // лечение одного воина: пехота и лучники 4, кавалерия 6, осадные 8 ИГРОВЫХ часов
|
||
public long GameHourMs = 5 * 60 * 1000; // 1 игровой час = 5 реальных минут
|
||
// Цена лечения одного воина по типам -- НЕ УТВЕРЖДЕНА: пока пусто, лечение без цены
|
||
public ResourceAmount[][] HealCostPerSoldier = null;
|
||
|
||
public static HospitalRules Default => new HospitalRules();
|
||
|
||
public long HealMs(int type)
|
||
{
|
||
int hours = HealGameHours != null && type >= 0 && type < HealGameHours.Length ? Math.Max(0, HealGameHours[type]) : 4;
|
||
return hours * GameHourMs;
|
||
}
|
||
}
|
||
|
||
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 int[] garrison = new int[Types];
|
||
private readonly List<MarchUnits> marches = new List<MarchUnits>();
|
||
|
||
private class TrainingOrder
|
||
{
|
||
public int id, type, count;
|
||
public long startMs, endMs;
|
||
public bool recruited; // под заказ взяты жители (Население). false -- старый заказ, людей не брали
|
||
}
|
||
|
||
private class HealingBatch
|
||
{
|
||
public int id, type, count;
|
||
public long startMs, endMs;
|
||
}
|
||
|
||
private readonly int[] woundedWaiting = new int[Types];
|
||
private readonly List<HealingBatch> healing = new List<HealingBatch>();
|
||
private readonly List<HealingInfo> healingView = new List<HealingInfo>();
|
||
private int nextHealId = 1;
|
||
private readonly HospitalRules hospital;
|
||
|
||
private readonly List<TrainingOrder> training = new List<TrainingOrder>();
|
||
private readonly List<TrainingOrderInfo> trainingView = new List<TrainingOrderInfo>();
|
||
private int nextOrderId = 1;
|
||
|
||
// Секция "army" была в сохранении -- старые войска из progress.json переносить не нужно
|
||
public bool RestoredFromSave { get; private set; }
|
||
|
||
// ---------------- Контракт IArmy ----------------
|
||
|
||
public int TypeCount => Types;
|
||
|
||
// Слоты похода: база из таблицы MarchConfig (регламент 31.10: 2, ТЕСТ). Бонус «Военной логистики» -- позже.
|
||
private readonly int baseMarchSlots;
|
||
|
||
public ArmyService(int baseMarchSlots = 2, HospitalRules hospital = null)
|
||
{
|
||
this.baseMarchSlots = Math.Max(1, baseMarchSlots);
|
||
this.hospital = hospital ?? HospitalRules.Default;
|
||
}
|
||
|
||
public HospitalRules Hospital => hospital;
|
||
|
||
public int MarchSlots => baseMarchSlots;
|
||
public int ActiveMarches => marches.Count;
|
||
|
||
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 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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
ClampGarrison();
|
||
if (lost > 0) Changed();
|
||
return lost;
|
||
}
|
||
|
||
public bool TakeForMarch(int marchId, int count)
|
||
{
|
||
// Поход с таким номером уже есть: перезапись потеряла бы его воинов
|
||
if (marches.Exists(m => m.marchId == marchId)) return false;
|
||
// Все слоты похода заняты. Походы из старых сохранений сверх лимита дойдут, но новых не будет
|
||
if (marches.Count >= MarchSlots) return false;
|
||
|
||
// В поход -- только свободные воины, гарнизон остаётся в городе
|
||
var free = new int[Types];
|
||
for (int i = 0; i < Types; i++) free[i] = troops[i] - garrison[i];
|
||
int[] taken = ArmyMath.TakeProportional(free, count);
|
||
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;
|
||
}
|
||
|
||
// Воины возвращаются только из известного похода, и только один раз: запись похода удаляется.
|
||
// Неизвестный поход (повторное событие, испорченное сохранение) не добавляет никого -- иначе солдаты
|
||
// появлялись бы из ниоткуда. Перенос старых сохранений -- отдельно, в ImportLegacy.
|
||
public bool ReturnFromMarch(int marchId, int survivors)
|
||
{
|
||
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
||
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);
|
||
Changed();
|
||
return true;
|
||
}
|
||
|
||
public int[] GetMarchUnits(int marchId)
|
||
{
|
||
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
||
return entry != null ? (int[])entry.units.Clone() : null;
|
||
}
|
||
|
||
// Выжившие точно по типам: бой посчитал потери каждого типа. Больше, чем ушло, вернуться не может.
|
||
// Правила те же, что у ReturnFromMarch: только известный поход, только один раз.
|
||
public bool ReturnUnits(int marchId, int[] survivorUnits)
|
||
{
|
||
return ReturnUnits(marchId, survivorUnits, null, 0);
|
||
}
|
||
|
||
// Выжившие -- в город, раненые -- в госпиталь (решения 31.10, п. 7). Остальные из ушедших погибли.
|
||
// Один солдат в одном месте: выжившие + раненые не больше, чем ушло, по каждому типу.
|
||
public bool ReturnUnits(int marchId, int[] survivorUnits, int[] woundedUnits, long nowMs)
|
||
{
|
||
MarchUnits entry = marches.Find(m => m.marchId == marchId);
|
||
if (entry == null) return false;
|
||
|
||
bool anyWounded = false;
|
||
if (woundedUnits != null) ProcessHealing(nowMs); // сначала освободить места, которые освободились раньше
|
||
for (int i = 0; i < Types; i++)
|
||
{
|
||
int alive = survivorUnits != null && i < survivorUnits.Length ? Math.Max(0, Math.Min(survivorUnits[i], entry.units[i])) : 0;
|
||
int hurt = woundedUnits != null && i < woundedUnits.Length ? Math.Max(0, Math.Min(woundedUnits[i], entry.units[i] - alive)) : 0;
|
||
troops[i] += alive;
|
||
woundedWaiting[i] += hurt;
|
||
if (hurt > 0) anyWounded = true;
|
||
}
|
||
marches.Remove(entry);
|
||
if (anyWounded) FillBeds(nowMs);
|
||
Changed();
|
||
return true;
|
||
}
|
||
|
||
// ---------------- Раненые и лечение ----------------
|
||
|
||
public int WoundedTotal
|
||
{
|
||
get
|
||
{
|
||
int n = 0;
|
||
foreach (var w in woundedWaiting) n += w;
|
||
foreach (var b in healing) n += b.count;
|
||
return n;
|
||
}
|
||
}
|
||
|
||
public int GetWoundedWaiting(int type)
|
||
{
|
||
return type >= 0 && type < Types ? woundedWaiting[type] : 0;
|
||
}
|
||
|
||
public IReadOnlyList<HealingInfo> Healing
|
||
{
|
||
get
|
||
{
|
||
healingView.Clear();
|
||
foreach (var b in healing)
|
||
healingView.Add(new HealingInfo { Id = b.id, Type = b.type, Count = b.count, StartMs = b.startMs, EndMs = b.endMs });
|
||
return healingView;
|
||
}
|
||
}
|
||
|
||
// Места: от здания госпиталя (Поселение), пока его нет -- временные из таблицы
|
||
public int HospitalBeds => GameServices.TryGet(out IHospital h) && h.Beds > 0 ? h.Beds : Math.Max(0, hospital.TemporaryBeds);
|
||
|
||
public int ProcessHealing(long nowMs)
|
||
{
|
||
int done = 0;
|
||
// По времени: самая ранняя группа заканчивает -- на её место в тот же момент ложатся ждущие
|
||
while (true)
|
||
{
|
||
HealingBatch next = null;
|
||
foreach (var b in healing)
|
||
if (b.endMs <= nowMs && (next == null || b.endMs < next.endMs || b.endMs == next.endMs && b.id < next.id)) next = b;
|
||
if (next == null) break;
|
||
|
||
healing.Remove(next); // группа вылечена один раз: повторный расчёт её уже не найдёт
|
||
troops[next.type] += next.count;
|
||
done++;
|
||
EventBus.Publish(new SoldiersHealedEvent { BatchId = next.id, Type = next.type, Count = next.count, TimeMs = next.endMs });
|
||
FillBeds(next.endMs);
|
||
}
|
||
FillBeds(nowMs); // мест могло стать больше (госпиталь улучшили)
|
||
if (done > 0) Changed();
|
||
return done;
|
||
}
|
||
|
||
// Положить ждущих на свободные места в момент atMs: по типам по порядку, каждая группа -- свой срок лечения.
|
||
// Цена лечения (если задана) списывается через Экономику целиком; не хватает -- группа ждёт.
|
||
void FillBeds(long atMs)
|
||
{
|
||
int free = HospitalBeds;
|
||
foreach (var b in healing) free -= b.count;
|
||
for (int type = 0; type < Types && free > 0; type++)
|
||
{
|
||
int n = Math.Min(free, woundedWaiting[type]);
|
||
if (n <= 0) continue;
|
||
if (!PayForHealing(type, n)) continue;
|
||
woundedWaiting[type] -= n;
|
||
free -= n;
|
||
healing.Add(new HealingBatch { id = nextHealId++, type = type, count = n, startMs = atMs, endMs = atMs + hospital.HealMs(type) });
|
||
}
|
||
}
|
||
|
||
bool PayForHealing(int type, int soldiers)
|
||
{
|
||
var perSoldier = hospital.HealCostPerSoldier != null && type < hospital.HealCostPerSoldier.Length ? hospital.HealCostPerSoldier[type] : null;
|
||
if (perSoldier == null || perSoldier.Length == 0) return true; // цена не утверждена -- без цены
|
||
if (!GameServices.TryGet(out IEconomy economy)) return false;
|
||
var cost = new List<ResourceAmount>();
|
||
foreach (var c in perSoldier) if (c.Amount > 0) cost.Add(new ResourceAmount(c.Type, c.Amount * soldiers));
|
||
return cost.Count == 0 || economy.TrySpend(cost);
|
||
}
|
||
|
||
// ---------------- Очередь обучения ----------------
|
||
|
||
public IReadOnlyList<TrainingOrderInfo> Training
|
||
{
|
||
get
|
||
{
|
||
trainingView.Clear();
|
||
foreach (var o in training)
|
||
trainingView.Add(new TrainingOrderInfo { Id = o.id, Type = o.type, Count = o.count, StartMs = o.startMs, EndMs = o.endMs });
|
||
return trainingView;
|
||
}
|
||
}
|
||
|
||
public bool CanQueueTraining(int type, int count, int queueSize, out string error)
|
||
{
|
||
error = null;
|
||
if (type < 0 || type >= Types || count <= 0) error = "Неверный заказ";
|
||
else if (training.Count >= queueSize) error = "Очередь обучения заполнена. Больше казарм -- длиннее очередь";
|
||
else if (GameServices.TryGet(out IPopulation population) && population.Recruitable < count)
|
||
error = population.Recruitable > 0
|
||
? $"Жителей хватит только на {population.Recruitable} воинов"
|
||
: $"Некого нанять: свободных жителей нет (резерв {population.CivilianReserve}). Снимите рабочих или стройте Дома";
|
||
return error == null;
|
||
}
|
||
|
||
public bool TryQueueTraining(int type, int count, long durationMs, int queueSize, long nowMs, out string error)
|
||
{
|
||
if (!CanQueueTraining(type, count, queueSize, out error)) return false;
|
||
|
||
// Воин -- житель города: люди уходят в Казармы сейчас, в Армию -- когда обучатся
|
||
bool recruited = false;
|
||
if (GameServices.TryGet(out IPopulation population))
|
||
{
|
||
if (!population.TryRecruit(count)) { error = "Некого нанять: жителей не хватает"; return false; }
|
||
recruited = true;
|
||
}
|
||
|
||
// Заказы идут друг за другом: новый начинается, когда закончится последний
|
||
long start = training.Count > 0 ? Math.Max(nowMs, training[training.Count - 1].endMs) : nowMs;
|
||
training.Add(new TrainingOrder
|
||
{
|
||
id = nextOrderId++, type = type, count = count,
|
||
startMs = start, endMs = start + Math.Max(0, durationMs), recruited = recruited
|
||
});
|
||
Changed();
|
||
return true;
|
||
}
|
||
|
||
public int ProcessTraining(long nowMs)
|
||
{
|
||
int done = 0;
|
||
while (training.Count > 0 && training[0].endMs <= nowMs)
|
||
{
|
||
TrainingOrder o = training[0];
|
||
if (o.recruited)
|
||
{
|
||
// Новобранцы переходят из Населения в Армию одним действием (Население само зовёт AddTroops).
|
||
// Населения ещё нет (запуск) -- подождём: заказ остаётся, никто не теряется.
|
||
if (!GameServices.TryGet(out IPopulation population)) break;
|
||
if (!population.CompleteTraining(o.type, o.count))
|
||
{
|
||
// Население не нашло этих новобранцев (испорченное сохранение): заказ снимается без воинов --
|
||
// иначе люди появились бы из ниоткуда, а очередь встала бы навсегда
|
||
training.RemoveAt(0);
|
||
Changed();
|
||
continue;
|
||
}
|
||
}
|
||
else AddTroops(o.type, o.count); // старый заказ: людей под него не брали
|
||
|
||
training.RemoveAt(0); // заказ закончен один раз: повторный расчёт его уже не найдёт
|
||
done++;
|
||
EventBus.Publish(new TroopsTrainedEvent { OrderId = o.id, Type = o.type, Count = o.count, TimeMs = o.endMs });
|
||
}
|
||
return done;
|
||
}
|
||
|
||
public int FinishAllTraining(long nowMs)
|
||
{
|
||
foreach (var o in training)
|
||
{
|
||
o.endMs = Math.Min(o.endMs, nowMs);
|
||
o.startMs = Math.Min(o.startMs, o.endMs);
|
||
}
|
||
return ProcessTraining(nowMs);
|
||
}
|
||
|
||
// Перенос очереди из старого progress.json (её вёл GameProgress). Время уже переведено в мс часов игры.
|
||
public void ImportLegacyTraining(IEnumerable<(int type, int count, long startMs, long endMs, bool recruited)> orders)
|
||
{
|
||
if (orders == null) return;
|
||
foreach (var o in orders)
|
||
{
|
||
if (o.type < 0 || o.type >= Types || o.count <= 0) continue;
|
||
training.Add(new TrainingOrder
|
||
{
|
||
id = nextOrderId++, type = o.type, count = o.count,
|
||
startMs = o.startMs, endMs = Math.Max(o.startMs, o.endMs), recruited = o.recruited
|
||
});
|
||
}
|
||
Changed();
|
||
}
|
||
|
||
void ClampGarrison()
|
||
{
|
||
for (int i = 0; i < Types; i++) garrison[i] = Math.Max(0, Math.Min(garrison[i], troops[i]));
|
||
}
|
||
|
||
void Changed()
|
||
{
|
||
EventBus.Publish(new TroopsChangedEvent { AtHome = SoldiersAtHome, InMarches = TroopsInMarches, Garrison = GarrisonTotal, Wounded = WoundedTotal });
|
||
}
|
||
|
||
// ---------------- Перенос из старого 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]);
|
||
Array.Clear(garrison, 0, Types);
|
||
|
||
marches.Clear();
|
||
if (legacyMarches != null)
|
||
{
|
||
foreach (var m in legacyMarches)
|
||
{
|
||
if (marches.Exists(x => x.marchId == m.Key)) continue; // один поход -- одна запись
|
||
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=<пехота>,<лучники>,<кавалерия>,<осадные>
|
||
// garrison=<пехота>,<лучники>,<кавалерия>,<осадные> (из troops; нет строки -- гарнизона нет)
|
||
// march=<id похода>:<пехота>,<лучники>,<кавалерия>,<осадные>
|
||
// train=<id>|<тип>|<сколько>|<начало, мс>|<конец, мс>|<люди взяты 1/0> (по порядку очереди)
|
||
// trainNext=<номер следующего заказа>
|
||
// wounded=<пехота>,<лучники>,<кавалерия>,<осадные> (ждут места в госпитале)
|
||
// heal=<id>|<тип>|<сколько>|<начало, мс>|<конец, мс>
|
||
// healNext=<номер следующей группы лечения>
|
||
|
||
public string SaveKey => "army";
|
||
|
||
public string Capture()
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.Append("troops=").Append(Join(troops)).Append('\n');
|
||
sb.Append("garrison=").Append(Join(garrison)).Append('\n');
|
||
foreach (var m in marches)
|
||
sb.Append("march=").Append(m.marchId.ToString(CultureInfo.InvariantCulture)).Append(':').Append(Join(m.units)).Append('\n');
|
||
foreach (var o in training)
|
||
sb.Append("train=").Append(string.Join("|", new[]
|
||
{
|
||
N(o.id), N(o.type), N(o.count),
|
||
o.startMs.ToString(CultureInfo.InvariantCulture), o.endMs.ToString(CultureInfo.InvariantCulture), o.recruited ? "1" : "0"
|
||
})).Append('\n');
|
||
sb.Append("trainNext=").Append(N(nextOrderId)).Append('\n');
|
||
sb.Append("wounded=").Append(Join(woundedWaiting)).Append('\n');
|
||
foreach (var b in healing)
|
||
sb.Append("heal=").Append(string.Join("|", new[]
|
||
{
|
||
N(b.id), N(b.type), N(b.count), b.startMs.ToString(CultureInfo.InvariantCulture), b.endMs.ToString(CultureInfo.InvariantCulture)
|
||
})).Append('\n');
|
||
sb.Append("healNext=").Append(N(nextHealId)).Append('\n');
|
||
return sb.ToString();
|
||
}
|
||
|
||
public void Restore(string data)
|
||
{
|
||
Array.Clear(troops, 0, Types);
|
||
Array.Clear(garrison, 0, Types);
|
||
marches.Clear();
|
||
training.Clear();
|
||
nextOrderId = 1;
|
||
Array.Clear(woundedWaiting, 0, Types);
|
||
healing.Clear();
|
||
nextHealId = 1;
|
||
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("garrison="))
|
||
{
|
||
int[] g = Parse(line.Substring("garrison=".Length));
|
||
Array.Copy(g, garrison, 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.Exists(x => x.marchId == id)) // один поход -- одна запись
|
||
marches.Add(new MarchUnits { marchId = id, units = Parse(parts[1]) });
|
||
}
|
||
else if (line.StartsWith("train="))
|
||
{
|
||
string[] p = line.Substring("train=".Length).Split('|');
|
||
if (p.Length == 6 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int oid)
|
||
&& int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int type) && type >= 0 && type < Types
|
||
&& int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int count) && count > 0
|
||
&& long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long start)
|
||
&& long.TryParse(p[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out long end)
|
||
&& !training.Exists(x => x.id == oid)) // один заказ -- одна запись
|
||
{
|
||
training.Add(new TrainingOrder { id = oid, type = type, count = count, startMs = start, endMs = end, recruited = p[5] == "1" });
|
||
nextOrderId = Math.Max(nextOrderId, oid + 1);
|
||
}
|
||
}
|
||
else if (line.StartsWith("wounded="))
|
||
{
|
||
int[] w = Parse(line.Substring("wounded=".Length));
|
||
Array.Copy(w, woundedWaiting, Types);
|
||
}
|
||
else if (line.StartsWith("heal="))
|
||
{
|
||
string[] p = line.Substring("heal=".Length).Split('|');
|
||
if (p.Length == 5 && int.TryParse(p[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int hid)
|
||
&& int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int htype) && htype >= 0 && htype < Types
|
||
&& int.TryParse(p[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int hcount) && hcount > 0
|
||
&& long.TryParse(p[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out long hstart)
|
||
&& long.TryParse(p[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out long hend)
|
||
&& !healing.Exists(x => x.id == hid))
|
||
{
|
||
healing.Add(new HealingBatch { id = hid, type = htype, count = hcount, startMs = hstart, endMs = hend });
|
||
nextHealId = Math.Max(nextHealId, hid + 1);
|
||
}
|
||
}
|
||
else if (line.StartsWith("healNext=")
|
||
&& int.TryParse(line.Substring("healNext=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int hnext))
|
||
nextHealId = Math.Max(nextHealId, hnext);
|
||
else if (line.StartsWith("trainNext=")
|
||
&& int.TryParse(line.Substring("trainNext=".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out int next))
|
||
nextOrderId = Math.Max(nextOrderId, next);
|
||
}
|
||
}
|
||
ClampGarrison(); // гарнизон не больше войск, даже если файл правили руками
|
||
Changed();
|
||
}
|
||
|
||
// Новая игра: войск нет, гарнизона и походов нет
|
||
public void ResetState()
|
||
{
|
||
Array.Clear(troops, 0, Types);
|
||
Array.Clear(garrison, 0, Types);
|
||
marches.Clear();
|
||
training.Clear();
|
||
nextOrderId = 1;
|
||
Array.Clear(woundedWaiting, 0, Types);
|
||
healing.Clear();
|
||
nextHealId = 1;
|
||
Changed();
|
||
}
|
||
|
||
static string N(int v) => v.ToString(CultureInfo.InvariantCulture);
|
||
|
||
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;
|
||
}
|
||
}
|