2026-10-27 18:04:27 +03:00
|
|
|
using System;
|
2026-10-28 22:11:36 +03:00
|
|
|
using System.Collections.Generic;
|
2026-10-27 18:04:27 +03:00
|
|
|
using System.Globalization;
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
// МОДУЛЬ НАСЕЛЕНИЯ: сколько в городе людей, кто где работает, сколько едят и как прирастают.
|
2026-10-27 18:51:35 +03:00
|
|
|
// Документ: Docs/Modules/Population.md. Без Unity: правила те же, что будут на сервере.
|
2026-10-27 18:04:27 +03:00
|
|
|
//
|
|
|
|
|
// Люди города = жители (этот модуль) + воины (модуль Армии). Воин -- тоже человек: он ест (0.2 еды в час)
|
|
|
|
|
// и входит в общее население, но хранится в Армии, чтобы один человек не был записан в двух местах.
|
2026-10-31 19:34:33 +03:00
|
|
|
// Жители: работать может любой житель (общего ограничения нет, регламент 31.10) -- рабочих ограничивают
|
|
|
|
|
// свободные жители и рабочие места в зданиях. Стартовый город: 120 рабочих из 270 жителей.
|
2026-10-31 20:52:28 +03:00
|
|
|
//
|
|
|
|
|
// Каждый человек ровно в одной группе (регламент 31.10):
|
|
|
|
|
// свободный житель = Civilians - Workers (Idle)
|
|
|
|
|
// рабочий = Workers (jobs[], только из жителей: SetWorkers берёт не больше свободных)
|
|
|
|
|
// новобранец = InTraining (TryRecruit забирает только свободных, рабочих не трогает)
|
2026-11-01 13:32:25 +01:00
|
|
|
// воин = Soldiers (хранит Армия: в городе, в походах и раненые; RecruitsTrained убирает новобранцев до зачисления в Армию)
|
2026-10-31 20:52:28 +03:00
|
|
|
// Total = Civilians + InTraining + Soldiers -- сумма всех групп.
|
|
|
|
|
// Рабочие места сами не заполняются: только стартовая расстановка (AutoFillAll, один раз), дальше -- игрок.
|
2026-10-28 22:11:36 +03:00
|
|
|
// Найм воина: свободный житель уходит в Казармы (InTraining), после обучения его считает Армия. Резерв не нанять.
|
2026-10-27 18:04:27 +03:00
|
|
|
//
|
|
|
|
|
// Время -- только из GameClock. Расчёт идёт кусками по 10 минут: раз в несколько секунд в игре,
|
|
|
|
|
// при входе в игру (офлайн-прогресс) и при перемотке в отладочной панели -- правила одни и те же.
|
2026-10-27 18:51:35 +03:00
|
|
|
// В каждом куске: сначала добыча (IProduction), потом еда, потом прирост.
|
2026-10-27 18:04:27 +03:00
|
|
|
public sealed class PopulationService : IPopulation, ISaveParticipant
|
|
|
|
|
{
|
|
|
|
|
const long SecondsPerHour = 3600;
|
|
|
|
|
const long StepSeconds = 600; // офлайн-время считаем кусками по 10 минут -- так же, как в игре
|
|
|
|
|
const double Epsilon = 1e-9;
|
|
|
|
|
|
|
|
|
|
readonly PopulationRules rules;
|
|
|
|
|
|
|
|
|
|
int civilians;
|
2026-10-27 18:51:35 +03:00
|
|
|
readonly int[] jobs = new int[WorkJobs.Count]; // рабочих по профессиям
|
|
|
|
|
bool jobsInitialized; // рабочих хоть раз расставляли (старому сохранению расставим сами)
|
|
|
|
|
int houseLevels; // сумма уровней Домов в городе (сообщает App/CitySync)
|
2026-10-27 18:04:27 +03:00
|
|
|
double growthProgress; // накопленная доля следующего человека (0..1)
|
|
|
|
|
double foodOwed; // съедено, но ещё не списано со склада (меньше 1 еды)
|
|
|
|
|
long lastTickUnix; // до какого момента население уже посчитано; 0 -- ещё ни разу
|
|
|
|
|
bool starving;
|
2026-10-28 22:11:36 +03:00
|
|
|
int inTraining; // люди, взятые из жителей в Казармы и ещё не обученные
|
|
|
|
|
// История голода: отрезки [начало, конец) в Unix-секундах, конец 0 -- голод идёт сейчас.
|
|
|
|
|
// Нужна Бою: поход мог дойти до цели, пока игра была закрыта, и голод считается на момент боя.
|
|
|
|
|
readonly List<long[]> famine = new List<long[]>();
|
2026-10-27 18:04:27 +03:00
|
|
|
bool startingKitGiven; // стартовое войско и ресурсы новой игры уже выданы (или это старое сохранение)
|
|
|
|
|
|
|
|
|
|
public PopulationService(PopulationRules rules)
|
|
|
|
|
{
|
|
|
|
|
this.rules = rules ?? new PopulationRules();
|
|
|
|
|
SetStartState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PopulationRules Rules => rules;
|
|
|
|
|
public long LastTickUnix => lastTickUnix;
|
|
|
|
|
public bool RestoredFromSave { get; private set; }
|
2026-10-27 18:51:35 +03:00
|
|
|
public bool JobsInitialized => jobsInitialized;
|
|
|
|
|
public int HouseLevels => houseLevels;
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
// ---------------- Контракт IPopulation ----------------
|
|
|
|
|
|
|
|
|
|
public int Civilians => civilians;
|
2026-10-27 18:51:35 +03:00
|
|
|
|
|
|
|
|
public int Workers
|
|
|
|
|
{
|
|
|
|
|
get { int n = 0; foreach (var j in jobs) n += j; return n; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int Idle => Math.Max(0, civilians - Workers);
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
// Работать может любой житель: общего ограничения нет (регламент 31.10).
|
|
|
|
|
// Рабочих ограничивают свободные жители и рабочие места (IProduction.Slots).
|
|
|
|
|
public int Workforce => civilians;
|
2026-10-27 18:51:35 +03:00
|
|
|
|
|
|
|
|
public int FreeWorkers => Math.Max(0, Workforce - Workers);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
public int Soldiers
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (!GameServices.TryGet(out IArmy army)) return 0;
|
2026-11-01 13:32:25 +01:00
|
|
|
return army.SoldiersAtHome + army.TroopsInMarches + army.WoundedTotal; // раненые -- тоже люди (решения 31.10, п. 7)
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
public int Total => civilians + inTraining + Soldiers;
|
|
|
|
|
|
|
|
|
|
// ---------------- Найм воинов ----------------
|
|
|
|
|
|
|
|
|
|
public int InTraining => inTraining;
|
|
|
|
|
|
|
|
|
|
public int CivilianReserve
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
int level = GameServices.TryGet(out ISettlement settlement) ? Math.Max(1, settlement.TownHallLevel) : 1;
|
2026-10-31 19:34:33 +03:00
|
|
|
int[] table = rules.CivilianReserveByTownHallLevel;
|
|
|
|
|
if (table == null || table.Length == 0) return 0;
|
|
|
|
|
return Math.Max(0, table[Math.Min(level, table.Length) - 1]);
|
2026-10-28 22:11:36 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Берём только свободных (Idle -- не на работе): рабочих сначала надо снять с профессии
|
|
|
|
|
public int Recruitable => Math.Max(0, Idle - CivilianReserve) / Math.Max(1, rules.PeoplePerSoldier);
|
|
|
|
|
|
|
|
|
|
public bool TryRecruit(int soldiers)
|
|
|
|
|
{
|
|
|
|
|
if (soldiers <= 0 || soldiers > Recruitable) return false;
|
|
|
|
|
int people = soldiers * Math.Max(1, rules.PeoplePerSoldier);
|
|
|
|
|
civilians -= people;
|
|
|
|
|
inTraining += people;
|
|
|
|
|
ClampJobs(); // жителей меньше -- рабочих тоже может стать меньше
|
|
|
|
|
Changed();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 20:52:28 +03:00
|
|
|
public bool CompleteTraining(int troopType, int soldiers)
|
|
|
|
|
{
|
|
|
|
|
if (soldiers <= 0) return false;
|
|
|
|
|
int people = soldiers * Math.Max(1, rules.PeoplePerSoldier);
|
|
|
|
|
if (people > inTraining) return false; // столько новобранцев нет (повтор или ошибка)
|
|
|
|
|
if (!GameServices.TryGet(out IArmy army)) return false;
|
|
|
|
|
|
|
|
|
|
// Сначала убираем из Казарм, потом отдаём в Армию: событие Армии увидит уже правильную сумму.
|
|
|
|
|
// Армия не приняла -- возвращаем новобранцев, как было.
|
|
|
|
|
inTraining -= people;
|
|
|
|
|
int before = army.SoldiersAtHome;
|
|
|
|
|
army.AddTroops(troopType, soldiers);
|
|
|
|
|
if (army.SoldiersAtHome - before != soldiers)
|
|
|
|
|
{
|
|
|
|
|
inTraining += people;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
Changed();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
public void RecruitsTrained(int soldiers)
|
|
|
|
|
{
|
|
|
|
|
if (soldiers <= 0) return;
|
|
|
|
|
// Заказы, начатые до найма из жителей (старое сохранение), людей не брали -- уходить в минус нельзя
|
|
|
|
|
inTraining = Math.Max(0, inTraining - soldiers * Math.Max(1, rules.PeoplePerSoldier));
|
|
|
|
|
Changed();
|
|
|
|
|
}
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
public int HousingCap
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
int level = GameServices.TryGet(out ISettlement settlement) ? Math.Max(1, settlement.TownHallLevel) : 1;
|
2026-10-27 18:51:35 +03:00
|
|
|
return rules.HousingBase + rules.HousingPerTownHallLevel * (level - 1) + rules.HousingPerHouseLevel * houseLevels;
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float FoodPerHour => (float)FoodPerHourExact(Soldiers);
|
|
|
|
|
public float GrowthPerHour => starving || Total >= HousingCap ? 0f : (float)rules.GrowthPerHour;
|
|
|
|
|
public bool IsStarving => starving;
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
public bool WasStarvingAt(long unixTime)
|
|
|
|
|
{
|
|
|
|
|
if (lastTickUnix == 0 || unixTime >= lastTickUnix) return starving; // ещё не посчитано -- как сейчас
|
|
|
|
|
foreach (var f in famine)
|
|
|
|
|
if (unixTime >= f[0] && (f[1] == 0 || unixTime < f[1])) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Голод начался или кончился в момент unix (вызывается из расчёта на каждом шаге)
|
|
|
|
|
void RecordFamine(bool nowStarving, long unix)
|
|
|
|
|
{
|
|
|
|
|
long[] open = famine.Count > 0 && famine[famine.Count - 1][1] == 0 ? famine[famine.Count - 1] : null;
|
|
|
|
|
if (nowStarving && open == null) famine.Add(new[] { unix, 0L });
|
|
|
|
|
else if (!nowStarving && open != null) open[1] = unix;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Отрезки, закончившиеся раньше, чем 48 часов назад, Бою уже не понадобятся
|
|
|
|
|
void PruneFamine(long unixNow)
|
|
|
|
|
{
|
|
|
|
|
long keepFrom = unixNow - (long)rules.MaxOfflineHours * SecondsPerHour;
|
|
|
|
|
famine.RemoveAll(f => f[1] != 0 && f[1] <= keepFrom);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
double FoodPerHourExact(int soldiers)
|
|
|
|
|
{
|
2026-10-28 22:11:36 +03:00
|
|
|
// Обучающиеся в Казармах едят как воины
|
2026-11-04 15:13:02 +01:00
|
|
|
return civilians * rules.FoodPerCivilianPerHour + SoldiersFood(soldiers) + inTraining * rules.FoodPerSoldierPerHour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Воины (в городе, в походах и раненые) едят по норме своего типа, если она задана (решения 01.11, п. 4)
|
|
|
|
|
double SoldiersFood(int soldiers)
|
|
|
|
|
{
|
|
|
|
|
var table = rules.FoodPerTroopTypePerHour;
|
|
|
|
|
if (table == null || table.Length == 0 || !GameServices.TryGet(out IArmy army)) return soldiers * rules.FoodPerSoldierPerHour;
|
|
|
|
|
double food = 0;
|
|
|
|
|
for (int t = 0; t < army.TypeCount; t++)
|
|
|
|
|
food += army.GetAllTroops(t) * (t < table.Length ? table[t] : rules.FoodPerSoldierPerHour);
|
|
|
|
|
return food;
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
public int GetWorkers(WorkJob job)
|
|
|
|
|
{
|
|
|
|
|
int j = (int)job;
|
|
|
|
|
return j >= 0 && j < WorkJobs.Count ? jobs[j] : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int SetWorkers(WorkJob job, int count)
|
|
|
|
|
{
|
|
|
|
|
int j = (int)job;
|
|
|
|
|
if (j < 0 || j >= WorkJobs.Count) return 0;
|
|
|
|
|
|
2026-10-31 20:52:28 +03:00
|
|
|
int limit = jobs[j] + FreeWorkers; // только свободные жители: не воины, не новобранцы, не уже работающие
|
2026-10-27 18:51:35 +03:00
|
|
|
int slots = SlotsFor(job);
|
|
|
|
|
if (slots >= 0) limit = Math.Min(limit, slots); // и больше, чем мест в зданиях
|
|
|
|
|
int value = Math.Max(0, Math.Min(count, limit));
|
|
|
|
|
if (value != jobs[j])
|
|
|
|
|
{
|
|
|
|
|
jobs[j] = value;
|
|
|
|
|
jobsInitialized = true;
|
|
|
|
|
WorkersChanged();
|
|
|
|
|
}
|
|
|
|
|
return jobs[j];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Мест для профессии: -1 -- модуль добычи не подключён (тесты без него), ограничения нет
|
|
|
|
|
static int SlotsFor(WorkJob job)
|
|
|
|
|
{
|
|
|
|
|
return GameServices.TryGet(out IProduction production) ? production.Slots(job) : -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
public void Tick(long unixNow)
|
|
|
|
|
{
|
|
|
|
|
if (lastTickUnix == 0)
|
|
|
|
|
{
|
|
|
|
|
lastTickUnix = unixNow; // первый расчёт: отсюда и считаем
|
|
|
|
|
Changed();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long elapsed = unixNow - lastTickUnix;
|
|
|
|
|
if (elapsed < 0)
|
|
|
|
|
{
|
|
|
|
|
// Часы ушли назад (сброс отладочной перемотки, перевод часов на телефоне) -- просто продолжаем с "сейчас"
|
|
|
|
|
lastTickUnix = unixNow;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (elapsed == 0) return;
|
|
|
|
|
|
|
|
|
|
long max = (long)rules.MaxOfflineHours * SecondsPerHour;
|
|
|
|
|
if (elapsed > max) elapsed = max;
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
Simulate(unixNow - elapsed, elapsed);
|
2026-10-27 18:04:27 +03:00
|
|
|
lastTickUnix = unixNow;
|
2026-10-28 22:11:36 +03:00
|
|
|
PruneFamine(unixNow);
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------- Расчёт ----------------
|
|
|
|
|
|
2026-10-28 22:11:36 +03:00
|
|
|
void Simulate(long startUnix, long seconds)
|
2026-10-27 18:04:27 +03:00
|
|
|
{
|
2026-10-27 18:51:35 +03:00
|
|
|
if (!GameServices.TryGet(out IEconomy economy)) return; // без склада считать нечего
|
|
|
|
|
GameServices.TryGet(out IProduction production);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
int totalBefore = Total;
|
|
|
|
|
bool starvingBefore = starving;
|
|
|
|
|
int soldiers = Soldiers;
|
|
|
|
|
|
|
|
|
|
long remaining = seconds;
|
2026-10-28 22:11:36 +03:00
|
|
|
long stepStart = startUnix;
|
2026-10-27 18:04:27 +03:00
|
|
|
while (remaining > 0)
|
|
|
|
|
{
|
|
|
|
|
long step = Math.Min(remaining, StepSeconds);
|
|
|
|
|
remaining -= step;
|
|
|
|
|
double hours = step / (double)SecondsPerHour;
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
// 0. Добыча: рабочие приносят ресурсы (в том числе еду) -- до того, как город поест
|
|
|
|
|
production?.Produce(hours);
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
// 1. Едим. Со склада списываются только целые единицы, доли копятся здесь (foodOwed):
|
|
|
|
|
// склад хранит float, и тысячи мелких списаний по 0.02 давали бы ошибку округления.
|
|
|
|
|
foodOwed += FoodPerHourExact(soldiers) * hours;
|
|
|
|
|
int portion = (int)Math.Floor(foodOwed + Epsilon);
|
|
|
|
|
double have = economy.Get(ResourceType.Food);
|
|
|
|
|
bool fed;
|
|
|
|
|
if (portion <= 0)
|
|
|
|
|
{
|
|
|
|
|
fed = have >= foodOwed - Epsilon; // пока едим из уже взятого -- голод, только если склад пуст
|
|
|
|
|
}
|
|
|
|
|
else if (have >= portion)
|
|
|
|
|
{
|
|
|
|
|
economy.Add(ResourceType.Food, -portion);
|
|
|
|
|
foodOwed -= portion;
|
|
|
|
|
fed = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
int rest = (int)Math.Floor(have);
|
|
|
|
|
if (rest > 0) economy.Add(ResourceType.Food, -rest); // доедаем остатки
|
|
|
|
|
foodOwed = 0; // долг за голодное время не копим
|
|
|
|
|
fed = false;
|
|
|
|
|
}
|
|
|
|
|
starving = !fed;
|
2026-10-28 22:11:36 +03:00
|
|
|
RecordFamine(starving, stepStart); // голод начался (кончился) в этом куске -- запоминаем его начало
|
|
|
|
|
stepStart += step;
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
// 2. Прирост -- только сытым и только до предела жилья. Что ещё бывает при голоде -- решит команда.
|
|
|
|
|
if (fed)
|
|
|
|
|
{
|
2026-10-28 22:11:36 +03:00
|
|
|
int room = HousingCap - (civilians + inTraining + soldiers);
|
2026-10-27 18:04:27 +03:00
|
|
|
if (room > 0)
|
|
|
|
|
{
|
|
|
|
|
growthProgress += rules.GrowthPerHour * hours;
|
|
|
|
|
int born = (int)Math.Floor(growthProgress + Epsilon);
|
|
|
|
|
if (born > 0)
|
|
|
|
|
{
|
|
|
|
|
growthProgress = Math.Max(0, growthProgress - born);
|
|
|
|
|
civilians += Math.Min(born, room);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
growthProgress = 0; // упёрлись в предел -- не копим "запас" людей
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (starving != starvingBefore) EventBus.Publish(new StarvationChangedEvent { IsStarving = starving });
|
|
|
|
|
if (Total != totalBefore) Changed();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
// ---------------- Рабочие: расстановка ----------------
|
|
|
|
|
|
2026-10-31 20:52:28 +03:00
|
|
|
// Стартовая расстановка -- единственное автоматическое заполнение мест (регламент 31.10).
|
|
|
|
|
// Один раз: в новой игре или при первой загрузке старого сохранения (пока JobsInitialized == false).
|
|
|
|
|
// Свободных -- поровну по всем местам, всего не больше StartWorkers (120). Новые места после стройки
|
|
|
|
|
// и улучшения остаются пустыми, пока игрок сам не поставит рабочих.
|
2026-10-27 18:51:35 +03:00
|
|
|
public void AutoFillAll()
|
|
|
|
|
{
|
|
|
|
|
bool changed = false;
|
|
|
|
|
bool progress = true;
|
2026-10-31 19:34:33 +03:00
|
|
|
int cap = Math.Max(Workers, rules.StartWorkers);
|
|
|
|
|
while (FreeWorkers > 0 && Workers < cap && progress)
|
2026-10-27 18:51:35 +03:00
|
|
|
{
|
|
|
|
|
progress = false;
|
2026-10-31 19:34:33 +03:00
|
|
|
for (int j = 0; j < WorkJobs.Count && FreeWorkers > 0 && Workers < cap; j++)
|
2026-10-27 18:51:35 +03:00
|
|
|
{
|
|
|
|
|
int slots = SlotsFor((WorkJob)j);
|
|
|
|
|
if (slots >= 0 && jobs[j] >= slots) continue;
|
|
|
|
|
jobs[j]++;
|
|
|
|
|
progress = changed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
jobsInitialized = true;
|
|
|
|
|
if (changed) WorkersChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Мест стало меньше или работников меньше (призыв в будущем) -- снять лишних
|
|
|
|
|
public void ClampJobs()
|
|
|
|
|
{
|
|
|
|
|
bool changed = false;
|
|
|
|
|
for (int j = 0; j < WorkJobs.Count; j++)
|
|
|
|
|
{
|
|
|
|
|
int slots = SlotsFor((WorkJob)j);
|
|
|
|
|
if (slots >= 0 && jobs[j] > slots) { jobs[j] = slots; changed = true; }
|
|
|
|
|
}
|
|
|
|
|
int over = Workers - Workforce;
|
|
|
|
|
for (int j = WorkJobs.Count - 1; j >= 0 && over > 0; j--)
|
|
|
|
|
{
|
|
|
|
|
int take = Math.Min(over, jobs[j]);
|
|
|
|
|
jobs[j] -= take; over -= take;
|
|
|
|
|
if (take > 0) changed = true;
|
|
|
|
|
}
|
|
|
|
|
if (changed) WorkersChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Сумма уровней Домов (каждый уровень прибавляет к пределу жилья)
|
|
|
|
|
public void SetHouseLevels(int levels)
|
|
|
|
|
{
|
|
|
|
|
levels = Math.Max(0, levels);
|
|
|
|
|
if (levels == houseLevels) return;
|
|
|
|
|
houseLevels = levels;
|
|
|
|
|
Changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WorkersChanged()
|
|
|
|
|
{
|
|
|
|
|
EventBus.Publish(new WorkersChangedEvent { Workers = Workers, FreeWorkers = FreeWorkers });
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:04:27 +03:00
|
|
|
// ---------------- Связь с остальной игрой (вызывает App/GameModules) ----------------
|
|
|
|
|
|
|
|
|
|
// Сохранение загружено: досчитать офлайн-время. Старое сохранение без секции населения -- стартовый набор не выдаём.
|
|
|
|
|
public void OnGameLoaded(long unixNow)
|
|
|
|
|
{
|
|
|
|
|
startingKitGiven = true;
|
|
|
|
|
Tick(unixNow);
|
|
|
|
|
Changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Новая игра (нет файла) или сброс: стартовый набор (войско, ресурсы) выдаётся один раз.
|
|
|
|
|
// true -- выдать сейчас; false -- уже выдан или это старое сохранение.
|
|
|
|
|
public bool TryTakeStartingKit()
|
|
|
|
|
{
|
|
|
|
|
if (startingKitGiven) return false;
|
|
|
|
|
startingKitGiven = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-27 18:51:35 +03:00
|
|
|
// Изменилась Армия, Ратуша или Дома -- пересчитать общее число людей для интерфейса
|
2026-10-27 18:04:27 +03:00
|
|
|
public void Changed()
|
|
|
|
|
{
|
|
|
|
|
EventBus.Publish(new PopulationChangedEvent
|
|
|
|
|
{
|
|
|
|
|
Total = Total,
|
|
|
|
|
Civilians = civilians,
|
|
|
|
|
Soldiers = Soldiers,
|
|
|
|
|
HousingCap = HousingCap
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SetStartState()
|
|
|
|
|
{
|
|
|
|
|
civilians = Math.Max(0, rules.StartCivilians);
|
2026-10-27 18:51:35 +03:00
|
|
|
Array.Clear(jobs, 0, jobs.Length);
|
|
|
|
|
jobsInitialized = false;
|
|
|
|
|
houseLevels = 0;
|
2026-10-27 18:04:27 +03:00
|
|
|
growthProgress = 0;
|
|
|
|
|
foodOwed = 0;
|
|
|
|
|
lastTickUnix = 0;
|
|
|
|
|
starving = false;
|
2026-10-28 22:11:36 +03:00
|
|
|
inTraining = 0;
|
|
|
|
|
famine.Clear();
|
2026-10-27 18:04:27 +03:00
|
|
|
startingKitGiven = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------- Сохранение (секция "population") ----------------
|
|
|
|
|
// Модуль без Unity, поэтому формат -- простые строки "ключ=значение", как у Армии.
|
|
|
|
|
|
|
|
|
|
public string SaveKey => "population";
|
|
|
|
|
|
|
|
|
|
public string Capture()
|
|
|
|
|
{
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.Append("civilians=").Append(civilians.ToString(CultureInfo.InvariantCulture)).Append('\n');
|
2026-10-27 18:51:35 +03:00
|
|
|
var parts = new string[jobs.Length];
|
|
|
|
|
for (int i = 0; i < jobs.Length; i++) parts[i] = jobs[i].ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
sb.Append("jobs=").Append(string.Join(",", parts)).Append('\n');
|
|
|
|
|
sb.Append("jobsInit=").Append(jobsInitialized ? "1" : "0").Append('\n');
|
|
|
|
|
sb.Append("houses=").Append(houseLevels.ToString(CultureInfo.InvariantCulture)).Append('\n');
|
2026-10-27 18:04:27 +03:00
|
|
|
sb.Append("growth=").Append(growthProgress.ToString("R", CultureInfo.InvariantCulture)).Append('\n');
|
|
|
|
|
sb.Append("food=").Append(foodOwed.ToString("R", CultureInfo.InvariantCulture)).Append('\n');
|
|
|
|
|
sb.Append("lastTick=").Append(lastTickUnix.ToString(CultureInfo.InvariantCulture)).Append('\n');
|
|
|
|
|
sb.Append("starving=").Append(starving ? "1" : "0").Append('\n');
|
2026-10-28 22:11:36 +03:00
|
|
|
sb.Append("training=").Append(inTraining.ToString(CultureInfo.InvariantCulture)).Append('\n');
|
|
|
|
|
// famine=начало-конец;начало-конец (конец 0 -- голод идёт сейчас)
|
|
|
|
|
var spans = new List<string>();
|
|
|
|
|
foreach (var f in famine)
|
|
|
|
|
spans.Add(f[0].ToString(CultureInfo.InvariantCulture) + "-" + f[1].ToString(CultureInfo.InvariantCulture));
|
|
|
|
|
sb.Append("famine=").Append(string.Join(";", spans.ToArray())).Append('\n');
|
2026-10-27 18:04:27 +03:00
|
|
|
sb.Append("kit=").Append(startingKitGiven ? "1" : "0").Append('\n');
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Restore(string data)
|
|
|
|
|
{
|
|
|
|
|
SetStartState();
|
|
|
|
|
RestoredFromSave = true;
|
|
|
|
|
if (string.IsNullOrEmpty(data)) return;
|
|
|
|
|
|
|
|
|
|
foreach (var raw in data.Split('\n'))
|
|
|
|
|
{
|
|
|
|
|
string line = raw.Trim();
|
|
|
|
|
int eq = line.IndexOf('=');
|
|
|
|
|
if (eq <= 0) continue;
|
|
|
|
|
string key = line.Substring(0, eq);
|
|
|
|
|
string value = line.Substring(eq + 1);
|
|
|
|
|
|
|
|
|
|
switch (key)
|
|
|
|
|
{
|
|
|
|
|
case "civilians": civilians = Math.Max(0, ParseInt(value, civilians)); break;
|
2026-10-27 18:51:35 +03:00
|
|
|
case "jobs":
|
|
|
|
|
string[] p = value.Split(',');
|
|
|
|
|
for (int i = 0; i < jobs.Length && i < p.Length; i++) jobs[i] = Math.Max(0, ParseInt(p[i].Trim(), 0));
|
|
|
|
|
break;
|
|
|
|
|
case "jobsInit": jobsInitialized = value == "1"; break;
|
|
|
|
|
case "houses": houseLevels = Math.Max(0, ParseInt(value, 0)); break;
|
2026-10-27 18:04:27 +03:00
|
|
|
case "growth":
|
|
|
|
|
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double g)) growthProgress = Math.Max(0, Math.Min(0.999, g));
|
|
|
|
|
break;
|
|
|
|
|
case "food":
|
|
|
|
|
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double f)) foodOwed = Math.Max(0, Math.Min(1, f));
|
|
|
|
|
break;
|
|
|
|
|
case "lastTick":
|
|
|
|
|
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long t)) lastTickUnix = Math.Max(0, t);
|
|
|
|
|
break;
|
|
|
|
|
case "starving": starving = value == "1"; break;
|
2026-10-28 22:11:36 +03:00
|
|
|
case "training": inTraining = Math.Max(0, ParseInt(value, 0)); break;
|
|
|
|
|
case "famine":
|
|
|
|
|
foreach (var span in value.Split(';'))
|
|
|
|
|
{
|
|
|
|
|
string[] se = span.Split('-');
|
|
|
|
|
if (se.Length == 2
|
|
|
|
|
&& long.TryParse(se[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long fs)
|
|
|
|
|
&& long.TryParse(se[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out long fe))
|
|
|
|
|
famine.Add(new[] { fs, fe });
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-10-27 18:04:27 +03:00
|
|
|
case "kit": startingKitGiven = value == "1"; break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-31 20:52:28 +03:00
|
|
|
|
|
|
|
|
// Рабочих не может быть больше, чем жителей (испорченное или очень старое сохранение).
|
|
|
|
|
// Места в зданиях здесь не проверяем: секция Экономики может загрузиться позже -- это делает ClampJobs.
|
|
|
|
|
int over = Workers - civilians;
|
|
|
|
|
for (int j = WorkJobs.Count - 1; j >= 0 && over > 0; j--)
|
|
|
|
|
{
|
|
|
|
|
int take = Math.Min(over, jobs[j]);
|
|
|
|
|
jobs[j] -= take;
|
|
|
|
|
over -= take;
|
|
|
|
|
}
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Новая игра: стартовый город. Стартовое войско выдаст App/GameModules после сброса всех модулей.
|
|
|
|
|
public void ResetState()
|
|
|
|
|
{
|
|
|
|
|
SetStartState();
|
|
|
|
|
Changed();
|
2026-10-27 18:51:35 +03:00
|
|
|
WorkersChanged();
|
2026-10-27 18:04:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int ParseInt(string s, int fallback)
|
|
|
|
|
{
|
|
|
|
|
return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v) ? v : fallback;
|
|
|
|
|
}
|
|
|
|
|
}
|