Армия (п. 2.1): - ReturnFromMarch / ReturnUnits возвращают bool и зачисляют воинов только по известному походу и только один раз (раньше неизвестный поход добавлял выживших пехотой, ReturnUnits — без ограничения) - вернуться может не больше, чем ушло - TakeForMarch отклоняет повторный номер похода (раньше перезапись теряла воинов первого) - загрузка сохранения и перенос старых данных пропускают дубликаты походов Мир (п. 2.2): - TryStartMarch проверяет TakeForMarch; при отказе поход откатывается (WorldSimulation.CancelMarch) - если Армия не знает возвращающийся поход — ни воинов, ни добычи, предупреждение в консоли Тесты (п. 2.1, 2.2, 2.4): - неизвестный поход, повторное возвращение, повторный номер, откат похода - крайние случаи боя: 1×1, 1×10, 10×1, 0×0, пустая сторона, бонусы −100% / −500% / +100000%, бой числом Формула потерь не менялась. Найдено и записано в Battle.md как открытый вопрос: одиночный проигравший не гибнет (потери округляются вниз) — правило округления решает команда. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
12 KiB
C#
267 lines
12 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);
|
||
|
||
// Лимит склада для ресурса. 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);
|
||
}
|
||
|
||
// ---------------- Поселение: здания и развитие города ----------------
|
||
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; } // сколько жителей вообще могут работать (взрослые; стартовый город -- 120)
|
||
int FreeWorkers { get; } // могут работать, но без работы: Workforce - Workers
|
||
int Soldiers { get; } // воины -- их хранит модуль Армии
|
||
int HousingCap { get; } // предел населения: выше него прирост не идёт
|
||
|
||
float FoodPerHour { get; } // сколько город съедает в час
|
||
float GrowthPerHour { get; } // прирост в час сейчас (0 -- если голод или упёрлись в предел)
|
||
bool IsStarving { get; } // еды не хватило на последний расчёт
|
||
|
||
// Досчитать население до момента unixNow (вызывается раз в несколько секунд и при перемотке)
|
||
void Tick(long unixNow);
|
||
|
||
// Рабочие по профессиям (этап 3)
|
||
int GetWorkers(WorkJob job);
|
||
// Поставить count рабочих на профессию (в пределах мест в зданиях и свободных рабочих). Возвращает, сколько стоит теперь.
|
||
int SetWorkers(WorkJob job, int count);
|
||
}
|
||
|
||
// ---------------- Армия: войска ----------------
|
||
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); // потери в бою: доля от каждого типа; возвращает, сколько погибло
|
||
|
||
// Забрать свободных воинов в поход (запоминает состав похода).
|
||
// 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);
|
||
}
|
||
|
||
// ---------------- Глобальный мир: карта и перемещения ----------------
|
||
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();
|
||
}
|
||
}
|