using System; using System.Collections.Generic; // Контракты модулей: всё, что один модуль может попросить у другого. // Реализация живёт в своём модуле, здесь -- только "что умеет". // Добавляя метод сюда, обнови документ модуля в Docs/Modules. // ---------------- Экономика: ресурсы и производство ---------------- public interface IEconomy { float Get(ResourceType type); bool Has(ResourceType type, float amount); void Add(ResourceType type, float amount); bool TrySpend(ResourceType type, float amount); bool CanAfford(IReadOnlyList cost); // Списывает всё или ничего bool TrySpend(IReadOnlyList cost); // Лимит склада для ресурса. float.PositiveInfinity -- без лимита (люди, сила). // Сверх лимита ресурс не прибавляется (добыча, награды) -- лишнее пропадает. float GetCapacity(ResourceType type); } // Защита ресурсов от грабежа (01.11, ТЕСТ). Сколько ресурса нельзя отнять в набеге: часть запаса прячут // Склады и Амбары. Добыча набега по ресурсу = Lootable. Реализует слой приложения (App/ResourceProtection) // по зданиям города; модули Боя и Мира берут отсюда, сколько можно унести у города. public interface IResourceProtection { float Protected(ResourceType type); // сколько спрятано (кристаллы -- всё) float Lootable(ResourceType type); // сколько можно унести: max(0, запас - спрятано) } // Добыча через рабочих (этап 3). Реализует модуль Экономики (ProductionService). public interface IProduction { int Slots(WorkJob job); // мест для рабочих этой профессии во всех зданиях города float Efficiency(WorkJob job); // множитель от уровня зданий (1 -- здания 1 уровня) float RatePerHour(ResourceType type); // добыча ресурса в час сейчас // Добыть за hours часов (вызывает модуль Населения в своём расчёте -- сначала добыли, потом съели) void Produce(double hours); } // ---------------- Поселение: здания и развитие города ---------------- public interface ISettlement { int TownHallLevel { get; } int BuildingCount { get; } bool IsCityLoaded { get; } // открыта ли сейчас сцена города } // Здание с уровнем (добывающие постройки и т.п.) -- Поселение сохраняет уровень, не зная, чьё это здание public interface ILeveledBuilding { int Level { get; set; } } // ---------------- Население: жители, рабочие, еда, голод, прирост, найм ---------------- public interface IPopulation { int Total { get; } // все люди города: жители + обучаются в Казармах + воины (в городе и в походах) int Civilians { get; } // жители (не воины) int Workers { get; } // жители на работе (все профессии вместе) int Idle { get; } // остальные жители: Civilians - Workers int Workforce { get; } // сколько жителей могут работать: все жители (общего ограничения нет, регламент 31.10) int FreeWorkers { get; } // могут работать, но без работы: Workforce - Workers int Soldiers { get; } // воины -- их хранит модуль Армии int HousingCap { get; } // предел населения: выше него прирост не идёт float FoodPerHour { get; } // сколько город съедает в час float GrowthPerHour { get; } // прирост в час сейчас (0 -- если голод или упёрлись в предел) bool IsStarving { get; } // еды не хватило на последний расчёт // Голодал ли город в момент unixTime (секунды). Для боёв, прошедших, пока игра была закрыта: // история хранится за последние MaxOfflineHours (48 ч), точность -- 10 минут (шаг расчёта). bool WasStarvingAt(long unixTime); // Найм воинов (решение команды от 19.10.2026): воины берутся из свободных жителей, гражданский резерв не отдаётся. int CivilianReserve { get; } // гражданский резерв: столько свободных жителей в войско не взять int Recruitable { get; } // сколько воинов можно нанять прямо сейчас: (свободные - резерв) / жителей на воина int InTraining { get; } // людей, взятых из жителей и ещё обучающихся в Казармах bool TryRecruit(int soldiers); // забрать людей в Казармы (в начале обучения); false -- столько жителей нет void RecruitsTrained(int soldiers); // обучение закончено: этих людей теперь считает Армия (без проверки Армии) // Обучение закончено -- передать новобранцев в Армию одним действием: люди уходят из Населения, только если // Армия их действительно приняла (IArmy.AddTroops). false -- ничего не изменилось: столько новобранцев нет, // Армия не подключена или не приняла (новобранцы остаются в Казармах, никто не теряется и не дублируется). bool CompleteTraining(int troopType, int soldiers); // Досчитать население до момента unixNow (вызывается раз в несколько секунд и при перемотке) void Tick(long unixNow); // Рабочие по профессиям (этап 3) int GetWorkers(WorkJob job); // Поставить count рабочих на профессию (в пределах мест в зданиях и свободных рабочих). Возвращает, сколько стоит теперь. int SetWorkers(WorkJob job, int count); } // ---------------- Армия: войска ---------------- // Раненые на лечении в госпитале: одна группа одного типа, лечится вместе. Время -- мс часов игры public struct HealingInfo { public int Id; public int Type; public int Count; public long StartMs; public long EndMs; } // Госпиталь -- здание Поселения (решения 31.10, п. 7): уровень, вместимость, улучшения. // Пока здания нет, контракт не зарегистрирован -- Армия берёт временное число мест из таблицы HospitalConfig. public interface IHospital { int Beds { get; } // мест для лечения: регламент 31.10 -- 20..60 по уровням I..V } // Заказ обучения в Казармах. Время -- миллисекунды часов игры (GameClock.UnixNowMs) public struct TrainingOrderInfo { public int Id; // номер заказа -- не повторяется public int Type; // тип войск (TroopType) public int Count; public long StartMs; // когда начнётся (заказы идут друг за другом) public long EndMs; // когда воины будут готовы } public interface IArmy { int TypeCount { get; } // типов войск (пехота, лучники, кавалерия, осадные) int GetTroops(int type); // воинов этого типа в городе int SoldiersAtHome { get; } // всего воинов в городе (вместе с гарнизоном) int TroopsInMarches { get; } // воинов в походах // Гарнизон -- часть воинов в городе: защищает город и в походы не уходит. Всего воинов от этого не меняется. int GarrisonTotal { get; } int GetGarrison(int type); int AvailableForMarch { get; } // можно отправить в поход: в городе минус гарнизон int SetGarrison(int count); // назначить гарнизон (по составу армии); возвращает, сколько назначено void AddTroops(int type, int count); // обучены / вернулись int LoseFraction(double fraction); // потери в бою: доля от каждого типа; возвращает, сколько погибло // Слоты похода (регламент 31.10, раздел 7): сколько походов игрока может идти одновременно. // База 2 (ТЕСТ), технология «Военная логистика» поднимет до 7 -- уровни и цены ещё не утверждены. int MarchSlots { get; } int ActiveMarches { get; } // походов в пути сейчас (у каждого -- свой слот) // Забрать свободных воинов в поход (запоминает состав похода). // false -- ничего не взято: не хватает воинов, нет свободного слота или поход с таким номером уже есть. bool TakeForMarch(int marchId, int count); // Вернуть выживших из похода по тем же типам войск. // Зачисляет только по известному походу и только один раз; false -- такого похода нет, никто не добавлен. bool ReturnFromMarch(int marchId, int survivors); // Состав похода по типам (копия); null -- такого похода нет int[] GetMarchUnits(int marchId); // Вернуть выживших точно по типам (бой посчитал потери каждого типа). Правила те же, что у ReturnFromMarch. bool ReturnUnits(int marchId, int[] survivorUnits); // ---- Раненые и лечение (решения по 8 вопросам Саши, 31.10, п. 7): состояние и очередь лечения -- у Армии ---- // Бой считает раненых, Армия хранит их и лечит, места -- от госпиталя (IHospital), цену лечения списывает Экономика. // Раненые -- люди: Население считает их вместе с воинами. Лишние раненые ждут места (ТЕСТ, решение Саши 01.11). int WoundedTotal { get; } // раненых всего: ждут места + лечатся int GetWoundedWaiting(int type); // ждут места, по типам IReadOnlyList Healing { get; } // лечатся сейчас int HospitalBeds { get; } // мест в госпитале сейчас // Вернуть из похода выживших и раненых точно по типам (раненые -- в госпиталь в момент nowMs). // Только известный поход и только один раз; больше, чем ушло, не вернётся. bool ReturnUnits(int marchId, int[] survivorUnits, int[] woundedUnits, long nowMs); // Закончить лечение, срок которого наступил (и офлайн), и положить ждущих на освободившиеся места. // Вылеченные возвращаются в город (SoldiersHealedEvent, один раз на группу). Возвращает, сколько групп вылечено. int ProcessHealing(long nowMs); // ---- Очередь обучения (решения по 8 вопросам Саши, 31.10, п. 8): единственный владелец -- Армия ---- // Жители для найма -- у Населения (IPopulation: Recruitable, резерв, TryRecruit, CompleteTraining), // размер очереди -- от Казарм (передаёт вызывающий), цену списывает вызывающий через Экономику. IReadOnlyList Training { get; } // Можно ли поставить заказ: очередь не заполнена, жителей хватает. Ничего не меняет; false -- error для игрока. bool CanQueueTraining(int type, int count, int queueSize, out string error); // Поставить заказ в конец очереди, люди уходят из жителей в Казармы. false -- ничего не изменилось. bool TryQueueTraining(int type, int count, long durationMs, int queueSize, long nowMs, out string error); // Закончить заказы, срок которых наступил к nowMs (и пока игра была закрыта): воины переходят в Армию, // каждый заказ -- один раз (TroopsTrainedEvent). Возвращает, сколько заказов закончено. int ProcessTraining(long nowMs); // Ускорение: все заказы заканчиваются в nowMs int FinishAllTraining(long nowMs); } // ---------------- Глобальный мир: карта и перемещения ---------------- public interface IWorld { int ActiveMarches { get; } bool TryStartMarch(string targetId, int soldiers, out string error); // Поход «Разведка» (решения 31.10, п. 2): дойти до цели и вернуться -- без боя, сбора и потерь, занимает слот похода bool TryStartScout(string targetId, int soldiers, out string error); } // ---------------- Мир -> Разведка: что где находится (решение №46) ---------------- // Координаты и марши ведёт Мир, разведка только читает их через этот контракт -- без прямых ссылок // World <-> Intelligence. Координаты -- милли-единицы карты (1000 = 1 клетка). public struct ArmyOnMap { public string Id; // устойчивый номер армии на карте ("march_12") public string OwnerId; // владелец (LocalPlayer.Id -- свои войска) public int X, Y; // где сейчас public int ToX, ToY; // куда движется public int Soldiers; // точная численность -- разведка показывает игроку только примерную } public interface IWorldMap { int HalfWidth { get; } // границы карты от центра, милли-единицы int HalfHeight { get; } bool TryGetSettlement(string ownerId, out int x, out int y); // где поселение игрока // Все армии на карте в момент timeMs (в том числе в прошлом -- для расчёта, пока игра была закрыта) void GetArmies(long timeMs, List result); } // ---------------- Разведка (Bogatyri.Intelligence): туман войны, наблюдение ---------------- public enum MapVisibility { Unexplored, // неизведано Explored, // исследовано: география известна, текущие движения не видны Observed // наблюдается сейчас } // Что игрок знает о чужой армии: только приблизительно и только на момент наблюдения public struct KnownContact { public string ArmyId; public bool Identified; // владелец опознан public string OwnerId; // null, пока не опознан public bool Observed; // видна прямо сейчас; false -- последний контакт (устаревшие данные) public int X, Y, ToX, ToY; // где видели последний раз и куда шла public int ApproxMin, ApproxMax; // примерная численность (±20%), одна на событие наблюдения public long LastSeenMs; // когда видели последний раз } public interface IIntelligence { int ObservationLevel { get; set; } // технология «Обзор»: 0..3 (+10 / +20 / +30% к радиусам) MapVisibility GetVisibility(int x, int y); // по последнему расчёту IReadOnlyList Contacts { get; } void ObserveUntil(long nowMs); // досчитать наблюдение до момента (в том числе офлайн) } // ---------------- Боевая система: урон, потери, исход ---------------- // Типы войск в бою -- порядок как в GameData.Troops и в модуле Армии public static class TroopType { public const int Infantry = 0; // пехота public const int Archers = 1; // лучники public const int Cavalry = 2; // кавалерия public const int Siege = 3; // осадные public const int Count = 4; } // Состояние войск стороны в бою (план от 27.10.2026). Штрафы применяются последовательно: // голод (-50%) и усталость (-20%) вместе оставляют 40% силы, а не 30%. Числа -- в правилах боя. [Flags] public enum BattleCondition { None = 0, Famine = 1, // голод в поселении на момент боя Fatigue = 2 // войско недавно сражалось (пока не выставляется: правила учёта усталости не утверждены) } public struct BattleRequest { // Состав сторон по типам войск (TroopType). Если состав не задан (null) -- // берётся число Attackers / Defenders, и все считаются пехотой. public int[] AttackerUnits; public int[] DefenderUnits; public int Attackers; public int Defenders; public bool DefenderFortified; // защитники за стенами (город, замок) public int AttackerBonusPercent; // герой, технологии, голод (минус) -- +10 = на 10% сильнее public int DefenderBonusPercent; // Состояния сторон: голод, усталость. Действуют последовательно (умножаются), см. BattleCondition public BattleCondition AttackerConditions; public BattleCondition DefenderConditions; // Номер боя: из него считается случайность сторон (±5%). Один и тот же Seed с теми же войсками -- // всегда тот же итог, бой можно перепроверить. В мультиплеере Seed выдаёт сервер. public long Seed; } // Итог боя -- одинаково полный для обеих сторон. Все массивы -- по типам войск (TroopType). // По каждому типу: до боя = выжившие + погибшие + раненые. public struct BattleOutcome { public bool Victory; // победил атакующий public long BattleId; // = Seed запроса public int RulesVersion; // версия правил боя (баланса), по которой посчитан итог public int AttackerLosses; // выбыли из строя: погибшие + раненые public int DefenderLosses; public int Survivors; // сколько атакующих осталось в строю public long AttackerPower; // итоговая сила сторон x100: с контрами, стенами, бонусами и случайностью public long DefenderPower; public int AttackerLuckPermille; // случайность стороны в десятых долях процента: +23 = +2,3% public int DefenderLuckPermille; public int AttackerConditionPercent; // сколько силы оставили состояния стороны: 100 -- без штрафов, 50 -- голод public int DefenderConditionPercent; public int[] AttackerUnitsBefore, DefenderUnitsBefore; public int[] AttackerSurvivorUnits, DefenderSurvivorUnits; public int[] AttackerDeadUnits, DefenderDeadUnits; public int[] AttackerWoundedUnits, DefenderWoundedUnits; // раненые (регламент 31.10, решения 31.10, п. 7): выбывшие минус погибшие public int[] DefenderLossUnits; // погибшие + раненые защитника (для старого кода) } // Прогноз до боя: с учётом случайности, без раскрытия итога (план от 27.10.2026) public enum BattleChance { Advantage, // победа при любой случайности Uncertain, // исход зависит от случайности HighRisk // поражение при любой случайности } public struct BattleForecast { public BattleChance Chance; public int WinChancePermille; // шанс победы нападающего в десятых долях процента: 273 = 27,3% (решение №11) public long AttackerPowerMin, AttackerPowerMax; // x100, крайние значения случайности public long DefenderPowerMin, DefenderPowerMax; public int AttackerLossesMin, AttackerLossesMax; // с учётом округления потерь (решение №13) } public interface IBattleSystem { BattleOutcome Resolve(BattleRequest request); BattleForecast Forecast(BattleRequest request); // Seed не нужен: перебираются крайние значения случайности } // ---------------- Чат: общий мир и Круг ---------------- public enum ChatChannel { World, // общий чат всего мира Circle // чат своего Круга } [Serializable] public class ChatMessage { public ChatChannel channel; public string senderId; public string senderName; public string text; public long timeMs; // Unix-время в миллисекундах } public interface IChat { int MaxMessageLength { get; } IReadOnlyList GetMessages(ChatChannel channel); bool CanWrite(ChatChannel channel); // в канал "Круг" -- только состоя в Круге // Отправить от имени локального игрока. false -- пустое сообщение или канал "Круг" без Круга bool Send(ChatChannel channel, string text); } // ---------------- Круг: объединение игроков ---------------- public enum CircleRole { Leader, // глава Member // участник } [Serializable] public class CircleMember { public string playerId; public string name; public CircleRole role; } [Serializable] public class CircleInfo { public string id; public string name; public List members = new List(); public string LeaderName { get { foreach (var m in members) if (m.role == CircleRole.Leader) return m.name; return ""; } } } public interface ICircle { int MaxMembers { get; } CircleInfo Current { get; } // Круг игрока; null -- игрок не в Круге bool IsInCircle { get; } bool IsLeader { get; } IReadOnlyList GetCircles(); // все Круги, в которые можно вступить bool TryCreate(string name, out string error); // создатель становится главой bool TryJoin(string circleId, out string error); void Leave(); // глава один в Круге -- Круг распускается } // ---------------- Сохранения: игровое состояние ---------------- public interface ISaveSystem { bool HasSave { get; } void SaveNow(); void DeleteSave(); // Новая игра: все модули возвращаются к стартовому состоянию, файл перезаписывается void ResetAll(); } // Участник сохранения: каждый модуль сам решает, что и как хранит, в своей секции файла. public interface ISaveParticipant { string SaveKey { get; } // имя секции в файле: "economy", "settlement", "world"... string Capture(); // JSON секции; null -- оставить то, что уже было в файле void Restore(string json); void ResetState(); // вернуться к состоянию новой игры (сброс сохранений) } // Список участников. Модули регистрируются сами, модуль Сохранений обходит их при записи и загрузке. public static class SaveRegistry { private static readonly List participants = new List(); public static IReadOnlyList All => participants; public static void Register(ISaveParticipant participant) { participants.RemoveAll(p => p.SaveKey == participant.SaveKey); participants.Add(participant); } public static void Unregister(ISaveParticipant participant) { participants.Remove(participant); } public static void Clear() { participants.Clear(); } }