Files
MVP1/Assets/Scripts/Modules/Core/Contracts/ModuleContracts.cs
T
AlexanderandClaude Opus 5.5 f10fb3b3aa Модуль Армии хранит войска; окно «Войско вернулось» в городе
Модуль Армии (Bogatyri.Army, без Unity):
- ArmyService — единственное хранилище войск (4 типа), секция сохранения "army"
- IArmy расширен: войска по типам, обучение (AddTroops), потери (LoseFraction), войска в походах
- событие TroopsChangedEvent — по нему перерисовываются панели HUD
- GameProgress и панели работают с войсками только через IArmy
- перенос из progress.json (troops, sashaMarchUnits) при первой загрузке;
  старые данные удаляются только после записи save.json (флаг armyInModule)
- ArmyBridge удалён: контракты и перенос сохранений проверены тестами
- тесты: поход не создаёт и не теряет солдат, потери по типам, сохранение, перенос, сброс

Окно «ВОЙСКО ВЕРНУЛОСЬ» в городе (UI/MarchReturnPopup):
- куда ходили, победа/поражение, сколько вернулось из скольких, добыча
- показывается и за походы, закончившиеся пока игра была закрыта; несколько — по очереди
- в MarchReturnedEvent добавлены Sent, TargetName, LootText

GameLoadedEvent публикуется и без файла сохранения. Документы Army, Core, Save, World, README обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:33:04 +02:00

192 lines
6.9 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 IArmy
{
int TypeCount { get; } // типов войск (пехота, лучники, кавалерия, осадные)
int GetTroops(int type); // воинов этого типа в городе
int SoldiersAtHome { get; } // всего воинов в городе
int TroopsInMarches { get; } // воинов в походах
void AddTroops(int type, int count); // обучены / вернулись
int LoseFraction(double fraction); // потери в бою: доля от каждого типа; возвращает, сколько погибло
// Забрать воинов из города в поход (запоминает состав похода)
bool TakeForMarch(int marchId, int count);
// Вернуть выживших из похода по тем же типам войск
void ReturnFromMarch(int marchId, int survivors);
}
// ---------------- Глобальный мир: карта и перемещения ----------------
public interface IWorld
{
int ActiveMarches { get; }
bool TryStartMarch(string targetId, int soldiers, out string error);
}
// ---------------- Боевая система: урон, потери, исход ----------------
public struct BattleRequest
{
public int Attackers;
public int Defenders;
}
public struct BattleOutcome
{
public bool Victory; // победил атакующий
public int AttackerLosses;
public int DefenderLosses;
public int Survivors; // сколько атакующих осталось
}
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();
}
}