2026-10-19 19:07:45 +02:00
|
|
|
|
using System;
|
2026-10-19 18:39:28 +02:00
|
|
|
|
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<ResourceAmount> cost);
|
|
|
|
|
|
// Списывает всё или ничего
|
|
|
|
|
|
bool TrySpend(IReadOnlyList<ResourceAmount> cost);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
|
|
|
|
|
|
// Лимит склада для ресурса. float.PositiveInfinity -- без лимита (люди, сила).
|
|
|
|
|
|
// Сверх лимита ресурс не прибавляется (добыча, награды) -- лишнее пропадает.
|
|
|
|
|
|
float GetCapacity(ResourceType type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Добыча через рабочих (этап 3). Реализует модуль Экономики (ProductionService).
|
|
|
|
|
|
public interface IProduction
|
|
|
|
|
|
{
|
|
|
|
|
|
int Slots(WorkJob job); // мест для рабочих этой профессии во всех зданиях города
|
|
|
|
|
|
float Efficiency(WorkJob job); // множитель от уровня зданий (1 -- здания 1 уровня)
|
|
|
|
|
|
float RatePerHour(ResourceType type); // добыча ресурса в час сейчас
|
|
|
|
|
|
// Добыть за hours часов (вызывает модуль Населения в своём расчёте -- сначала добыли, потом съели)
|
|
|
|
|
|
void Produce(double hours);
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Поселение: здания и развитие города ----------------
|
|
|
|
|
|
public interface ISettlement
|
|
|
|
|
|
{
|
|
|
|
|
|
int TownHallLevel { get; }
|
|
|
|
|
|
int BuildingCount { get; }
|
|
|
|
|
|
bool IsCityLoaded { get; } // открыта ли сейчас сцена города
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Здание с уровнем (добывающие постройки и т.п.) -- Поселение сохраняет уровень, не зная, чьё это здание
|
|
|
|
|
|
public interface ILeveledBuilding
|
|
|
|
|
|
{
|
|
|
|
|
|
int Level { get; set; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 19:34:33 +03:00
|
|
|
|
// ---------------- Население: жители, рабочие, еда, голод, прирост, найм ----------------
|
2026-10-27 18:04:27 +03:00
|
|
|
|
public interface IPopulation
|
|
|
|
|
|
{
|
2026-10-28 22:11:36 +03:00
|
|
|
|
int Total { get; } // все люди города: жители + обучаются в Казармах + воины (в городе и в походах)
|
2026-10-27 18:04:27 +03:00
|
|
|
|
int Civilians { get; } // жители (не воины)
|
2026-10-27 18:51:35 +03:00
|
|
|
|
int Workers { get; } // жители на работе (все профессии вместе)
|
2026-10-27 18:04:27 +03:00
|
|
|
|
int Idle { get; } // остальные жители: Civilians - Workers
|
2026-10-31 19:34:33 +03:00
|
|
|
|
int Workforce { get; } // сколько жителей могут работать: все жители (общего ограничения нет, регламент 31.10)
|
2026-10-27 18:51:35 +03:00
|
|
|
|
int FreeWorkers { get; } // могут работать, но без работы: Workforce - Workers
|
2026-10-27 18:04:27 +03:00
|
|
|
|
int Soldiers { get; } // воины -- их хранит модуль Армии
|
|
|
|
|
|
int HousingCap { get; } // предел населения: выше него прирост не идёт
|
|
|
|
|
|
|
|
|
|
|
|
float FoodPerHour { get; } // сколько город съедает в час
|
|
|
|
|
|
float GrowthPerHour { get; } // прирост в час сейчас (0 -- если голод или упёрлись в предел)
|
|
|
|
|
|
bool IsStarving { get; } // еды не хватило на последний расчёт
|
2026-10-28 22:11:36 +03:00
|
|
|
|
// Голодал ли город в момент 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 -- столько жителей нет
|
2026-10-31 20:52:28 +03:00
|
|
|
|
void RecruitsTrained(int soldiers); // обучение закончено: этих людей теперь считает Армия (без проверки Армии)
|
|
|
|
|
|
// Обучение закончено -- передать новобранцев в Армию одним действием: люди уходят из Населения, только если
|
|
|
|
|
|
// Армия их действительно приняла (IArmy.AddTroops). false -- ничего не изменилось: столько новобранцев нет,
|
|
|
|
|
|
// Армия не подключена или не приняла (новобранцы остаются в Казармах, никто не теряется и не дублируется).
|
|
|
|
|
|
bool CompleteTraining(int troopType, int soldiers);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
|
|
|
|
|
|
// Досчитать население до момента unixNow (вызывается раз в несколько секунд и при перемотке)
|
|
|
|
|
|
void Tick(long unixNow);
|
2026-10-27 18:51:35 +03:00
|
|
|
|
|
|
|
|
|
|
// Рабочие по профессиям (этап 3)
|
|
|
|
|
|
int GetWorkers(WorkJob job);
|
|
|
|
|
|
// Поставить count рабочих на профессию (в пределах мест в зданиях и свободных рабочих). Возвращает, сколько стоит теперь.
|
|
|
|
|
|
int SetWorkers(WorkJob job, int count);
|
2026-10-27 18:04:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Армия: войска ----------------
|
|
|
|
|
|
public interface IArmy
|
|
|
|
|
|
{
|
2026-10-23 18:33:04 +02:00
|
|
|
|
int TypeCount { get; } // типов войск (пехота, лучники, кавалерия, осадные)
|
|
|
|
|
|
int GetTroops(int type); // воинов этого типа в городе
|
2026-10-27 16:54:49 +01:00
|
|
|
|
int SoldiersAtHome { get; } // всего воинов в городе (вместе с гарнизоном)
|
2026-10-23 18:33:04 +02:00
|
|
|
|
int TroopsInMarches { get; } // воинов в походах
|
|
|
|
|
|
|
2026-10-27 16:54:49 +01:00
|
|
|
|
// Гарнизон -- часть воинов в городе: защищает город и в походы не уходит. Всего воинов от этого не меняется.
|
|
|
|
|
|
int GarrisonTotal { get; }
|
|
|
|
|
|
int GetGarrison(int type);
|
|
|
|
|
|
int AvailableForMarch { get; } // можно отправить в поход: в городе минус гарнизон
|
|
|
|
|
|
int SetGarrison(int count); // назначить гарнизон (по составу армии); возвращает, сколько назначено
|
|
|
|
|
|
|
2026-10-23 18:33:04 +02:00
|
|
|
|
void AddTroops(int type, int count); // обучены / вернулись
|
|
|
|
|
|
int LoseFraction(double fraction); // потери в бою: доля от каждого типа; возвращает, сколько погибло
|
|
|
|
|
|
|
2026-10-31 16:05:55 +01:00
|
|
|
|
// Слоты похода (регламент 31.10, раздел 7): сколько походов игрока может идти одновременно.
|
|
|
|
|
|
// База 2 (ТЕСТ), технология «Военная логистика» поднимет до 7 -- уровни и цены ещё не утверждены.
|
|
|
|
|
|
int MarchSlots { get; }
|
|
|
|
|
|
int ActiveMarches { get; } // походов в пути сейчас (у каждого -- свой слот)
|
|
|
|
|
|
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Забрать свободных воинов в поход (запоминает состав похода).
|
2026-10-31 16:05:55 +01:00
|
|
|
|
// false -- ничего не взято: не хватает воинов, нет свободного слота или поход с таким номером уже есть.
|
2026-10-19 18:39:28 +02:00
|
|
|
|
bool TakeForMarch(int marchId, int count);
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Вернуть выживших из похода по тем же типам войск.
|
|
|
|
|
|
// Зачисляет только по известному походу и только один раз; false -- такого похода нет, никто не добавлен.
|
|
|
|
|
|
bool ReturnFromMarch(int marchId, int survivors);
|
2026-10-27 16:34:58 +01:00
|
|
|
|
|
|
|
|
|
|
// Состав похода по типам (копия); null -- такого похода нет
|
|
|
|
|
|
int[] GetMarchUnits(int marchId);
|
2026-10-28 18:38:01 +01:00
|
|
|
|
// Вернуть выживших точно по типам (бой посчитал потери каждого типа). Правила те же, что у ReturnFromMarch.
|
|
|
|
|
|
bool ReturnUnits(int marchId, int[] survivorUnits);
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------- Глобальный мир: карта и перемещения ----------------
|
|
|
|
|
|
public interface IWorld
|
|
|
|
|
|
{
|
|
|
|
|
|
int ActiveMarches { get; }
|
|
|
|
|
|
bool TryStartMarch(string targetId, int soldiers, out string error);
|
2026-11-01 13:11:55 +01:00
|
|
|
|
// Поход «Разведка» (решения 31.10, п. 2): дойти до цели и вернуться -- без боя, сбора и потерь, занимает слот похода
|
|
|
|
|
|
bool TryStartScout(string targetId, int soldiers, out string error);
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 17:05:17 +01:00
|
|
|
|
// ---------------- Мир -> Разведка: что где находится (решение №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<ArmyOnMap> 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<KnownContact> Contacts { get; }
|
|
|
|
|
|
void ObserveUntil(long nowMs); // досчитать наблюдение до момента (в том числе офлайн)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Боевая система: урон, потери, исход ----------------
|
2026-10-27 16:34:58 +01:00
|
|
|
|
// Типы войск в бою -- порядок как в GameData.Troops и в модуле Армии
|
|
|
|
|
|
public static class TroopType
|
2026-10-19 18:39:28 +02:00
|
|
|
|
{
|
2026-10-27 16:34:58 +01:00
|
|
|
|
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;
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 19:16:25 +01:00
|
|
|
|
// Состояние войск стороны в бою (план от 27.10.2026). Штрафы применяются последовательно:
|
|
|
|
|
|
// голод (-50%) и усталость (-20%) вместе оставляют 40% силы, а не 30%. Числа -- в правилах боя.
|
|
|
|
|
|
[Flags]
|
|
|
|
|
|
public enum BattleCondition
|
|
|
|
|
|
{
|
|
|
|
|
|
None = 0,
|
|
|
|
|
|
Famine = 1, // голод в поселении на момент боя
|
|
|
|
|
|
Fatigue = 2 // войско недавно сражалось (пока не выставляется: правила учёта усталости не утверждены)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
public struct BattleRequest
|
|
|
|
|
|
{
|
2026-10-27 16:34:58 +01:00
|
|
|
|
// Состав сторон по типам войск (TroopType). Если состав не задан (null) --
|
|
|
|
|
|
// берётся число Attackers / Defenders, и все считаются пехотой.
|
|
|
|
|
|
public int[] AttackerUnits;
|
|
|
|
|
|
public int[] DefenderUnits;
|
2026-10-19 18:39:28 +02:00
|
|
|
|
public int Attackers;
|
|
|
|
|
|
public int Defenders;
|
2026-10-27 16:34:58 +01:00
|
|
|
|
|
|
|
|
|
|
public bool DefenderFortified; // защитники за стенами (город, замок)
|
|
|
|
|
|
public int AttackerBonusPercent; // герой, технологии, голод (минус) -- +10 = на 10% сильнее
|
|
|
|
|
|
public int DefenderBonusPercent;
|
2026-10-28 19:03:14 +01:00
|
|
|
|
|
2026-10-28 19:16:25 +01:00
|
|
|
|
// Состояния сторон: голод, усталость. Действуют последовательно (умножаются), см. BattleCondition
|
|
|
|
|
|
public BattleCondition AttackerConditions;
|
|
|
|
|
|
public BattleCondition DefenderConditions;
|
|
|
|
|
|
|
2026-10-28 19:03:14 +01:00
|
|
|
|
// Номер боя: из него считается случайность сторон (±5%). Один и тот же Seed с теми же войсками --
|
|
|
|
|
|
// всегда тот же итог, бой можно перепроверить. В мультиплеере Seed выдаёт сервер.
|
|
|
|
|
|
public long Seed;
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 19:03:14 +01:00
|
|
|
|
// Итог боя -- одинаково полный для обеих сторон. Все массивы -- по типам войск (TroopType).
|
|
|
|
|
|
// По каждому типу: до боя = выжившие + погибшие + раненые.
|
2026-10-19 18:39:28 +02:00
|
|
|
|
public struct BattleOutcome
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool Victory; // победил атакующий
|
2026-10-28 19:03:14 +01:00
|
|
|
|
public long BattleId; // = Seed запроса
|
|
|
|
|
|
public int RulesVersion; // версия правил боя (баланса), по которой посчитан итог
|
2026-10-27 16:34:58 +01:00
|
|
|
|
|
2026-10-28 19:03:14 +01:00
|
|
|
|
public int AttackerLosses; // выбыли из строя: погибшие + раненые
|
2026-10-19 18:39:28 +02:00
|
|
|
|
public int DefenderLosses;
|
2026-10-28 19:03:14 +01:00
|
|
|
|
public int Survivors; // сколько атакующих осталось в строю
|
|
|
|
|
|
|
|
|
|
|
|
public long AttackerPower; // итоговая сила сторон x100: с контрами, стенами, бонусами и случайностью
|
2026-10-27 16:34:58 +01:00
|
|
|
|
public long DefenderPower;
|
2026-10-28 19:03:14 +01:00
|
|
|
|
public int AttackerLuckPermille; // случайность стороны в десятых долях процента: +23 = +2,3%
|
|
|
|
|
|
public int DefenderLuckPermille;
|
2026-10-28 19:16:25 +01:00
|
|
|
|
public int AttackerConditionPercent; // сколько силы оставили состояния стороны: 100 -- без штрафов, 50 -- голод
|
|
|
|
|
|
public int DefenderConditionPercent;
|
2026-10-28 19:03:14 +01:00
|
|
|
|
|
|
|
|
|
|
public int[] AttackerUnitsBefore, DefenderUnitsBefore;
|
|
|
|
|
|
public int[] AttackerSurvivorUnits, DefenderSurvivorUnits;
|
|
|
|
|
|
public int[] AttackerDeadUnits, DefenderDeadUnits;
|
|
|
|
|
|
public int[] AttackerWoundedUnits, DefenderWoundedUnits; // пока нули: доля раненых не утверждена командой
|
|
|
|
|
|
public int[] DefenderLossUnits; // погибшие + раненые защитника (для старого кода)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Прогноз до боя: с учётом случайности, без раскрытия итога (план от 27.10.2026)
|
|
|
|
|
|
public enum BattleChance
|
|
|
|
|
|
{
|
|
|
|
|
|
Advantage, // победа при любой случайности
|
|
|
|
|
|
Uncertain, // исход зависит от случайности
|
|
|
|
|
|
HighRisk // поражение при любой случайности
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public struct BattleForecast
|
|
|
|
|
|
{
|
|
|
|
|
|
public BattleChance Chance;
|
2026-10-30 17:02:05 +01:00
|
|
|
|
public int WinChancePermille; // шанс победы нападающего в десятых долях процента: 273 = 27,3% (решение №11)
|
2026-10-28 19:03:14 +01:00
|
|
|
|
public long AttackerPowerMin, AttackerPowerMax; // x100, крайние значения случайности
|
|
|
|
|
|
public long DefenderPowerMin, DefenderPowerMax;
|
2026-10-30 17:02:05 +01:00
|
|
|
|
public int AttackerLossesMin, AttackerLossesMax; // с учётом округления потерь (решение №13)
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public interface IBattleSystem
|
|
|
|
|
|
{
|
|
|
|
|
|
BattleOutcome Resolve(BattleRequest request);
|
2026-10-28 19:03:14 +01:00
|
|
|
|
BattleForecast Forecast(BattleRequest request); // Seed не нужен: перебираются крайние значения случайности
|
2026-10-19 18:39:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 19:07:45 +02:00
|
|
|
|
// ---------------- Чат: общий мир и Круг ----------------
|
|
|
|
|
|
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<ChatMessage> 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<CircleMember> members = new List<CircleMember>();
|
|
|
|
|
|
|
|
|
|
|
|
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<CircleInfo> GetCircles(); // все Круги, в которые можно вступить
|
|
|
|
|
|
|
|
|
|
|
|
bool TryCreate(string name, out string error); // создатель становится главой
|
|
|
|
|
|
bool TryJoin(string circleId, out string error);
|
|
|
|
|
|
void Leave(); // глава один в Круге -- Круг распускается
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 18:39:28 +02:00
|
|
|
|
// ---------------- Сохранения: игровое состояние ----------------
|
|
|
|
|
|
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<ISaveParticipant> participants = new List<ISaveParticipant>();
|
|
|
|
|
|
|
|
|
|
|
|
public static IReadOnlyList<ISaveParticipant> 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|