Files
MVP1/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs
T
AlexanderandClaude Opus 5.5 4865b14ff2 Гарнизон города
Модуль Армии (Bogatyri.Army, без Unity):
- гарнизон — часть воинов в городе: защищает город и в походы не уходит, всего воинов столько же
- IArmy: GarrisonTotal, GetGarrison(type), AvailableForMarch, SetGarrison(count)
- гарнизон набирается по составу армии, не больше воинов в городе; после потерь обрезается по типам
- в поход уходят только свободные воины (TakeForMarch)
- сохранение: строка garrison= в секции army (старые сохранения — без гарнизона); сброс очищает
- TroopsChangedEvent.Garrison

Интерфейс:
- окно «Войска» в городе: карточка «Гарнизон города» (0, -10, -1, +1, +10, «Все»)
- карта мира: «Свободно / Гарнизон / В походе», ползунок похода и прогноз — только по свободным воинам
- понятная ошибка, если воины есть, но все в гарнизоне

Тест «один солдат в одном месте»: 30 воинов, 10 в гарнизон → 20 свободны, всего 30 (решение команды от 19.10.2026).
Документы Army, Core, World, README обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:54:49 +01:00

244 lines
10 KiB
C#

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<ResourceAmount> cost);
// Списывает всё или ничего
bool TrySpend(IReadOnlyList<ResourceAmount> cost);
}
// ---------------- Поселение: здания и развитие города ----------------
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; } // жители на работе (этап 3: распределение по зданиям)
int Idle { get; } // остальные жители: Civilians - Workers
int Soldiers { get; } // воины -- их хранит модуль Армии
int HousingCap { get; } // предел населения: выше него прирост не идёт
float FoodPerHour { get; } // сколько город съедает в час
float GrowthPerHour { get; } // прирост в час сейчас (0 -- если голод или упёрлись в предел)
bool IsStarving { get; } // еды не хватило на последний расчёт
// Досчитать население до момента unixNow (вызывается раз в несколько секунд и при перемотке)
void Tick(long unixNow);
}
// ---------------- Армия: войска ----------------
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); // потери в бою: доля от каждого типа; возвращает, сколько погибло
// Забрать воинов из города в поход (запоминает состав похода)
bool TakeForMarch(int marchId, int count);
// Вернуть выживших из похода по тем же типам войск
void ReturnFromMarch(int marchId, int survivors);
// Состав похода по типам (копия); null -- такого похода нет
int[] GetMarchUnits(int marchId);
// Вернуть выживших точно по типам (бой посчитал потери каждого типа)
void ReturnUnits(int marchId, int[] survivorUnits);
}
// ---------------- Глобальный мир: карта и перемещения ----------------
public interface IWorld
{
int ActiveMarches { get; }
bool TryStartMarch(string targetId, int soldiers, out string error);
}
// ---------------- Боевая система: урон, потери, исход ----------------
// Типы войск в бою -- порядок как в 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;
}
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;
}
public struct BattleOutcome
{
public bool Victory; // победил атакующий
public int AttackerLosses;
public int DefenderLosses;
public int Survivors; // сколько атакующих осталось
public long AttackerPower; // сила сторон с учётом контр, стен и бонусов
public long DefenderPower;
public int[] AttackerSurvivorUnits; // выжившие атакующие по типам
public int[] DefenderLossUnits; // потери защитников по типам
}
public interface IBattleSystem
{
BattleOutcome Resolve(BattleRequest request);
}
// ---------------- Чат: общий мир и Круг ----------------
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(); // глава один в Круге -- Круг распускается
}
// ---------------- Сохранения: игровое состояние ----------------
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();
}
}