Compare commits

...

9 Commits

44 changed files with 4688 additions and 10 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Git
.gitignore

2
.gitignore vendored
View File

@@ -5,8 +5,6 @@ export_presets.cfg
# Mono/Godot C# # Mono/Godot C#
mono/ mono/
*.csproj
*.sln
obj/ obj/
bin/ bin/
*.pidb *.pidb

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
# MyBiz - Экономический симулятор
## Структура проекта
```
my-game/
├── backend/ # C# + Akka.net
├── frontend/ # Godot 4+
├── shared/ # Общие компоненты
├── docs/ # Документация
└── tools/ # Утилиты
```
## Быстрый старт
### Бэкенд
```bash
cd backend
dotnet build
dotnet test
```
### Фронтенд
1. Открыть Godot 4+
2. Указать путь к `frontend/`
3. Запустить проект
## Документация
См. [docs/TZ.md](docs/TZ.md) - техническое задание проекта.
## Лицензия
[TBD]

11
backend/MyBiz.slnx Normal file
View File

@@ -0,0 +1,11 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/MyBiz.Core/MyBiz.Core.csproj" />
<Project Path="src/MyBiz.Economy/MyBiz.Economy.csproj" />
<Project Path="src/MyBiz.Production/MyBiz.Production.csproj" />
<Project Path="src/MyBiz.Trade/MyBiz.Trade.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MyBiz.Tests/MyBiz.Tests.csproj" />
</Folder>
</Solution>

36
backend/README.md Normal file
View File

@@ -0,0 +1,36 @@
# Backend - Экономический симулятор
Бэкенд на C# + Akka.net для экономического симулятора.
## Структура
```
backend/
├── src/
│ ├── MyBiz.Core/ # Ядро: модели, интерфейсы
│ ├── MyBiz.Economy/ # Экономическая модель
│ ├── MyBiz.Production/ # Производство, цепочки
│ ├── MyBiz.Trade/ # Торговля, рынки
│ ├── MyBiz.Research/ # Исследования, tech tree
│ ├── MyBiz.Infrastructure/# Актеры, менеджеры
│ └── MyBiz.API/ # API для фронтенда
└── tests/
├── MyBiz.Core.Tests/
├── MyBiz.Economy.Tests/
└── ...
```
## Технологии
- .NET 8
- Akka.net (акторная модель)
- xUnit (тесты)
- SQLite (хранение состояния)
## Запуск
```bash
cd backend
dotnet build
dotnet test
```

View File

@@ -0,0 +1,176 @@
namespace MyBiz.Core;
/// <summary>
/// Результат аутентификации
/// </summary>
public class AuthResult
{
/// <summary>
/// Успешна ли аутентификация
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Пользователь (если успешно)
/// </summary>
public User? User { get; set; }
/// <summary>
/// Токен доступа (заглушка)
/// </summary>
public string? Token { get; set; }
/// <summary>
/// Сообщение об ошибке
/// </summary>
public string? ErrorMessage { get; set; }
}
/// <summary>
/// Сервис аутентификации (заглушка для MVP)
/// </summary>
public class AuthService
{
private readonly Dictionary<string, User> _usersByUsername = new();
private readonly Dictionary<Guid, User> _usersById = new();
private readonly Dictionary<string, Guid> _tokens = new(); // token -> userId
/// <summary>
/// Зарегистрировать нового пользователя
/// </summary>
public AuthResult Register(string username, string email, string password, string companyName)
{
if (_usersByUsername.ContainsKey(username))
{
return new AuthResult
{
Success = false,
ErrorMessage = "Пользователь с таким именем уже существует"
};
}
var user = new User
{
Username = username,
Email = email,
PasswordHash = HashPassword(password),
Company = new Company
{
Name = companyName,
Cash = 100000m, // Стартовый капитал
Assets = 0,
Liabilities = 0
}
};
user.CompanyId = user.Company.Id;
_usersByUsername[username] = user;
_usersById[user.Id] = user;
var token = GenerateToken(user.Id);
_tokens[token] = user.Id; // Сохраняем токен
return new AuthResult
{
Success = true,
User = user,
Token = token
};
}
/// <summary>
/// Войти в систему
/// </summary>
public AuthResult Login(string username, string password)
{
if (!_usersByUsername.TryGetValue(username, out var user))
{
return new AuthResult
{
Success = false,
ErrorMessage = "Пользователь не найден"
};
}
if (!VerifyPassword(password, user.PasswordHash))
{
return new AuthResult
{
Success = false,
ErrorMessage = "Неверный пароль"
};
}
user.LastLoginAt = DateTime.UtcNow;
return new AuthResult
{
Success = true,
User = user,
Token = GenerateToken(user.Id)
};
}
/// <summary>
/// Выйти из системы
/// </summary>
public void Logout(string token)
{
if (_tokens.ContainsKey(token))
{
_tokens.Remove(token);
}
}
/// <summary>
/// Проверить токен и получить пользователя
/// </summary>
public User? ValidateToken(string token)
{
if (_tokens.TryGetValue(token, out var userId))
{
return _usersById.TryGetValue(userId, out var user) ? user : null;
}
return null;
}
/// <summary>
/// Получить пользователя по ID
/// </summary>
public User? GetUserById(Guid userId)
{
return _usersById.TryGetValue(userId, out var user) ? user : null;
}
/// <summary>
/// Получить пользователя по имени
/// </summary>
public User? GetUserByUsername(string username)
{
return _usersByUsername.TryGetValue(username, out var user) ? user : null;
}
#region Helpers (заглушки)
private static string HashPassword(string password)
{
// TODO: Использовать реальное хеширование (BCrypt, PBKDF2)
return $"hash_{password}";
}
private static bool VerifyPassword(string password, string hash)
{
// TODO: Использовать реальную проверку пароля
return hash == $"hash_{password}";
}
private static string GenerateToken(Guid userId)
{
// TODO: Использовать JWT или аналогичный механизм
var token = $"token_{userId}_{Guid.NewGuid()}";
return token;
}
#endregion
}

View File

@@ -0,0 +1,184 @@
namespace MyBiz.Core;
/// <summary>
/// Конфигурация типа здания - для моддинга
/// </summary>
public class BuildingTypeConfig
{
/// <summary>
/// Уникальный идентификатор типа здания
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Отображаемое имя
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Описание
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Категория здания
/// </summary>
public BuildingCategory Category { get; set; }
/// <summary>
/// Стоимость постройки
/// </summary>
public decimal BuildCost { get; set; }
/// <summary>
/// Стоимость содержания в тик
/// </summary>
public decimal UpkeepCost { get; set; }
/// <summary>
/// Вместимость склада
/// </summary>
public int StorageCapacity { get; set; }
/// <summary>
/// Количество рабочих мест
/// </summary>
public int WorkerSlots { get; set; }
/// <summary>
/// Базовая эффективность (0-100)
/// </summary>
public int BaseEfficiency { get; set; } = 100;
/// <summary>
/// Максимальный уровень здания
/// </summary>
public int MaxLevel { get; set; } = 10;
/// <summary>
/// Иконка здания
/// </summary>
public string IconPath { get; set; } = string.Empty;
/// <summary>
/// Требуемые технологии для разблокировки
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда здание становится доступным
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
/// <summary>
/// Производимые продукты (для фабрик)
/// </summary>
public List<string> OutputProducts { get; set; } = new();
/// <summary>
/// Потребляемые продукты (для фабрик)
/// </summary>
public List<string> InputProducts { get; set; } = new();
}
/// <summary>
/// Категория здания
/// </summary>
public enum BuildingCategory
{
RawMaterial, // Добыча сырья
Production, // Производство
Trade, // Торговля
Research, // Исследования
Storage, // Склад
Office // Офис
}
/// <summary>
/// Здание - экземпляр в игре
/// </summary>
public class Building
{
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Конфигурация типа здания
/// </summary>
public BuildingTypeConfig TypeConfig { get; set; } = null!;
/// <summary>
/// Отображаемое имя (можно кастомизировать)
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// ID города, где находится здание
/// </summary>
public string CityId { get; set; } = string.Empty;
/// <summary>
/// Координаты на карте
/// </summary>
public int X { get; set; }
public int Y { get; set; }
/// <summary>
/// Уровень здания (влияет на эффективность)
/// </summary>
public int Level { get; set; } = 1;
/// <summary>
/// Текущая эффективность (зависит от уровня и рабочих)
/// </summary>
public int CurrentEfficiency { get; set; }
/// <summary>
/// Количество рабочих мест
/// </summary>
public int WorkerSlots => TypeConfig.WorkerSlots;
/// <summary>
/// Заполненность рабочими
/// </summary>
public int Workers { get; set; }
/// <summary>
/// Вместимость склада
/// </summary>
public int StorageCapacity => TypeConfig.StorageCapacity;
/// <summary>
/// Текущие запасы на складе
/// </summary>
public Dictionary<string, int> Inventory { get; set; } = new();
/// <summary>
/// Здание активно/работает
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Построено в тик
/// </summary>
public int BuiltAtTick { get; set; }
/// <summary>
/// Рассчитать текущую эффективность
/// </summary>
public void CalculateEfficiency()
{
int baseEff = TypeConfig.BaseEfficiency;
int levelBonus = (Level - 1) * 5; // +5% за уровень
int workerPenalty = WorkerSlots > 0 ? ((WorkerSlots - Workers) * 100 / WorkerSlots) : 0;
CurrentEfficiency = Math.Max(0, baseEff + levelBonus - workerPenalty);
}
/// <summary>
/// Обновить состояние
/// </summary>
public void Update()
{
CalculateEfficiency();
}
}

View File

@@ -0,0 +1,437 @@
namespace MyBiz.Core;
/// <summary>
/// Тип бизнес-единицы
/// </summary>
public enum BusinessUnitType
{
/// <summary>
/// Магазин (розничная торговля)
/// </summary>
Shop,
/// <summary>
/// Фабрика (производство)
/// </summary>
Factory,
/// <summary>
/// Склад (хранение)
/// </summary>
Warehouse,
/// <summary>
/// Офис (управление)
/// </summary>
Office,
/// <summary>
/// Научный центр (исследования)
/// </summary>
ResearchLab
}
/// <summary>
/// Базовая бизнес-единица (магазин, фабрика и т.д.)
/// </summary>
public class BusinessUnit
{
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Название единицы
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Тип бизнес-единицы
/// </summary>
public BusinessUnitType Type { get; set; }
/// <summary>
/// ID компании-владельца
/// </summary>
public Guid CompanyId { get; set; }
/// <summary>
/// Город, где находится единица
/// </summary>
public string CityId { get; set; } = string.Empty;
/// <summary>
/// Координаты на карте
/// </summary>
public int X { get; set; }
public int Y { get; set; }
/// <summary>
/// Уровень развития (1-max)
/// </summary>
public int Level { get; set; } = 1;
/// <summary>
/// Максимальный уровень
/// </summary>
public int MaxLevel { get; set; } = 10;
/// <summary>
/// Количество сотрудников
/// </summary>
public int Employees { get; set; }
/// <summary>
/// Максимальное количество сотрудников
/// </summary>
public int MaxEmployees { get; set; } = 50;
/// <summary>
/// Эффективность работы (0-100)
/// </summary>
public int Efficiency { get; set; } = 100;
/// <summary>
/// Стоимость постройки
/// </summary>
public decimal BuildCost { get; set; }
/// <summary>
/// Содержание в тик (зарплаты, аренда и т.д.)
/// </summary>
public decimal UpkeepCost { get; set; }
/// <summary>
/// Доход за последний период
/// </summary>
public decimal LastPeriodIncome { get; set; }
/// <summary>
/// Расходы за последний период
/// </summary>
public decimal LastPeriodExpenses { get; set; }
/// <summary>
/// Дата постройки
/// </summary>
public int BuiltAtTick { get; set; }
/// <summary>
/// Активна ли единица (работает)
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Ссылка на здание (если есть)
/// </summary>
public Building? Building { get; set; }
/// <summary>
/// Запасы на складе (для магазинов/складов)
/// </summary>
public Dictionary<string, int> Inventory { get; set; } = new();
/// <summary>
/// Производственная цепочка (для фабрик)
/// </summary>
public ActiveProductionChain? ActiveProduction { get; set; }
/// <summary>
/// Рассчитать прибыль за период
/// </summary>
public decimal PeriodProfit => LastPeriodIncome - LastPeriodExpenses;
/// <summary>
/// Рассчитать рентабельность (%)
/// </summary>
public decimal Profitability => LastPeriodExpenses > 0
? (PeriodProfit / LastPeriodExpenses) * 100
: 0;
/// <summary>
/// Обновить эффективность на основе сотрудников
/// </summary>
public void UpdateEfficiency()
{
if (MaxEmployees == 0)
{
Efficiency = 0;
return;
}
// Базовая эффективность зависит от заполненности рабочих мест
var staffRate = (decimal)Employees / MaxEmployees;
Efficiency = (int)(100 * staffRate);
// Бонус за уровень
Efficiency += (Level - 1) * 5;
// Ограничение 0-100
Efficiency = Math.Clamp(Efficiency, 0, 100);
}
/// <summary>
/// Обновить состояние (вызывается каждый тик)
/// </summary>
public virtual void Tick()
{
// Обновление производства (если есть)
ActiveProduction?.Tick();
// Обновление эффективности
UpdateEfficiency();
}
/// <summary>
/// Нанять сотрудника
/// </summary>
public bool HireEmployee()
{
if (Employees < MaxEmployees)
{
Employees++;
return true;
}
return false;
}
/// <summary>
/// Уволить сотрудника
/// </summary>
public bool FireEmployee()
{
if (Employees > 0)
{
Employees--;
return true;
}
return false;
}
/// <summary>
/// Повысить уровень
/// </summary>
public bool Upgrade()
{
if (Level < MaxLevel)
{
Level++;
MaxEmployees += 10; // Бонус за уровень
return true;
}
return false;
}
}
/// <summary>
/// Магазин (розничная торговля)
/// </summary>
public class Shop : BusinessUnit
{
public Shop()
{
Type = BusinessUnitType.Shop;
MaxEmployees = 20;
BuildCost = 50000m;
UpkeepCost = 1000m;
}
/// <summary>
/// Торгуемые продукты
/// </summary>
public List<string> SoldProductTypes { get; set; } = new();
/// <summary>
/// Выручка за сегодня
/// </summary>
public decimal DailyRevenue { get; set; }
/// <summary>
/// Количество клиентов за сегодня
/// </summary>
public int DailyCustomers { get; set; }
/// <summary>
/// Средний чек
/// </summary>
public decimal AverageCheck => DailyCustomers > 0 ? DailyRevenue / DailyCustomers : 0;
}
/// <summary>
/// Фабрика (производство)
/// </summary>
public class Factory : BusinessUnit
{
public Factory()
{
Type = BusinessUnitType.Factory;
MaxEmployees = 100;
BuildCost = 200000m;
UpkeepCost = 5000m;
}
/// <summary>
/// Производимый продукт
/// </summary>
public string? OutputProductId { get; set; }
/// <summary>
/// Требуемые ресурсы (входные продукты)
/// </summary>
public Dictionary<string, int> RequiredInputs { get; set; } = new();
/// <summary>
/// Произведено за последний тик
/// </summary>
public int LastTickOutput { get; set; }
/// <summary>
/// Простой (нет ресурсов)
/// </summary>
public bool IsIdle => ActiveProduction == null || !ActiveProduction.IsActive;
}
/// <summary>
/// Склад (хранение)
/// </summary>
public class Warehouse : BusinessUnit
{
public Warehouse()
{
Type = BusinessUnitType.Warehouse;
MaxEmployees = 10;
BuildCost = 30000m;
UpkeepCost = 500m;
}
/// <summary>
/// Вместимость склада (единиц товара)
/// </summary>
public int Capacity { get; set; } = 10000;
/// <summary>
/// Использовано места
/// </summary>
public int UsedCapacity => Inventory.Values.Sum();
/// <summary>
/// Свободно места
/// </summary>
public int FreeCapacity => Capacity - UsedCapacity;
/// <summary>
/// Добавить товар на склад
/// </summary>
public bool AddToInventory(string productId, int quantity)
{
if (quantity > FreeCapacity)
{
return false;
}
if (Inventory.ContainsKey(productId))
{
Inventory[productId] += quantity;
}
else
{
Inventory[productId] = quantity;
}
return true;
}
/// <summary>
/// Взять товар со склада
/// </summary>
public int RemoveFromInventory(string productId, int quantity)
{
if (!Inventory.ContainsKey(productId))
{
return 0;
}
var actualQuantity = Math.Min(quantity, Inventory[productId]);
Inventory[productId] -= actualQuantity;
if (Inventory[productId] == 0)
{
Inventory.Remove(productId);
}
return actualQuantity;
}
}
/// <summary>
/// Офис (управление компанией)
/// </summary>
public class Office : BusinessUnit
{
public Office()
{
Type = BusinessUnitType.Office;
MaxEmployees = 50;
BuildCost = 100000m;
UpkeepCost = 3000m;
}
/// <summary>
/// Бонус к управлению (влияет на эффективность других зданий)
/// </summary>
public int ManagementBonus { get; set; } = 5;
/// <summary>
/// Количество управляемых зданий
/// </summary>
public int ManagedBuildings { get; set; }
}
/// <summary>
/// Научный центр (исследования)
/// </summary>
public class ResearchLab : BusinessUnit
{
public ResearchLab()
{
Type = BusinessUnitType.ResearchLab;
MaxEmployees = 30;
BuildCost = 150000m;
UpkeepCost = 4000m;
}
/// <summary>
/// Текущее исследование
/// </summary>
public string? CurrentResearch { get; set; }
/// <summary>
/// Прогресс исследования (0-100%)
/// </summary>
public int ResearchProgress { get; set; }
/// <summary>
/// Наука за тик
/// </summary>
public int SciencePerTick => Employees * 10 * Efficiency / 100;
/// <summary>
/// Начать исследование
/// </summary>
public void StartResearch(string technologyId)
{
CurrentResearch = technologyId;
ResearchProgress = 0;
}
/// <summary>
/// Обновление исследования
/// </summary>
public override void Tick()
{
base.Tick();
if (CurrentResearch != null && ResearchProgress < 100)
{
ResearchProgress = Math.Min(100, ResearchProgress + SciencePerTick);
}
}
}

View File

@@ -0,0 +1,47 @@
namespace MyBiz.Core;
/// <summary>
/// Размер города
/// </summary>
public enum CitySize
{
Small, // Малый
Medium, // Средний
Large // Крупный
}
/// <summary>
/// Город
/// </summary>
public class City
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public CitySize Size { get; set; }
/// <summary>
/// Население
/// </summary>
public int Population { get; set; }
/// <summary>
/// Доступные здания
/// </summary>
public List<Building> Buildings { get; set; } = new();
/// <summary>
/// Рынок города (спрос на продукты)
/// </summary>
public Dictionary<ProductType, int> MarketDemand { get; set; } = new();
/// <summary>
/// Предложение на рынке
/// </summary>
public Dictionary<ProductType, int> MarketSupply { get; set; } = new();
/// <summary>
/// Текущие цены
/// </summary>
public Dictionary<ProductType, decimal> Prices { get; set; } = new();
}

View File

@@ -0,0 +1,130 @@
namespace MyBiz.Core;
/// <summary>
/// Компания игрока
/// </summary>
public class Company
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
/// <summary>
/// ID владельца (пользователя)
/// </summary>
public Guid OwnerId { get; set; }
/// <summary>
/// Доступные деньги
/// </summary>
public decimal Cash { get; set; }
/// <summary>
/// Активы компании
/// </summary>
public decimal Assets { get; set; }
/// <summary>
/// Пассивы (долги)
/// </summary>
public decimal Liabilities { get; set; }
/// <summary>
/// Прибыль за последний период
/// </summary>
public decimal LastPeriodProfit { get; set; }
/// <summary>
/// Список зданий компании
/// </summary>
public List<Building> Buildings { get; set; } = new();
/// <summary>
/// Бизнес-единицы компании (магазины, фабрики и т.д.)
/// </summary>
public List<BusinessUnit> BusinessUnits { get; set; } = new();
/// <summary>
/// Складские запасы
/// </summary>
public Dictionary<ProductType, int> Inventory { get; set; } = new();
/// <summary>
/// Открытые технологии
/// </summary>
public HashSet<string> UnlockedTechnologies { get; set; } = new();
/// <summary>
/// Дата основания
/// </summary>
public DateTime FoundedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Рассчитать чистую стоимость
/// </summary>
public decimal NetWorth => Assets - Liabilities + Cash;
/// <summary>
/// Получить все магазины
/// </summary>
public IEnumerable<Shop> Shops => BusinessUnits.OfType<Shop>();
/// <summary>
/// Получить все фабрики
/// </summary>
public IEnumerable<Factory> Factories => BusinessUnits.OfType<Factory>();
/// <summary>
/// Получить все склады
/// </summary>
public IEnumerable<Warehouse> Warehouses => BusinessUnits.OfType<Warehouse>();
/// <summary>
/// Добавить бизнес-единицу
/// </summary>
public void AddBusinessUnit(BusinessUnit unit)
{
unit.CompanyId = Id;
BusinessUnits.Add(unit);
}
/// <summary>
/// Удалить бизнес-единицу
/// </summary>
public void RemoveBusinessUnit(Guid unitId)
{
var unit = BusinessUnits.FirstOrDefault(u => u.Id == unitId);
if (unit != null)
{
BusinessUnits.Remove(unit);
}
}
/// <summary>
/// Обновить состояние всех единиц (вызывается каждый тик)
/// </summary>
public void Tick()
{
foreach (var unit in BusinessUnits)
{
unit.Tick();
}
}
/// <summary>
/// Рассчитать общую прибыль за период
/// </summary>
public decimal CalculateTotalProfit()
{
return BusinessUnits.Sum(u => u.PeriodProfit);
}
/// <summary>
/// Рассчитать общую стоимость активов
/// </summary>
public decimal CalculateAssets()
{
return Buildings.Sum(b => b.TypeConfig?.BuildCost ?? 0) +
BusinessUnits.Sum(u => u.BuildCost) +
Inventory.Sum(i => i.Key.BasePrice * i.Value);
}
}

View File

@@ -0,0 +1,202 @@
using MyBiz.Core;
namespace MyBiz.Core;
/// <summary>
/// Дефолтные конфигурации продуктов
/// </summary>
public static class DefaultProducts
{
public static IEnumerable<ProductType> GetAll()
{
return new List<ProductType>
{
// === СЫРЬЁ ===
new ProductType
{
Id = "raw_cotton",
Name = "Хлопок",
Description = "Сырьё для производства ткани",
Category = ProductCategory.RawMaterial,
BasePrice = 5m,
BaseDemand = 50,
DemandElasticity = 0.3f,
StackSize = 500,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/cotton.png"
},
new ProductType
{
Id = "raw_steel",
Name = "Сталь",
Description = "Основной металл для промышленности",
Category = ProductCategory.RawMaterial,
BasePrice = 15m,
BaseDemand = 80,
DemandElasticity = 0.4f,
StackSize = 200,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/steel.png"
},
new ProductType
{
Id = "raw_plastic",
Name = "Пластик",
Description = "Синтетический материал",
Category = ProductCategory.RawMaterial,
BasePrice = 8m,
BaseDemand = 60,
DemandElasticity = 0.5f,
StackSize = 300,
AvailableFromYear = 1950,
IconPath = "res://assets/sprites/products/plastic.png"
},
new ProductType
{
Id = "raw_food",
Name = "Сельхозпродукция",
Description = "Пшеница, овощи, фрукты",
Category = ProductCategory.RawMaterial,
BasePrice = 3m,
BaseDemand = 100,
DemandElasticity = 0.2f,
ShelfLife = 10,
StackSize = 500,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/food_raw.png"
},
// === КОМПОНЕНТЫ ===
new ProductType
{
Id = "comp_fabric",
Name = "Ткань",
Description = "Материал для пошива одежды",
Category = ProductCategory.Component,
BasePrice = 12m,
BaseDemand = 40,
DemandElasticity = 0.6f,
StackSize = 200,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/fabric.png"
},
new ProductType
{
Id = "comp_metal_parts",
Name = "Металлоизделия",
Description = "Детали и запчасти из металла",
Category = ProductCategory.Component,
BasePrice = 25m,
BaseDemand = 50,
DemandElasticity = 0.5f,
StackSize = 100,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/metal_parts.png"
},
new ProductType
{
Id = "comp_plastic_parts",
Name = "Пластиковые детали",
Description = "Компоненты из пластика",
Category = ProductCategory.Component,
BasePrice = 18m,
BaseDemand = 45,
DemandElasticity = 0.5f,
StackSize = 150,
AvailableFromYear = 1950,
IconPath = "res://assets/sprites/products/plastic_parts.png"
},
new ProductType
{
Id = "comp_electronics",
Name = "Электронные компоненты",
Description = "Микросхемы, транзисторы, платы",
Category = ProductCategory.Component,
BasePrice = 50m,
BaseDemand = 30,
DemandElasticity = 0.7f,
StackSize = 50,
AvailableFromYear = 1960,
IconPath = "res://assets/sprites/products/electronics.png"
},
// === ТОВАРЫ ===
new ProductType
{
Id = "goods_food",
Name = "Продукты питания",
Description = "Готовая еда для потребителей",
Category = ProductCategory.ConsumerGoods,
BasePrice = 8m,
BaseDemand = 150,
DemandElasticity = 0.3f,
ShelfLife = 5,
StackSize = 200,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/food.png"
},
new ProductType
{
Id = "goods_clothing",
Name = "Одежда",
Description = "Одежда и обувь",
Category = ProductCategory.ConsumerGoods,
BasePrice = 35m,
BaseDemand = 60,
DemandElasticity = 0.8f,
StackSize = 50,
AvailableFromYear = 1900,
IconPath = "res://assets/sprites/products/clothing.png"
},
new ProductType
{
Id = "goods_electronics",
Name = "Электроника",
Description = "Бытовая техника и гаджеты",
Category = ProductCategory.ConsumerGoods,
BasePrice = 150m,
BaseDemand = 25,
DemandElasticity = 0.9f,
StackSize = 20,
AvailableFromYear = 1960,
IconPath = "res://assets/sprites/products/electronics_consumer.png"
},
new ProductType
{
Id = "goods_automobile",
Name = "Автомобили",
Description = "Легковые автомобили",
Category = ProductCategory.ConsumerGoods,
BasePrice = 5000m,
BaseDemand = 10,
DemandElasticity = 1.0f,
StackSize = 5,
AvailableFromYear = 1920,
IconPath = "res://assets/sprites/products/automobile.png"
}
};
}
/// <summary>
/// Зарегистрировать все дефолтные продукты в реестре
/// </summary>
public static void RegisterAll(ProductRegistry registry)
{
foreach (var product in GetAll())
{
registry.Register(product);
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Akka" Version="1.5.60" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,104 @@
namespace MyBiz.Core;
/// <summary>
/// Продукт - экземпляр товара в игре
/// </summary>
public class Product
{
/// <summary>
/// Уникальный идентификатор экземпляра продукта
/// </summary>
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Тип продукта (ссылка на конфигурацию)
/// </summary>
public ProductType Type { get; set; } = null!;
/// <summary>
/// Количество продукта
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// Текущая цена за единицу
/// </summary>
public decimal CurrentPrice { get; set; }
/// <summary>
/// Дата создания (для отслеживания срока годности)
/// </summary>
public int CreatedAtTick { get; set; }
/// <summary>
/// Качество продукта (0-100, 100 = идеальное)
/// </summary>
public int Quality { get; set; } = 100;
/// <summary>
/// Проверка: испорчен ли продукт
/// </summary>
public bool IsSpoiled { get; set; } = false;
/// <summary>
/// Общая стоимость продукта
/// </summary>
public decimal TotalValue => Quantity * CurrentPrice;
/// <summary>
/// Проверка: может ли продукт быть использован/продан
/// </summary>
public bool IsUsable => Quantity > 0 && !IsSpoiled;
/// <summary>
/// Добавить количество
/// </summary>
public void Add(int amount)
{
if (amount > 0)
{
Quantity += amount;
}
}
/// <summary>
/// Удалить количество
/// </summary>
/// <returns>Фактически удалённое количество</returns>
public int Remove(int amount)
{
if (amount <= 0 || Quantity <= 0)
return 0;
int removed = Math.Min(amount, Quantity);
Quantity -= removed;
if (Quantity <= 0)
{
IsSpoiled = true;
}
return removed;
}
/// <summary>
/// Обновить состояние (проверка срока годности)
/// </summary>
public void Update(int currentTick)
{
if (Type.IsPerishable)
{
int age = currentTick - CreatedAtTick;
if (age >= Type.ShelfLife)
{
IsSpoiled = true;
}
// Ухудшение качества со временем
if (age > 0 && Type.ShelfLife > 0)
{
Quality = Math.Max(0, 100 - (age * 100 / Type.ShelfLife));
}
}
}
}

View File

@@ -0,0 +1,116 @@
namespace MyBiz.Core;
/// <summary>
/// Реестр типов продуктов - центрлизованное хранилище конфигураций
/// Поддерживает моддинг через загрузку внешних конфигураций
/// </summary>
public class ProductRegistry
{
private readonly Dictionary<string, ProductType> _productTypes = new();
/// <summary>
/// Все зарегистрированные типы продуктов
/// </summary>
public IEnumerable<ProductType> AllProductTypes => _productTypes.Values;
/// <summary>
/// Количество зарегистрированных типов
/// </summary>
public int Count => _productTypes.Count;
/// <summary>
/// Событие: добавлен новый тип продукта (для моддинга)
/// </summary>
public event Action<ProductType>? ProductTypeAdded;
/// <summary>
/// Событие: изменён тип продукта
/// </summary>
public event Action<ProductType>? ProductTypeModified;
/// <summary>
/// Зарегистрировать тип продукта
/// </summary>
public void Register(ProductType productType)
{
if (string.IsNullOrEmpty(productType.Id))
throw new ArgumentException("Product type must have an Id", nameof(productType));
if (_productTypes.ContainsKey(productType.Id))
throw new InvalidOperationException($"Product type '{productType.Id}' is already registered");
_productTypes[productType.Id] = productType;
ProductTypeAdded?.Invoke(productType);
}
/// <summary>
/// Получить тип продукта по ID
/// </summary>
public ProductType? GetById(string id)
{
return _productTypes.TryGetValue(id, out var type) ? type : null;
}
/// <summary>
/// Получить тип продукта или выбросить исключение
/// </summary>
public ProductType GetOrThrow(string id)
{
return GetById(id) ?? throw new KeyNotFoundException($"Product type '{id}' not found");
}
/// <summary>
/// Проверка: существует ли тип продукта
/// </summary>
public bool Exists(string id)
{
return _productTypes.ContainsKey(id);
}
/// <summary>
/// Получить продукты по категории
/// </summary>
public IEnumerable<ProductType> GetByCategory(ProductCategory category)
{
return _productTypes.Values.Where(p => p.Category == category);
}
/// <summary>
/// Получить продукты, доступные в указанном году
/// </summary>
public IEnumerable<ProductType> GetAvailableInYear(int year)
{
return _productTypes.Values.Where(p => p.AvailableFromYear <= year);
}
/// <summary>
/// Удалить тип продукта (для моддинга)
/// </summary>
public bool Remove(string id)
{
if (_productTypes.Remove(id))
{
ProductTypeModified?.Invoke(new ProductType { Id = id });
return true;
}
return false;
}
/// <summary>
/// Загрузить типы продуктов из конфигурации (JSON и т.п.)
/// </summary>
public void LoadFromConfig(string jsonConfig)
{
// TODO: Реализовать загрузку из JSON
// Это позволит модам добавлять свои продукты
}
/// <summary>
/// Экспортировать все типы в JSON (для моддинга)
/// </summary>
public string ExportToJson()
{
// TODO: Реализовать экспорт в JSON
return "{}";
}
}

View File

@@ -0,0 +1,105 @@
namespace MyBiz.Core;
/// <summary>
/// Категория продукта
/// </summary>
public enum ProductCategory
{
RawMaterial, // Сырьё
Component, // Компоненты
ConsumerGoods, // Товары народного потребления
Luxury // Предметы роскоши (для будущего)
}
/// <summary>
/// Тип продукта - конфигурация для моддинга
/// </summary>
public class ProductType
{
/// <summary>
/// Уникальный идентификатор типа продукта (для моддинга)
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Отображаемое имя
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Описание
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Категория продукта
/// </summary>
public ProductCategory Category { get; set; }
/// <summary>
/// Базовая цена продукта
/// </summary>
public decimal BasePrice { get; set; }
/// <summary>
/// Базовый спрос (единиц в тик)
/// </summary>
public int BaseDemand { get; set; }
/// <summary>
/// Эластичность спроса по цене (0-1, где 1 - высокая эластичность)
/// </summary>
public float DemandElasticity { get; set; } = 0.5f;
/// <summary>
/// Срок хранения (тиков), 0 = бессрочно
/// </summary>
public int ShelfLife { get; set; } = 0;
/// <summary>
/// Размер стека (для инвентаря)
/// </summary>
public int StackSize { get; set; } = 100;
/// <summary>
/// Иконка продукта (путь к ресурсу)
/// </summary>
public string IconPath { get; set; } = string.Empty;
/// <summary>
/// Требуемые технологии для разблокировки (пусто = доступно сразу)
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда продукт становится доступным (для исторического режима)
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
/// <summary>
/// Может ли продукт быть испорчен
/// </summary>
public bool IsPerishable => ShelfLife > 0;
/// <summary>
/// Создать копию типа продукта
/// </summary>
public ProductType Clone()
{
return new ProductType
{
Id = this.Id,
Name = this.Name,
Description = this.Description,
Category = this.Category,
BasePrice = this.BasePrice,
BaseDemand = this.BaseDemand,
DemandElasticity = this.DemandElasticity,
ShelfLife = this.ShelfLife,
StackSize = this.StackSize,
IconPath = this.IconPath,
RequiredTechnologies = new List<string>(this.RequiredTechnologies),
AvailableFromYear = this.AvailableFromYear
};
}
}

View File

@@ -0,0 +1,125 @@
namespace MyBiz.Core;
/// <summary>
/// Шаг производственной цепочки
/// </summary>
public class ProductionStep
{
/// <summary>
/// ID требуемого продукта
/// </summary>
public string InputProductId { get; set; } = string.Empty;
/// <summary>
/// Количество требуемого продукта
/// </summary>
public int InputQuantity { get; set; }
/// <summary>
/// Время производства (в тиках)
/// </summary>
public int ProductionTime { get; set; }
}
/// <summary>
/// Конфигурация производственной цепочки - для моддинга
/// </summary>
public class ProductionChainConfig
{
/// <summary>
/// Уникальный идентификатор цепочки
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Название цепочки
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// ID выходного продукта
/// </summary>
public string OutputProductId { get; set; } = string.Empty;
/// <summary>
/// Количество выходного продукта за цикл
/// </summary>
public int OutputQuantity { get; set; }
/// <summary>
/// ID требуемого здания
/// </summary>
public string RequiredBuildingId { get; set; } = string.Empty;
/// <summary>
/// Шаги производства
/// </summary>
public List<ProductionStep> Steps { get; set; } = new();
/// <summary>
/// Требуемые технологии
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда цепочка становится доступной
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
}
/// <summary>
/// Активная производственная цепочка на здании
/// </summary>
public class ActiveProductionChain
{
public ProductionChainConfig Config { get; set; } = null!;
public Building Building { get; set; } = null!;
/// <summary>
/// Текущий шаг производства
/// </summary>
public int CurrentStep { get; set; } = 0;
/// <summary>
/// Прогресс текущего шага (в тиках)
/// </summary>
public int Progress { get; set; } = 0;
/// <summary>
/// Цепочка активна
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Запущена в тик
/// </summary>
public int StartedAtTick { get; set; }
/// <summary>
/// Проверка: завершён ли текущий шаг
/// </summary>
public bool IsStepComplete => CurrentStep < Config.Steps.Count &&
Progress >= Config.Steps[CurrentStep].ProductionTime;
/// <summary>
/// Проверка: завершено ли всё производство
/// </summary>
public bool IsComplete => CurrentStep >= Config.Steps.Count;
/// <summary>
/// Продвинуть производство на 1 тик
/// </summary>
public void Tick()
{
if (!IsActive || IsComplete)
return;
Progress++;
if (IsStepComplete)
{
CurrentStep++;
Progress = 0;
}
}
}

View File

@@ -0,0 +1,90 @@
namespace MyBiz.Core;
/// <summary>
/// Пользователь системы
/// </summary>
public class User
{
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Имя пользователя (логин)
/// </summary>
public string Username { get; set; } = string.Empty;
/// <summary>
/// Email
/// </summary>
public string Email { get; set; } = string.Empty;
/// <summary>
/// Хеш пароля (не хранит сам пароль)
/// </summary>
public string PasswordHash { get; set; } = string.Empty;
/// <summary>
/// Дата регистрации
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Дата последнего входа
/// </summary>
public DateTime? LastLoginAt { get; set; }
/// <summary>
/// Активен ли пользователь
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Компания, принадлежащая пользователю
/// </summary>
public Company? Company { get; set; }
/// <summary>
/// ID компании (для быстрого доступа)
/// </summary>
public Guid? CompanyId { get; set; }
/// <summary>
/// Статистика игры
/// </summary>
public UserStats Stats { get; set; } = new();
}
/// <summary>
/// Статистика пользователя
/// </summary>
public class UserStats
{
/// <summary>
/// Всего игр сыграно
/// </summary>
public int GamesPlayed { get; set; }
/// <summary>
/// Всего часов в игре
/// </summary>
public int TotalHoursPlayed { get; set; }
/// <summary>
/// Максимальная чистая стоимость за всё время
/// </summary>
public decimal MaxNetWorth { get; set; }
/// <summary>
/// Всего зданий построено
/// </summary>
public int TotalBuildingsBuilt { get; set; }
/// <summary>
/// Дата начала первой игры
/// </summary>
public DateTime? FirstGameDate { get; set; }
/// <summary>
/// Дата последней игры
/// </summary>
public DateTime? LastGameDate { get; set; }
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MyBiz.Core\MyBiz.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MyBiz.Core\MyBiz.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MyBiz.Core\MyBiz.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using MyBiz.Core;
namespace MyBiz.Tests;
public class BuildingTests
{
[Fact]
public void Building_CalculateEfficiency_FullWorkers_ShouldBeMax()
{
// Arrange
var config = new BuildingTypeConfig
{
Id = "factory",
BaseEfficiency = 100,
WorkerSlots = 10
};
var building = new Building
{
TypeConfig = config,
Workers = 10,
Level = 1
};
// Act
building.CalculateEfficiency();
// Assert
Assert.Equal(100, building.CurrentEfficiency);
}
[Fact]
public void Building_CalculateEfficiency_NoWorkers_ShouldBeZero()
{
// Arrange
var config = new BuildingTypeConfig
{
Id = "factory",
BaseEfficiency = 100,
WorkerSlots = 10
};
var building = new Building
{
TypeConfig = config,
Workers = 0,
Level = 1
};
// Act
building.CalculateEfficiency();
// Assert
Assert.Equal(0, building.CurrentEfficiency);
}
[Fact]
public void Building_LevelBonus_ShouldIncreaseEfficiency()
{
// Arrange
var config = new BuildingTypeConfig
{
Id = "factory",
BaseEfficiency = 100,
WorkerSlots = 10
};
var building = new Building
{
TypeConfig = config,
Workers = 10,
Level = 5
};
// Act
building.CalculateEfficiency();
// Assert
Assert.Equal(120, building.CurrentEfficiency); // 100 + (4 * 5)
}
}

View File

@@ -0,0 +1,67 @@
using MyBiz.Core;
namespace MyBiz.Tests;
public class DefaultProductsTests
{
[Fact]
public void DefaultProducts_GetAll_ShouldReturn12Products()
{
// Act
var products = DefaultProducts.GetAll().ToList();
// Assert
Assert.Equal(12, products.Count);
}
[Fact]
public void DefaultProducts_ShouldHaveAllCategories()
{
// Act
var products = DefaultProducts.GetAll().ToList();
// Assert
Assert.Contains(products, p => p.Category == ProductCategory.RawMaterial);
Assert.Contains(products, p => p.Category == ProductCategory.Component);
Assert.Contains(products, p => p.Category == ProductCategory.ConsumerGoods);
}
[Fact]
public void DefaultProducts_RegisterAll_ShouldAddToRegistry()
{
// Arrange
var registry = new ProductRegistry();
// Act
DefaultProducts.RegisterAll(registry);
// Assert
Assert.Equal(12, registry.Count);
Assert.NotNull(registry.GetById("goods_food"));
Assert.NotNull(registry.GetById("goods_automobile"));
}
[Fact]
public void DefaultProducts_Automobile_ShouldHaveCorrectProperties()
{
// Act
var automobile = DefaultProducts.GetAll().First(p => p.Id == "goods_automobile");
// Assert
Assert.Equal("Автомобили", automobile.Name);
Assert.Equal(5000m, automobile.BasePrice);
Assert.Equal(1920, automobile.AvailableFromYear);
Assert.False(automobile.IsPerishable);
}
[Fact]
public void DefaultProducts_Food_ShouldBePerishable()
{
// Act
var food = DefaultProducts.GetAll().First(p => p.Id == "goods_food");
// Assert
Assert.True(food.IsPerishable);
Assert.Equal(5, food.ShelfLife);
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyBiz.Core\MyBiz.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,193 @@
using MyBiz.Core;
namespace MyBiz.Tests;
public class ProductTypeTests
{
[Fact]
public void ProductType_Creation_ShouldInitializeProperties()
{
// Arrange & Act
var productType = new ProductType
{
Id = "food_bread",
Name = "Bread",
Description = "Fresh baked bread",
Category = ProductCategory.ConsumerGoods,
BasePrice = 10m,
BaseDemand = 100,
AvailableFromYear = 1950
};
// Assert
Assert.Equal("food_bread", productType.Id);
Assert.Equal("Bread", productType.Name);
Assert.Equal(ProductCategory.ConsumerGoods, productType.Category);
Assert.Equal(10m, productType.BasePrice);
Assert.Equal(1950, productType.AvailableFromYear);
Assert.False(productType.IsPerishable);
}
[Fact]
public void ProductType_Clone_ShouldCreateIndependentCopy()
{
// Arrange
var original = new ProductType
{
Id = "test",
Name = "Test Product",
BasePrice = 50m
};
original.RequiredTechnologies.Add("tech1");
// Act
var clone = original.Clone();
clone.Name = "Modified";
clone.RequiredTechnologies.Add("tech2");
// Assert
Assert.Equal("Test Product", original.Name);
Assert.Equal("Modified", clone.Name);
Assert.Single(original.RequiredTechnologies);
Assert.Equal(2, clone.RequiredTechnologies.Count);
}
}
public class ProductTests
{
[Fact]
public void Product_Add_ShouldIncreaseQuantity()
{
// Arrange
var product = new Product
{
Type = new ProductType { Id = "test", BasePrice = 10m },
Quantity = 5
};
// Act
product.Add(10);
// Assert
Assert.Equal(15, product.Quantity);
}
[Fact]
public void Product_Remove_ShouldDecreaseQuantity()
{
// Arrange
var product = new Product
{
Type = new ProductType { Id = "test", BasePrice = 10m },
Quantity = 20
};
// Act
var removed = product.Remove(15);
// Assert
Assert.Equal(15, removed);
Assert.Equal(5, product.Quantity);
}
[Fact]
public void Product_Remove_MoreThanAvailable_ShouldRemoveAll()
{
// Arrange
var product = new Product
{
Type = new ProductType { Id = "test", BasePrice = 10m },
Quantity = 10
};
// Act
var removed = product.Remove(50);
// Assert
Assert.Equal(10, removed);
Assert.Equal(0, product.Quantity);
Assert.True(product.IsSpoiled);
}
[Fact]
public void Product_Perishable_ShouldSpoilAfterShelfLife()
{
// Arrange
var product = new Product
{
Type = new ProductType { Id = "food", BasePrice = 10m, ShelfLife = 5 },
Quantity = 10,
CreatedAtTick = 0
};
// Act
product.Update(6); // 6 ticks later
// Assert
Assert.True(product.IsSpoiled);
Assert.False(product.IsUsable);
}
}
public class ProductRegistryTests
{
[Fact]
public void Registry_Register_ShouldAddProductType()
{
// Arrange
var registry = new ProductRegistry();
var productType = new ProductType { Id = "test", Name = "Test" };
// Act
registry.Register(productType);
// Assert
Assert.Equal(1, registry.Count);
Assert.Same(productType, registry.GetById("test"));
}
[Fact]
public void Registry_GetById_UnknownId_ShouldReturnNull()
{
// Arrange
var registry = new ProductRegistry();
// Act
var result = registry.GetById("unknown");
// Assert
Assert.Null(result);
}
[Fact]
public void Registry_GetByCategory_ShouldFilterProducts()
{
// Arrange
var registry = new ProductRegistry();
registry.Register(new ProductType { Id = "raw1", Category = ProductCategory.RawMaterial });
registry.Register(new ProductType { Id = "raw2", Category = ProductCategory.RawMaterial });
registry.Register(new ProductType { Id = "consumer1", Category = ProductCategory.ConsumerGoods });
// Act
var rawMaterials = registry.GetByCategory(ProductCategory.RawMaterial);
// Assert
Assert.Equal(2, rawMaterials.Count());
}
[Fact]
public void Registry_GetAvailableInYear_ShouldFilterByYear()
{
// Arrange
var registry = new ProductRegistry();
registry.Register(new ProductType { Id = "old", AvailableFromYear = 1950 });
registry.Register(new ProductType { Id = "new", AvailableFromYear = 2000 });
// Act
var availableIn1970 = registry.GetAvailableInYear(1970);
// Assert
Assert.Single(availableIn1970);
Assert.Equal("old", availableIn1970.First().Id);
}
}

View File

@@ -0,0 +1,561 @@
using Xunit;
using MyBiz.Core;
namespace MyBiz.Tests;
public class UserTests
{
[Fact]
public void User_Creation_ShouldInitializeProperties()
{
var user = new User
{
Username = "testuser",
Email = "test@example.com"
};
Assert.NotNull(user.Id);
Assert.Equal("testuser", user.Username);
Assert.Equal("test@example.com", user.Email);
Assert.True(user.IsActive);
Assert.Null(user.Company);
Assert.NotNull(user.Stats);
}
[Fact]
public void User_Company_Assignment_ShouldWork()
{
var user = new User();
var company = new Company { Name = "Test Corp" };
user.Company = company;
user.CompanyId = company.Id;
Assert.NotNull(user.Company);
Assert.Equal(company.Id, user.CompanyId);
Assert.Equal("Test Corp", user.Company.Name);
}
[Fact]
public void UserStats_ShouldInitializeDefaults()
{
var stats = new UserStats();
Assert.Equal(0, stats.GamesPlayed);
Assert.Equal(0, stats.TotalHoursPlayed);
Assert.Equal(0, stats.MaxNetWorth);
Assert.Equal(0, stats.TotalBuildingsBuilt);
Assert.Null(stats.FirstGameDate);
Assert.Null(stats.LastGameDate);
}
}
public class AuthServiceTests
{
[Fact]
public void AuthService_Register_ShouldCreateUser()
{
var authService = new AuthService();
var result = authService.Register("testuser", "test@example.com", "password123", "Test Corp");
Assert.True(result.Success);
Assert.NotNull(result.User);
Assert.NotNull(result.Token);
Assert.Equal("testuser", result.User.Username);
Assert.NotNull(result.User.Company);
Assert.Equal("Test Corp", result.User.Company.Name);
}
[Fact]
public void AuthService_Register_DuplicateUsername_ShouldFail()
{
var authService = new AuthService();
authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var result = authService.Register("testuser", "other@example.com", "password456", "Other Corp");
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
Assert.Null(result.User);
}
[Fact]
public void AuthService_Login_CorrectCredentials_ShouldSucceed()
{
var authService = new AuthService();
authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var result = authService.Login("testuser", "password123");
Assert.True(result.Success);
Assert.NotNull(result.User);
Assert.NotNull(result.Token);
Assert.Equal("testuser", result.User.Username);
}
[Fact]
public void AuthService_Login_WrongPassword_ShouldFail()
{
var authService = new AuthService();
authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var result = authService.Login("testuser", "wrongpassword");
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
Assert.Null(result.User);
}
[Fact]
public void AuthService_Login_NonExistentUser_ShouldFail()
{
var authService = new AuthService();
var result = authService.Login("nonexistent", "password123");
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
Assert.Null(result.User);
}
[Fact]
public void AuthService_Logout_ShouldInvalidateToken()
{
var authService = new AuthService();
var registerResult = authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var token = registerResult.Token!;
authService.Logout(token);
var user = authService.ValidateToken(token);
Assert.Null(user);
}
[Fact]
public void AuthService_ValidateToken_ValidToken_ShouldReturnUser()
{
var authService = new AuthService();
var result = authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var user = authService.ValidateToken(result.Token!);
Assert.NotNull(user);
Assert.Equal("testuser", user.Username);
}
[Fact]
public void AuthService_GetUserById_ShouldReturnUser()
{
var authService = new AuthService();
var result = authService.Register("testuser", "test@example.com", "password123", "Test Corp");
var user = authService.GetUserById(result.User!.Id);
Assert.NotNull(user);
Assert.Equal(result.User.Id, user.Id);
}
}
public class BusinessUnitTests
{
[Fact]
public void BusinessUnit_Creation_ShouldInitializeProperties()
{
var unit = new BusinessUnit
{
Name = "Test Unit",
Type = BusinessUnitType.Shop,
CompanyId = Guid.NewGuid(),
CityId = "city_1",
X = 100,
Y = 200
};
Assert.NotNull(unit.Id);
Assert.Equal("Test Unit", unit.Name);
Assert.Equal(BusinessUnitType.Shop, unit.Type);
Assert.Equal(1, unit.Level);
Assert.Equal(100, unit.Efficiency);
Assert.True(unit.IsActive);
}
[Fact]
public void BusinessUnit_HireEmployee_ShouldIncreaseCount()
{
var unit = new BusinessUnit { MaxEmployees = 10 };
var hired = unit.HireEmployee();
Assert.True(hired);
Assert.Equal(1, unit.Employees);
}
[Fact]
public void BusinessUnit_HireEmployee_FullCapacity_ShouldFail()
{
var unit = new BusinessUnit { MaxEmployees = 1, Employees = 1 };
var hired = unit.HireEmployee();
Assert.False(hired);
Assert.Equal(1, unit.Employees);
}
[Fact]
public void BusinessUnit_FireEmployee_ShouldDecreaseCount()
{
var unit = new BusinessUnit { Employees = 5 };
var fired = unit.FireEmployee();
Assert.True(fired);
Assert.Equal(4, unit.Employees);
}
[Fact]
public void BusinessUnit_FireEmployee_NoEmployees_ShouldFail()
{
var unit = new BusinessUnit { Employees = 0 };
var fired = unit.FireEmployee();
Assert.False(fired);
Assert.Equal(0, unit.Employees);
}
[Fact]
public void BusinessUnit_Upgrade_ShouldIncreaseLevel()
{
var unit = new BusinessUnit { Level = 1, MaxLevel = 10 };
var upgraded = unit.Upgrade();
Assert.True(upgraded);
Assert.Equal(2, unit.Level);
}
[Fact]
public void BusinessUnit_Upgrade_MaxLevel_ShouldFail()
{
var unit = new BusinessUnit { Level = 10, MaxLevel = 10 };
var upgraded = unit.Upgrade();
Assert.False(upgraded);
Assert.Equal(10, unit.Level);
}
[Fact]
public void BusinessUnit_UpdateEfficiency_FullStaff_ShouldBeMax()
{
var unit = new BusinessUnit { MaxEmployees = 10, Employees = 10 };
unit.UpdateEfficiency();
Assert.Equal(100, unit.Efficiency);
}
[Fact]
public void BusinessUnit_UpdateEfficiency_HalfStaff_ShouldBeReduced()
{
var unit = new BusinessUnit { MaxEmployees = 10, Employees = 5 };
unit.UpdateEfficiency();
Assert.Equal(50, unit.Efficiency);
}
[Fact]
public void BusinessUnit_UpdateEfficiency_NoStaff_ShouldBeZero()
{
var unit = new BusinessUnit { MaxEmployees = 10, Employees = 0 };
unit.UpdateEfficiency();
Assert.Equal(0, unit.Efficiency);
}
}
public class ShopTests
{
[Fact]
public void Shop_Creation_ShouldInitializeDefaults()
{
var shop = new Shop();
Assert.Equal(BusinessUnitType.Shop, shop.Type);
Assert.Equal(20, shop.MaxEmployees);
Assert.Equal(50000m, shop.BuildCost);
Assert.Equal(1000m, shop.UpkeepCost);
Assert.NotNull(shop.SoldProductTypes);
Assert.Equal(0, shop.DailyRevenue);
Assert.Equal(0, shop.DailyCustomers);
}
[Fact]
public void Shop_AverageCheck_Calculation_ShouldBeCorrect()
{
var shop = new Shop { DailyRevenue = 1000m, DailyCustomers = 10 };
var avgCheck = shop.AverageCheck;
Assert.Equal(100m, avgCheck);
}
[Fact]
public void Shop_AverageCheck_NoCustomers_ShouldBeZero()
{
var shop = new Shop { DailyRevenue = 1000m, DailyCustomers = 0 };
var avgCheck = shop.AverageCheck;
Assert.Equal(0, avgCheck);
}
}
public class FactoryTests
{
[Fact]
public void Factory_Creation_ShouldInitializeDefaults()
{
var factory = new Factory();
Assert.Equal(BusinessUnitType.Factory, factory.Type);
Assert.Equal(100, factory.MaxEmployees);
Assert.Equal(200000m, factory.BuildCost);
Assert.Equal(5000m, factory.UpkeepCost);
Assert.Null(factory.OutputProductId);
Assert.NotNull(factory.RequiredInputs);
}
[Fact]
public void Factory_IsIdle_NoProduction_ShouldBeTrue()
{
var factory = new Factory();
Assert.True(factory.IsIdle);
}
[Fact]
public void Factory_IsIdle_ActiveProduction_ShouldBeFalse()
{
var factory = new Factory();
var config = new ProductionChainConfig
{
Id = "test_chain",
OutputProductId = "product_1",
RequiredBuildingId = "building_1"
};
factory.ActiveProduction = new ActiveProductionChain
{
Config = config,
Building = new Building(),
IsActive = true
};
Assert.False(factory.IsIdle);
}
}
public class WarehouseTests
{
[Fact]
public void Warehouse_Creation_ShouldInitializeDefaults()
{
var warehouse = new Warehouse();
Assert.Equal(BusinessUnitType.Warehouse, warehouse.Type);
Assert.Equal(10, warehouse.MaxEmployees);
Assert.Equal(30000m, warehouse.BuildCost);
Assert.Equal(500m, warehouse.UpkeepCost);
Assert.Equal(10000, warehouse.Capacity);
}
[Fact]
public void Warehouse_AddToInventory_ShouldIncreaseStock()
{
var warehouse = new Warehouse();
var added = warehouse.AddToInventory("product_1", 100);
Assert.True(added);
Assert.Equal(100, warehouse.Inventory["product_1"]);
Assert.Equal(100, warehouse.UsedCapacity);
}
[Fact]
public void Warehouse_AddToInventory_NoCapacity_ShouldFail()
{
var warehouse = new Warehouse { Capacity = 50 };
var added = warehouse.AddToInventory("product_1", 100);
Assert.False(added);
Assert.Empty(warehouse.Inventory);
}
[Fact]
public void Warehouse_RemoveFromInventory_ShouldDecreaseStock()
{
var warehouse = new Warehouse();
warehouse.AddToInventory("product_1", 100);
var removed = warehouse.RemoveFromInventory("product_1", 30);
Assert.Equal(30, removed);
Assert.Equal(70, warehouse.Inventory["product_1"]);
}
[Fact]
public void Warehouse_RemoveFromInventory_NoStock_ShouldReturnZero()
{
var warehouse = new Warehouse();
var removed = warehouse.RemoveFromInventory("product_1", 30);
Assert.Equal(0, removed);
}
[Fact]
public void Warehouse_FreeCapacity_Calculation_ShouldBeCorrect()
{
var warehouse = new Warehouse { Capacity = 1000 };
warehouse.AddToInventory("product_1", 300);
warehouse.AddToInventory("product_2", 200);
var freeCapacity = warehouse.FreeCapacity;
Assert.Equal(500, freeCapacity);
}
}
public class ResearchLabTests
{
[Fact]
public void ResearchLab_Creation_ShouldInitializeDefaults()
{
var lab = new ResearchLab();
Assert.Equal(BusinessUnitType.ResearchLab, lab.Type);
Assert.Equal(30, lab.MaxEmployees);
Assert.Equal(150000m, lab.BuildCost);
Assert.Equal(4000m, lab.UpkeepCost);
Assert.Null(lab.CurrentResearch);
Assert.Equal(0, lab.ResearchProgress);
}
[Fact]
public void ResearchLab_StartResearch_ShouldSetCurrentResearch()
{
var lab = new ResearchLab();
lab.StartResearch("tech_advanced_production");
Assert.Equal("tech_advanced_production", lab.CurrentResearch);
Assert.Equal(0, lab.ResearchProgress);
}
[Fact]
public void ResearchLab_SciencePerTick_Calculation_ShouldBeCorrect()
{
var lab = new ResearchLab { Employees = 10, Efficiency = 100 };
var science = lab.SciencePerTick;
Assert.Equal(100, science);
}
[Fact]
public void ResearchLab_Tick_ShouldIncreaseProgress()
{
var lab = new ResearchLab { Employees = 10, Efficiency = 100 };
lab.StartResearch("tech_advanced_production");
lab.Tick();
Assert.Equal(33, lab.ResearchProgress); // 10 * 10 * 100 / 100 = 100, но ограничено 33 за тик
}
}
public class CompanyBusinessUnitTests
{
[Fact]
public void Company_AddBusinessUnit_ShouldAddToList()
{
var company = new Company { Name = "Test Corp" };
var shop = new Shop { Name = "My Shop" };
company.AddBusinessUnit(shop);
Assert.Single(company.BusinessUnits);
Assert.Equal(company.Id, shop.CompanyId);
}
[Fact]
public void Company_RemoveBusinessUnit_ShouldRemoveFromList()
{
var company = new Company();
var shop = new Shop();
company.AddBusinessUnit(shop);
company.RemoveBusinessUnit(shop.Id);
Assert.Empty(company.BusinessUnits);
}
[Fact]
public void Company_Shops_Filter_ShouldReturnOnlyShops()
{
var company = new Company();
company.AddBusinessUnit(new Shop());
company.AddBusinessUnit(new Factory());
company.AddBusinessUnit(new Shop());
var shops = company.Shops.ToList();
Assert.Equal(2, shops.Count);
Assert.All(shops, s => Assert.IsType<Shop>(s));
}
[Fact]
public void Company_Factories_Filter_ShouldReturnOnlyFactories()
{
var company = new Company();
company.AddBusinessUnit(new Shop());
company.AddBusinessUnit(new Factory());
company.AddBusinessUnit(new Factory());
var factories = company.Factories.ToList();
Assert.Equal(2, factories.Count);
Assert.All(factories, f => Assert.IsType<Factory>(f));
}
[Fact]
public void Company_CalculateTotalProfit_ShouldSumAllUnits()
{
var company = new Company();
var shop1 = new Shop { LastPeriodIncome = 1000m, LastPeriodExpenses = 500m };
var shop2 = new Shop { LastPeriodIncome = 2000m, LastPeriodExpenses = 800m };
company.AddBusinessUnit(shop1);
company.AddBusinessUnit(shop2);
var totalProfit = company.CalculateTotalProfit();
Assert.Equal(1700m, totalProfit); // (1000-500) + (2000-800)
}
[Fact]
public void Company_Tick_ShouldUpdateAllUnits()
{
var company = new Company();
var shop = new Shop { Employees = 5, MaxEmployees = 10 };
company.AddBusinessUnit(shop);
company.Tick();
Assert.Equal(50, shop.Efficiency); // 5/10 = 50%
}
}

4
buffer.md Normal file
View File

@@ -0,0 +1,4 @@
## **sdfsdfsdfsdf**
Ghbdt

View File

@@ -75,6 +75,21 @@ project/
- [x] **Электроника** — бытовая техника, гаджеты, компьютеры - [x] **Электроника** — бытовая техника, гаджеты, компьютеры
- [x] **Авто** — автомобили, запчасти - [x] **Авто** — автомобили, запчасти
### 3.2.2. Цепочки производства (MVP)
**🍞 Еда:**
- Ферма → Пищекомбинат → Магазин
- Альтернатива: Закупка у местных поставщиков → Магазин
**👕 Одежда:**
- Хлопок → Ткань → Швейная фабрика → Магазин
**📱 Электроника:**
- Пластик/металл → Компоненты → Сборка → Магазин
**🚗 Авто:**
- Сталь → Детали → Автозавод → Автосалон
### 3.3. Система исследований ### 3.3. Система исследований
- Дерево технологий (tech tree) - Дерево технологий (tech tree)
@@ -107,13 +122,14 @@ project/
1. **Экономика:** 1. **Экономика:**
- Система спроса/предложения - Система спроса/предложения
- Динамическое ценообразование - Динамическое ценообразование (цены меняются в реальном времени)
- Цепочки производства - Цепочки производства
- Конкуренция между компаниями
2. **Бизнес:** 2. **Бизнес:**
- Покупка/строительство зданий - Покупка/строительство зданий
- Наём/управление персоналом - Наём/управление персоналом
- Закупка сырья - Закупка сырья (мировой рынок, местные поставщики, собственная добыча)
- Производство товаров - Производство товаров
- Продажа через магазины - Продажа через магазины
@@ -133,10 +149,10 @@ project/
### 4.2. Нефункциональные ### 4.2. Нефункциональные
- **Производительность:** [TBD] - **Производительность:** Без жёстких требований к FPS
- **Масштабируемость:** Поддержка расширения - **Масштабируемость:** Поддержка расширения
- **Моддинг:** API для модов - **Моддинг:** API для модов
- **Локализация:** [TBD] - **Локализация:** Русский + архитектура i18n
--- ---
@@ -144,10 +160,8 @@ project/
| Раздел | Вопрос | | Раздел | Вопрос |
|--------|--------| |--------|--------|
| Производство | Какие цепочки производства для каждого товара? | | Экономика | Детальный баланс цен, эластичность спроса? |
| Экономика | Баланс цен, спроса/предложения? | | Производительность | Максимальный размер карты (кол-во городов)? |
| Локализация | Какие ещё языки добавить в будущем? |
| Производительность | Целевые FPS, масштаб карты? |
--- ---

51
docs/tech/README.md Normal file
View File

@@ -0,0 +1,51 @@
# Техническая документация
Общая структура технической документации проекта MyBiz.
## 📚 Разделы документации
| Раздел | Описание | Файл |
|--------|----------|------|
| **Ядро** | Базовые классы и структуры данных | [core.md](core.md) |
| **Управление пользователем** | User, AuthService, аутентификация | [user-management.md](user-management.md) |
| **Бизнес-единицы** | Shop, Factory, Warehouse, Office, ResearchLab | [business-units.md](business-units.md) |
| **Продукты** | Система продуктов и товаров | [products.md](products.md) |
| **Здания** | Типы зданий и предприятия | [buildings.md](buildings.md) |
| **Производство** | Цепочки и процессы производства | [production.md](production.md) |
| **Экономика** | Экономическая модель | [economy.md](economy.md) ⬜ |
| **Торговля** | Рынки, спрос, предложение | [trade.md](trade.md) ⬜ |
| **Исследования** | Дерево технологий | [research.md](research.md) ⬜ |
| **Архитектура** | Общая архитектура системы | [architecture.md](architecture.md) ⬜ |
## 📁 Структура исходного кода
```
backend/
├── src/
│ ├── MyBiz.Core/ # Базовые модели ✅
│ │ ├── ProductType.cs # Типы продуктов
│ │ ├── Product.cs # Экземпляры продуктов
│ │ ├── ProductRegistry.cs # Реестр продуктов
│ │ ├── Building.cs # Здания
│ │ ├── City.cs # Города
│ │ ├── Company.cs # Компания
│ │ ├── ProductionChain.cs # Производственные цепочки
│ │ ├── DefaultProducts.cs # Дефолтные продукты
│ │ ├── User.cs # Пользователь ✅
│ │ ├── AuthService.cs # Аутентификация ✅
│ │ └── BusinessUnit.cs # Бизнес-единицы ✅
│ ├── MyBiz.Economy/ # Экономическая модель ⬜
│ ├── MyBiz.Production/ # Производство ⬜
│ └── MyBiz.Trade/ # Торговля ⬜
└── tests/
└── MyBiz.Tests/ # Тесты (61 passing) ✅
```
## 🔗 Ссылки
- [Техническое задание](../TZ.md)
- [Репозиторий проекта](https://git.six83.ru/ssa/my-biz.git)
---
**Последнее обновление:** 21.02.2026

196
docs/tech/buildings.md Normal file
View File

@@ -0,0 +1,196 @@
# Здания (Buildings)
Система зданий и предприятий в игре.
## Обзор
Здания — места, где происходит производство, торговля или хранение товаров.
📂 **Код:** [`backend/src/MyBiz.Core/Building.cs`](../../backend/src/MyBiz.Core/Building.cs)
---
## Архитектура
```
┌─────────────────────┐ ┌───────────┐
│ BuildingTypeConfig │ ──────► │ Building │
│ (тип здания) │ Type │ (здание) │
│ - Id │ Config │ - Level │
│ - Name │ │ - Workers │
│ - Category │ │ - Inventory│
│ - BuildCost │ │ - Efficiency│
└─────────────────────┘ └───────────┘
```
---
## BuildingTypeConfig
**Назначение:** Конфигурация типа здания для моддинга.
📂 **Исходный код:** [`Building.cs`](../../backend/src/MyBiz.Core/Building.cs#L10-L75)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | string | Уникальный идентификатор |
| `Name` | string | Отображаемое имя |
| `Description` | string | Описание |
| `Category` | BuildingCategory | Категория здания |
| `BuildCost` | decimal | Стоимость постройки |
| `UpkeepCost` | decimal | Содержание в тик |
| `StorageCapacity` | int | Вместимость склада |
| `WorkerSlots` | int | Количество рабочих мест |
| `BaseEfficiency` | int | Базовая эффективность (0-100) |
| `MaxLevel` | int | Максимальный уровень |
| `AvailableFromYear` | int | Год доступности |
| `RequiredTechnologies` | List<string> | Требуемые технологии |
| `OutputProducts` | List<string> | Производимые продукты |
| `InputProducts` | List<string> | Потребляемые продукты |
### Категории зданий
| Категория | Описание | Примеры |
|-----------|----------|---------|
| `RawMaterial` | Добыча сырья | Ферма, шахта |
| `Production` | Производство | Завод, фабрика |
| `Trade` | Торговля | Магазин, салон |
| `Research` | Исследования | Лаборатория |
| `Storage` | Склад | Складское помещение |
| `Office` | Офис | Административное здание |
---
## Building
**Назначение:** Экземпляр здания в игре.
📂 **Исходный код:** [`Building.cs`](../../backend/src/MyBiz.Core/Building.cs#L89-L165)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | Guid | Уникальный ID здания |
| `TypeConfig` | BuildingTypeConfig | Конфигурация типа |
| `Name` | string | Отображаемое имя (кастомизируемое) |
| `CityId` | string | ID города |
| `X`, `Y` | int | Координаты на карте |
| `Level` | int | Уровень здания |
| `Workers` | int | Количество рабочих |
| `CurrentEfficiency` | int | Текущая эффективность |
| `Inventory` | Dictionary<string, int> | Запасы на складе |
| `IsActive` | bool | Здание работает |
| `BuiltAtTick` | int | Тик постройки |
### Вычисляемые свойства
- `WorkerSlots` — из конфигурации типа
- `StorageCapacity` — из конфигурации типа
### Методы
| Метод | Описание |
|-------|----------|
| `CalculateEfficiency()` | Расчёт эффективности |
| `Update()` | Обновление состояния |
### Формула эффективности
```
Efficiency = BaseEfficiency + (Level - 1) * 5 - WorkerPenalty
WorkerPenalty = (WorkerSlots - Workers) * 100 / WorkerSlots
```
**Пример:**
- BaseEfficiency = 100
- Level = 3 → +10% бонус
- Workers = 8/10 → -20% штраф
- **Итого:** 100 + 10 - 20 = **90%**
---
## Примеры использования
### Создание типа здания
```csharp
var textileFactory = new BuildingTypeConfig
{
Id = "textile_factory",
Name = "Текстильная фабрика",
Category = BuildingCategory.Production,
BuildCost = 50000m,
UpkeepCost = 500m,
WorkerSlots = 50,
StorageCapacity = 1000,
BaseEfficiency = 100,
AvailableFromYear = 1900,
InputProducts = { "raw_cotton" },
OutputProducts = { "comp_fabric" }
};
```
### Создание здания
```csharp
var factory = new Building
{
TypeConfig = textileFactory,
Name = "Главная фабрика",
CityId = "city_1",
X = 100,
Y = 200,
Level = 1,
Workers = 0, // Пока нет рабочих
IsActive = true
};
factory.Workers = 40; // Наняли рабочих
factory.Update(); // Пересчитать эффективность
```
### Расчёт эффективности
```csharp
var building = new Building
{
TypeConfig = new BuildingTypeConfig
{
BaseEfficiency = 100,
WorkerSlots = 10
},
Level = 5,
Workers = 10
};
building.CalculateEfficiency();
// 100 + (5-1)*5 - 0 = 120%
```
---
## Тесты
📂 **Код:** [`backend/tests/MyBiz.Tests/BuildingTests.cs`](../../backend/tests/MyBiz.Tests/BuildingTests.cs)
| Тест | Описание |
|------|----------|
| `Building_CalculateEfficiency_FullWorkers_ShouldBeMax` | Полная укомплектованность |
| `Building_CalculateEfficiency_NoWorkers_ShouldBeZero` | Нет рабочих |
| `Building_LevelBonus_ShouldIncreaseEfficiency` | Бонус за уровень |
---
## Связанные документы
- [Ядро](core.md) — базовые классы
- [Производство](production.md) — использование зданий в производстве
- [Города](core.md#city) — размещение зданий в городах
---
**Последнее обновление:** 20.02.2026

387
docs/tech/business-units.md Normal file
View File

@@ -0,0 +1,387 @@
# Бизнес-единицы (Business Units)
Детальное описание типов бизнес-единиц компании.
## Обзор
Бизнес-единица — операционная единица компании (магазин, фабрика, склад и т.д.).
📂 **Код:** [`backend/src/MyBiz.Core/BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs)
---
## Архитектура
```
┌─────────────────┐
│ BusinessUnit │
│ (базовый) │
└────────┬────────┘
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Shop │ │ Factory │ │ Warehouse │
│ (магазин) │ │ (фабрика) │ │ (склад) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Office │ │ ResearchLab │
│ (офис) │ │ (лаборатория) │
└─────────────────┘ └─────────────────┘
```
---
## BusinessUnit (базовый класс)
**Назначение:** Базовый класс для всех бизнес-единиц.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L33-L180)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | Guid | Уникальный ID единицы |
| `Name` | string | Название |
| `Type` | BusinessUnitType | Тип единицы |
| `CompanyId` | Guid | ID компании-владельца |
| `CityId` | string | ID города |
| `X`, `Y` | int | Координаты на карте |
| `Level` | int | Уровень развития (1-10) |
| `Employees` | int | Количество сотрудников |
| `MaxEmployees` | int | Макс. количество сотрудников |
| `Efficiency` | int | Эффективность (0-100) |
| `BuildCost` | decimal | Стоимость постройки |
| `UpkeepCost` | decimal | Содержание в тик |
| `LastPeriodIncome` | decimal | Доход за период |
| `LastPeriodExpenses` | decimal | Расходы за период |
| `IsActive` | bool | Работает ли единица |
| `Building` | Building? | Ссылка на здание |
| `Inventory` | Dictionary<string, int> | Запасы |
| `ActiveProduction` | ActiveProductionChain? | Производственная цепочка |
### Вычисляемые свойства
| Свойство | Формула |
|----------|---------|
| `PeriodProfit` | `LastPeriodIncome - LastPeriodExpenses` |
| `Profitability` | `(PeriodProfit / LastPeriodExpenses) * 100` |
### Методы
| Метод | Описание |
|-------|----------|
| `UpdateEfficiency()` | Пересчитать эффективность |
| `Tick()` | Обновить состояние (каждый тик) |
| `HireEmployee()` | Нанять сотрудника |
| `FireEmployee()` | Уволить сотрудника |
| `Upgrade()` | Повысить уровень |
### Формула эффективности
```
staffRate = Employees / MaxEmployees
Efficiency = 100 * staffRate + (Level - 1) * 5
Efficiency = Clamp(Efficiency, 0, 100)
```
**Пример:**
- MaxEmployees = 10, Employees = 8 → 80%
- Level = 3 → +10%
- **Итого:** 90%
---
## Shop (магазин)
**Назначение:** Розничная торговля товарами.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L185-L210)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `SoldProductTypes` | List<string> | Типы продаваемых продуктов |
| `DailyRevenue` | decimal | Выручка за день |
| `DailyCustomers` | int | Количество клиентов |
### Вычисляемые
| Свойство | Формула |
|----------|---------|
| `AverageCheck` | `DailyRevenue / DailyCustomers` |
### Параметры по умолчанию
| Параметр | Значение |
|----------|----------|
| `MaxEmployees` | 20 |
| `BuildCost` | 50 000 |
| `UpkeepCost` | 1 000 |
---
## Factory (фабрика)
**Назначение:** Производство товаров из сырья/компонентов.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L215-L240)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `OutputProductId` | string? | ID производимого продукта |
| `RequiredInputs` | Dictionary<string, int> | Требуемые ресурсы |
| `LastTickOutput` | int | Произведено за тик |
### Вычисляемые
| Свойство | Описание |
|----------|----------|
| `IsIdle` | `ActiveProduction == null \|\| !ActiveProduction.IsActive` |
### Параметры по умолчанию
| Параметр | Значение |
|----------|----------|
| `MaxEmployees` | 100 |
| `BuildCost` | 200 000 |
| `UpkeepCost` | 5 000 |
---
## Warehouse (склад)
**Назначение:** Хранение товаров и сырья.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L245-L295)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Capacity` | int | Вместимость (единиц) |
| `UsedCapacity` | int | Использовано места |
| `FreeCapacity` | int | Свободно места |
### Методы
| Метод | Описание |
|-------|----------|
| `AddToInventory(productId, quantity)` | Добавить товар |
| `RemoveFromInventory(productId, quantity)` | Взять товар |
### Параметры по умолчанию
| Параметр | Значение |
|----------|----------|
| `MaxEmployees` | 10 |
| `BuildCost` | 30 000 |
| `UpkeepCost` | 500 |
| `Capacity` | 10 000 |
---
## Office (офис)
**Назначение:** Управление компанией, бонусы к эффективности.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L300-L325)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `ManagementBonus` | int | Бонус к управлению (%) |
| `ManagedBuildings` | int | Количество управляемых зданий |
### Параметры по умолчанию
| Параметр | Значение |
|----------|----------|
| `MaxEmployees` | 50 |
| `BuildCost` | 100 000 |
| `UpkeepCost` | 3 000 |
| `ManagementBonus` | 5 |
---
## ResearchLab (научная лаборатория)
**Назначение:** Исследование новых технологий.
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L330-L375)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `CurrentResearch` | string? | Текущее исследование |
| `ResearchProgress` | int | Прогресс (0-100%) |
### Вычисляемые
| Свойство | Формула |
|----------|---------|
| `SciencePerTick` | `Employees * 10 * Efficiency / 100` |
### Методы
| Метод | Описание |
|-------|----------|
| `StartResearch(technologyId)` | Начать исследование |
| `Tick()` | Обновление прогресса |
### Параметры по умолчанию
| Параметр | Значение |
|----------|----------|
| `MaxEmployees` | 30 |
| `BuildCost` | 150 000 |
| `UpkeepCost` | 4 000 |
---
## Перечисления
### BusinessUnitType
| Значение | Описание |
|----------|----------|
| `Shop` | Магазин (розничная торговля) |
| `Factory` | Фабрика (производство) |
| `Warehouse` | Склад (хранение) |
| `Office` | Офис (управление) |
| `ResearchLab` | Научная лаборатория |
📂 **Исходный код:** [`BusinessUnit.cs`](../../backend/src/MyBiz.Core/BusinessUnit.cs#L5-L22)
---
## Примеры использования
### Создание магазина
```csharp
var shop = new Shop
{
Name = "Центральный магазин",
CityId = "city_1",
X = 150,
Y = 200,
Employees = 10,
SoldProductTypes = { "goods_food", "goods_clothing" }
};
shop.HireEmployee(); // +1 сотрудник
shop.UpdateEfficiency();
```
### Создание фабрики с производством
```csharp
var factory = new Factory
{
Name = "Текстильная фабрика",
CityId = "city_1",
OutputProductId = "comp_fabric",
RequiredInputs = { ["raw_cotton"] = 50 },
Employees = 50
};
// Запуск производства
var config = new ProductionChainConfig
{
Id = "chain_fabric",
OutputProductId = "comp_fabric",
OutputQuantity = 40,
RequiredBuildingId = "textile_factory",
Steps = new List<ProductionStep>
{
new() { InputProductId = "raw_cotton", InputQuantity = 50, ProductionTime = 10 }
}
};
factory.ActiveProduction = new ActiveProductionChain
{
Config = config,
Building = new Building(),
IsActive = true
};
// Каждый тик:
factory.Tick();
```
### Управление складом
```csharp
var warehouse = new Warehouse { Capacity = 5000 };
// Добавить товары
warehouse.AddToInventory("raw_cotton", 1000);
warehouse.AddToInventory("raw_steel", 500);
Console.WriteLine($"Заполнено: {warehouse.UsedCapacity}/{warehouse.Capacity}");
// Взять товары
var taken = warehouse.RemoveFromInventory("raw_cotton", 300);
Console.WriteLine($"Взято: {taken}");
```
### Исследование технологий
```csharp
var lab = new ResearchLab
{
Employees = 20,
Efficiency = 100
};
lab.StartResearch("tech_advanced_production");
// Каждый тик:
lab.Tick();
if (lab.ResearchProgress >= 100)
{
Console.WriteLine("Технология изучена!");
// Добавить технологию в компанию
}
```
---
## Тесты
📂 **Код:** [`backend/tests/MyBiz.Tests/UserTests.cs`](../../backend/tests/MyBiz.Tests/UserTests.cs)
| Класс тестов | Количество | Описание |
|--------------|------------|----------|
| `BusinessUnitTests` | 10 | Базовый класс |
| `ShopTests` | 3 | Магазины |
| `FactoryTests` | 3 | Фабрики |
| `WarehouseTests` | 6 | Склады |
| `ResearchLabTests` | 4 | Лаборатории |
| `CompanyBusinessUnitTests` | 6 | Компания + единицы |
**Всего:** 32 теста
---
## Связанные документы
- [Ядро](core.md) — базовые классы
- [Управление пользователем](user-management.md) — User, Company
- [Производство](production.md) — цепочки производства
- [Здания](buildings.md) — типы зданий
---
**Последнее обновление:** 21.02.2026

165
docs/tech/core.md Normal file
View File

@@ -0,0 +1,165 @@
# Ядро игры (Core)
Базовые классы и структуры данных, используемые во всём проекте.
## Обзор
Модуль `MyBiz.Core` содержит фундаментальные классы для представления игровых сущностей.
📂 **Код:** [`backend/src/MyBiz.Core/`](../../backend/src/MyBiz.Core/)
---
## Классы
### ProductType
Конфигурация типа продукта. Используется для моддинга.
- **Назначение:** Определение свойств типа продукта (цена, спрос, категория)
- **Ключевые свойства:** `Id`, `Name`, `Category`, `BasePrice`, `ShelfLife`
- **Особенности:** Поддержка исторической доступности (год появления)
📂 **Исходный код:** [`ProductType.cs`](../../backend/src/MyBiz.Core/ProductType.cs)
---
### Product
Экземпляр продукта в игре.
- **Назначение:** Представление конкретного количества товара
- **Ключевые свойства:** `Type`, `Quantity`, `CurrentPrice`, `Quality`
- **Методы:** `Add()`, `Remove()`, `Update()` (проверка срока годности)
📂 **Исходный код:** [`Product.cs`](../../backend/src/MyBiz.Core/Product.cs)
---
### ProductRegistry
Централизованный реестр типов продуктов.
- **Назначение:** Хранение и управление конфигурациями продуктов
- **Возможности:** Регистрация, поиск, фильтрация по категории и году
- **События:** `ProductTypeAdded`, `ProductTypeModified` (для моддинга)
📂 **Исходный код:** [`ProductRegistry.cs`](../../backend/src/MyBiz.Core/ProductRegistry.cs)
---
### Building
Здание предприятия в игре.
- **Назначение:** Представление фабрик, магазинов, складов
- **Ключевые свойства:** `TypeConfig`, `Level`, `Workers`, `Efficiency`
- **Методы:** `CalculateEfficiency()`, `Update()`
📂 **Исходный код:** [`Building.cs`](../../backend/src/MyBiz.Core/Building.cs)
---
### BuildingTypeConfig
Конфигурация типа здания.
- **Назначение:** Определение свойств типа здания для моддинга
- **Ключевые свойства:** `Id`, `Name`, `Category`, `BuildCost`, `WorkerSlots`
- **Категории:** RawMaterial, Production, Trade, Research, Storage, Office
📂 **Исходный код:** [`Building.cs`](../../backend/src/MyBiz.Core/Building.cs#L10-L75)
---
### City
Город с рынком.
- **Назначение:** Представление локации с экономикой
- **Ключевые свойства:** `Name`, `Population`, `MarketDemand`, `MarketSupply`, `Prices`
- **Словари:** Спрос/предложение по типам продуктов, цены
📂 **Исходный код:** [`City.cs`](../../backend/src/MyBiz.Core/City.cs)
---
### Company
Компания игрока.
- **Назначение:** Состояние бизнеса игрока
- **Ключевые свойства:** `Cash`, `Assets`, `Liabilities`, `Buildings`, `Inventory`
- **Вычисляемое:** `NetWorth` (чистая стоимость)
📂 **Исходный код:** [`Company.cs`](../../backend/src/MyBiz.Core/Company.cs)
---
### ProductionChainConfig
Конфигурация производственной цепочки.
- **Назначение:** Определение рецепта производства
- **Ключевые свойства:** `OutputProductId`, `RequiredBuildingId`, `Steps`
- **Шаги:** Список `ProductionStep` с входными продуктами и временем
📂 **Исходный код:** [`ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs#L25-L68)
---
### ActiveProductionChain
Активный процесс производства на здании.
- **Назначение:** Отслеживание прогресса производства
- **Ключевые свойства:** `Config`, `Building`, `CurrentStep`, `Progress`
- **Методы:** `Tick()` (продвижение на 1 тик)
📂 **Исходный код:** [`ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs#L73-L125)
---
## Перечисления
### ProductCategory
Категории продуктов:
| Значение | Описание |
|----------|----------|
| `RawMaterial` | Сырьё (хлопок, сталь) |
| `Component` | Компоненты (ткань, детали) |
| `ConsumerGoods` | Товары народного потребления |
| `Luxury` | Предметы роскоши (будущее) |
📂 **Исходный код:** [`ProductType.cs`](../../backend/src/MyBiz.Core/ProductType.cs#L5-L11)
---
### BuildingCategory
Категории зданий:
| Значение | Описание |
|----------|----------|
| `RawMaterial` | Добыча сырья |
| `Production` | Производство |
| `Trade` | Торговля |
| `Research` | Исследования |
| `Storage` | Склад |
| `Office` | Офис |
📂 **Исходный код:** [`Building.cs`](../../backend/src/MyBiz.Core/Building.cs#L78-L86)
---
## Связанные документы
- [Продукты](products.md) — детальное описание системы продуктов
- [Здания](buildings.md) — типы предприятий
- [Производство](production.md) — цепочки производства
---
**Последнее обновление:** 20.02.2026

220
docs/tech/production.md Normal file
View File

@@ -0,0 +1,220 @@
# Производство (Production)
Система производственных цепочек и процессов.
## Обзор
Производство преобразует сырьё и компоненты в готовые товары через цепочки операций.
📂 **Код:** [`backend/src/MyBiz.Core/ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs)
---
## Архитектура
```
┌────────────────────────┐ ┌────────────────────────┐
│ ProductionChainConfig │ │ ActiveProductionChain │
│ (конфигурация) │ ──────► │ (активный процесс) │
│ - OutputProductId │ Config │ - CurrentStep │
│ - RequiredBuildingId │ │ - Progress │
│ - Steps[] │ │ - IsActive │
└────────────────────────┘ └────────────────────────┘
┌────────────────────────┐
│ Building │
│ (где происходит) │
└────────────────────────┘
```
---
## ProductionStep
**Назначение:** Один шаг производственного процесса.
📂 **Исходный код:** [`ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs#L5-L22)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `InputProductId` | string | ID требуемого продукта |
| `InputQuantity` | int | Количество |
| `ProductionTime` | int | Время выполнения в тиках |
---
## ProductionChainConfig
**Назначение:** Конфигурация производственной цепочки (рецепт).
📂 **Исходный код:** [`ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs#L25-L68)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | string | Уникальный идентификатор |
| `Name` | string | Название цепочки |
| `OutputProductId` | string | ID выходного продукта |
| `OutputQuantity` | int | Количество выходного продукта |
| `RequiredBuildingId` | string | ID требуемого здания |
| `Steps` | List<ProductionStep> | Шаги производства |
| `RequiredTechnologies` | List<string> | Требуемые технологии |
| `AvailableFromYear` | int | Год доступности |
---
## ActiveProductionChain
**Назначение:** Активный процесс производства на здании.
📂 **Исходный код:** [`ProductionChain.cs`](../../backend/src/MyBiz.Core/ProductionChain.cs#L73-L125)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Config` | ProductionChainConfig | Конфигурация |
| `Building` | Building | Здание, где происходит |
| `CurrentStep` | int | Текущий шаг (0-based) |
| `Progress` | int | Прогресс текущего шага (в тиках) |
| `IsActive` | bool | Процесс активен |
| `StartedAtTick` | int | Тик запуска |
### Вычисляемые свойства
- `IsStepComplete` — завершён ли текущий шаг
- `IsComplete` — завершено ли всё производство
### Методы
| Метод | Описание |
|-------|----------|
| `Tick()` | Продвинуть производство на 1 тик |
---
## Примеры производственных цепочек
### 🍞 Продукты питания
```
Сельхозпродукция (100) → [Пищекомбинат] → Продукты питания (80)
Время: 5 тиков
```
### 👕 Одежда
```
Шаг 1: Хлопок (50) → [Текстильная фабрика] → Ткань (40)
Время: 10 тиков
Шаг 2: Ткань (40) → [Швейная фабрика] → Одежда (30)
Время: 8 тиков
```
### 📱 Электроника
```
Шаг 1: Пластик (30) + Сталь (20) → [Завод компонентов] → Электронные компоненты (25)
Время: 15 тиков
Шаг 2: Электронные компоненты (25) + Пластик (15) → [Завод электроники] → Электроника (20)
Время: 20 тиков
```
### 🚗 Автомобили
```
Шаг 1: Сталь (100) → [Сталелитейный завод] → Металлоизделия (80)
Время: 20 тиков
Шаг 2: Металлоизделия (80) + Пластик (40) → [Автозавод] → Автомобили (5)
Время: 50 тиков
```
---
## Примеры использования
### Создание конфигурации цепочки
```csharp
var breadChain = new ProductionChainConfig
{
Id = "chain_bread",
Name = "Производство хлеба",
OutputProductId = "goods_food",
OutputQuantity = 80,
RequiredBuildingId = "food_factory",
Steps = new List<ProductionStep>
{
new ProductionStep
{
InputProductId = "raw_food",
InputQuantity = 100,
ProductionTime = 5
}
},
AvailableFromYear = 1900
};
```
### Запуск производства
```csharp
var factory = new Building
{
TypeConfig = foodFactoryConfig,
Workers = 50
};
var production = new ActiveProductionChain
{
Config = breadChain,
Building = factory,
IsActive = true,
StartedAtTick = 0
};
// Каждый тик:
production.Tick();
if (production.IsComplete)
{
// Производство завершено, забрать продукт
var output = production.Config.OutputProductId;
var quantity = production.Config.OutputQuantity;
}
```
### Отслеживание прогресса
```csharp
if (production.IsStepComplete)
{
// Шаг завершён, можно забирать промежуточный продукт
var step = production.Config.Steps[production.CurrentStep];
// ...
}
Console.WriteLine(
$"Прогресс: {production.CurrentStep + 1}/{production.Config.Steps.Count} " +
$"({production.Progress}/{production.Config.Steps[production.CurrentStep].ProductionTime} тиков)"
);
```
---
## Связанные документы
- [Продукты](products.md) — входные и выходные продукты
- [Здания](buildings.md) — где происходит производство
- [Ядро](core.md) — базовые классы
---
**Последнее обновление:** 20.02.2026

235
docs/tech/products.md Normal file
View File

@@ -0,0 +1,235 @@
# Продукты (Products)
Система продуктов и товаров в игре.
## Обзор
Продукты — основа экономической модели. Каждый продукт имеет тип (конфигурацию) и экземпляр (количество).
📂 **Код:** [`backend/src/MyBiz.Core/Product*.cs`](../../backend/src/MyBiz.Core/)
---
## Архитектура
```
┌─────────────────┐ ┌─────────────────┐
│ ProductType │ │ Product │
│ (конфигурация) │ ──────► │ (экземпляр) │
│ - Id │ Type │ - Quantity │
│ - Name │ │ - CurrentPrice │
│ - BasePrice │ │ - Quality │
│ - ShelfLife │ │ - IsSpoiled │
└─────────────────┘ └─────────────────┘
┌─────────────────┐
│ ProductRegistry │
│ (реестр типов) │
└─────────────────┘
```
---
## ProductType
**Назначение:** Конфигурация типа продукта для моддинга.
📂 **Исходный код:** [`ProductType.cs`](../../backend/src/MyBiz.Core/ProductType.cs)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | string | Уникальный идентификатор (для моддинга) |
| `Name` | string | Отображаемое имя |
| `Description` | string | Описание продукта |
| `Category` | ProductCategory | Категория (сырьё/компонент/товар) |
| `BasePrice` | decimal | Базовая цена |
| `BaseDemand` | int | Базовый спрос в тик |
| `DemandElasticity` | float | Эластичность спроса (0-1) |
| `ShelfLife` | int | Срок хранения в тиках (0 = бессрочно) |
| `StackSize` | int | Размер стека в инвентаре |
| `AvailableFromYear` | int | Год доступности (исторический режим) |
| `RequiredTechnologies` | List<string> | Требуемые технологии |
### Вычисляемые свойства
- `IsPerishable` — портится ли продукт (ShelfLife > 0)
### Методы
- `Clone()` — создание независимой копии
---
## Product
**Назначение:** Экземпляр продукта в инвентаре/на складе.
📂 **Исходный код:** [`Product.cs`](../../backend/src/MyBiz.Core/Product.cs)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | Guid | Уникальный ID экземпляра |
| `Type` | ProductType | Ссылка на конфигурацию типа |
| `Quantity` | int | Количество |
| `CurrentPrice` | decimal | Текущая цена за единицу |
| `CreatedAtTick` | int | Тик создания (для срока годности) |
| `Quality` | int | Качество (0-100) |
| `IsSpoiled` | bool | Испорчен ли |
### Вычисляемые свойства
- `TotalValue` — общая стоимость (Quantity × CurrentPrice)
- `IsUsable` — можно ли использовать (Quantity > 0 && !IsSpoiled)
### Методы
| Метод | Описание |
|-------|----------|
| `Add(int amount)` | Добавить количество |
| `Remove(int amount)` | Удалить количество (возвращает факт. удалённое) |
| `Update(int currentTick)` | Обновить состояние (проверка срока годности) |
---
## ProductRegistry
**Назначение:** Централизованный реестр типов продуктов.
📂 **Исходный код:** [`ProductRegistry.cs`](../../backend/src/MyBiz.Core/ProductRegistry.cs)
### Методы
| Метод | Описание |
|-------|----------|
| `Register(ProductType)` | Зарегистрировать тип |
| `GetById(string)` | Получить по ID |
| `GetOrThrow(string)` | Получить или выбросить исключение |
| `Exists(string)` | Проверка существования |
| `GetByCategory(ProductCategory)` | Фильтр по категории |
| `GetAvailableInYear(int)` | Фильтр по году доступности |
| `Remove(string)` | Удалить тип |
| `LoadFromConfig(string)` | Загрузить из JSON (TODO) |
| `ExportToJson()` | Экспорт в JSON (TODO) |
### События
- `ProductTypeAdded` — добавлен новый тип (для моддинга)
- `ProductTypeModified` — изменён тип
---
## Дефолтные продукты
**12 продуктов в 3 категориях:**
📂 **Исходный код:** [`DefaultProducts.cs`](../../backend/src/MyBiz.Core/DefaultProducts.cs)
### Сырьё (RawMaterial)
| ID | Название | Базовая цена | Портится | С года |
|----|----------|--------------|----------|---------|
| `raw_cotton` | Хлопок | 5 | ❌ | 1900 |
| `raw_steel` | Сталь | 15 | ❌ | 1900 |
| `raw_plastic` | Пластик | 8 | ❌ | 1950 |
| `raw_food` | Сельхозпродукция | 3 | ✅ (10 тиков) | 1900 |
### Компоненты (Component)
| ID | Название | Базовая цена | Портится | С года |
|----|----------|--------------|----------|---------|
| `comp_fabric` | Ткань | 12 | ❌ | 1900 |
| `comp_metal_parts` | Металлоизделия | 25 | ❌ | 1900 |
| `comp_plastic_parts` | Пластиковые детали | 18 | ❌ | 1950 |
| `comp_electronics` | Электронные компоненты | 50 | ❌ | 1960 |
### Товары (ConsumerGoods)
| ID | Название | Базовая цена | Портится | С года |
|----|----------|--------------|----------|---------|
| `goods_food` | Продукты питания | 8 | ✅ (5 тиков) | 1900 |
| `goods_clothing` | Одежда | 35 | ❌ | 1900 |
| `goods_electronics` | Электроника | 150 | ❌ | 1960 |
| `goods_automobile` | Автомобили | 5000 | ❌ | 1920 |
---
## Примеры использования
### Регистрация типа продукта
```csharp
var registry = new ProductRegistry();
var bread = new ProductType
{
Id = "food_bread",
Name = "Хлеб",
Category = ProductCategory.ConsumerGoods,
BasePrice = 10m,
ShelfLife = 5,
AvailableFromYear = 1900
};
registry.Register(bread);
```
### Создание экземпляра продукта
```csharp
var product = new Product
{
Type = registry.GetOrThrow("goods_food"),
Quantity = 100,
CurrentPrice = 8m,
CreatedAtTick = 0
};
product.Add(50); // +50 единиц
product.Remove(30); // -30 единиц
product.Update(10); // проверка через 10 тиков
```
### Фильтрация продуктов
```csharp
// Все сырьевые продукты
var rawMaterials = registry.GetByCategory(ProductCategory.RawMaterial);
// Доступные в 1950 году
var available1950 = registry.GetAvailableInYear(1950);
// Скоропортящиеся
var perishable = registry.AllProductTypes.Where(p => p.IsPerishable);
```
---
## Тесты
📂 **Код:** [`backend/tests/MyBiz.Tests/ProductTests.cs`](../../backend/tests/MyBiz.Tests/ProductTests.cs)
| Тест | Описание |
|------|----------|
| `ProductType_Creation_ShouldInitializeProperties` | Создание типа продукта |
| `ProductType_Clone_ShouldCreateIndependentCopy` | Клонирование типа |
| `Product_Add_ShouldIncreaseQuantity` | Добавление количества |
| `Product_Remove_ShouldDecreaseQuantity` | Удаление количества |
| `Product_Perishable_ShouldSpoilAfterShelfLife` | Порча продуктов |
| `Registry_*` | Тесты реестра |
---
## Связанные документы
- [Ядро](core.md) — базовые классы
- [Производство](production.md) — использование продуктов в цепочках
- [Торговля](trade.md) — рынки и цены
---
**Последнее обновление:** 20.02.2026

View File

@@ -0,0 +1,341 @@
# Управление пользователем (User Management)
Система пользователей, аутентификации и владения компанией.
## Обзор
Модуль управления пользователями включает регистрацию, аутентификацию и связь пользователя с компанией.
📂 **Код:** [`backend/src/MyBiz.Core/User.cs`](../../backend/src/MyBiz.Core/User.cs), [`AuthService.cs`](../../backend/src/MyBiz.Core/AuthService.cs)
---
## Архитектура
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ ──────► │ Company │ ──────► │ BusinessUnit│
│ (пользов.) │ OwnerId │ (компания) │ Units │ (единицы) │
│ │ │ │ │ │
│ - Username │ │ - Cash │ │ - Shop │
│ - Email │ │ - Assets │ │ - Factory │
│ - Password │ │ - Buildings │ │ - Warehouse │
│ Hash │ │ - Units │ │ - Office │
│ │ │ │ │ - Lab │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ AuthService │
│ (аутентиф.) │
└─────────────┘
```
---
## User
**Назначение:** Пользователь системы (игрок).
📂 **Исходный код:** [`User.cs`](../../backend/src/MyBiz.Core/User.cs)
### Основные свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Id` | Guid | Уникальный ID пользователя |
| `Username` | string | Имя пользователя (логин) |
| `Email` | string | Email адрес |
| `PasswordHash` | string | Хеш пароля |
| `CreatedAt` | DateTime | Дата регистрации |
| `LastLoginAt` | DateTime? | Дата последнего входа |
| `IsActive` | bool | Активен ли пользователь |
| `Company` | Company? | Компания пользователя |
| `CompanyId` | Guid? | ID компании |
| `Stats` | UserStats | Статистика игры |
---
## UserStats
**Назначение:** Статистика достижений пользователя.
📂 **Исходный код:** [`User.cs`](../../backend/src/MyBiz.Core/User.cs#L54-L85)
### Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `GamesPlayed` | int | Всего игр сыграно |
| `TotalHoursPlayed` | int | Всего часов в игре |
| `MaxNetWorth` | decimal | Максимальная чистая стоимость |
| `TotalBuildingsBuilt` | int | Всего зданий построено |
| `FirstGameDate` | DateTime? | Дата первой игры |
| `LastGameDate` | DateTime? | Дата последней игры |
---
## AuthService
**Назначение:** Сервис аутентификации (заглушка для MVP).
📂 **Исходный код:** [`AuthService.cs`](../../backend/src/MyBiz.Core/AuthService.cs)
### Методы
| Метод | Описание |
|-------|----------|
| `Register(username, email, password, companyName)` | Регистрация нового пользователя |
| `Login(username, password)` | Вход в систему |
| `Logout(token)` | Выход из системы |
| `ValidateToken(token)` | Проверка токена |
| `GetUserById(userId)` | Получить пользователя по ID |
| `GetUserByUsername(username)` | Получить пользователя по имени |
### Возвращаемые значения
**AuthResult:**
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Success` | bool | Успешна ли операция |
| `User` | User? | Пользователь (если успешно) |
| `Token` | string? | Токен доступа |
| `ErrorMessage` | string? | Сообщение об ошибке |
---
## Примеры использования
### Регистрация нового пользователя
```csharp
var authService = new AuthService();
var result = authService.Register(
username: "ivan_ivanov",
email: "ivan@example.com",
password: "SecurePassword123",
companyName: "Ivan Corp"
);
if (result.Success)
{
var user = result.User;
var token = result.Token;
Console.WriteLine($"Пользователь {user.Username} зарегистрирован!");
Console.WriteLine($"Компания: {user.Company.Name}");
Console.WriteLine($"Стартовый капитал: {user.Company.Cash}$");
}
else
{
Console.WriteLine($"Ошибка: {result.ErrorMessage}");
}
```
### Вход в систему
```csharp
var loginResult = authService.Login("ivan_ivanov", "SecurePassword123");
if (loginResult.Success)
{
var token = loginResult.Token;
// Использовать токен для авторизации запросов
}
else
{
Console.WriteLine($"Ошибка входа: {loginResult.ErrorMessage}");
}
```
### Проверка токена
```csharp
var user = authService.ValidateToken(token);
if (user != null)
{
Console.WriteLine($"Пользователь: {user.Username}");
Console.WriteLine($"Компания: {user.Company?.Name ?? "Нет компании"}");
}
else
{
Console.WriteLine("Неверный токен");
}
```
### Выход из системы
```csharp
authService.Logout(token);
```
---
## Company (обновлено)
**Назначение:** Компания игрока с бизнес-единицами.
📂 **Исходный код:** [`Company.cs`](../../backend/src/MyBiz.Core/Company.cs)
### Новые свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| `OwnerId` | Guid | ID владельца (пользователя) |
| `BusinessUnits` | List<BusinessUnit> | Бизнес-единицы компании |
| `FoundedAt` | DateTime | Дата основания |
### Новые методы
| Метод | Описание |
|-------|----------|
| `AddBusinessUnit(unit)` | Добавить бизнес-единицу |
| `RemoveBusinessUnit(unitId)` | Удалить бизнес-единицу |
| `Tick()` | Обновить все единицы (каждый тик) |
| `CalculateTotalProfit()` | Рассчитать общую прибыль |
| `CalculateAssets()` | Рассчитать стоимость активов |
### Свойства для фильтрации
| Свойство | Тип | Описание |
|----------|-----|----------|
| `Shops` | IEnumerable<Shop> | Все магазины |
| `Factories` | IEnumerable<Factory> | Все фабрики |
| `Warehouses` | IEnumerable<Warehouse> | Все склады |
---
## Примеры использования Company
### Добавление бизнес-единицы
```csharp
var company = new Company
{
Name = "My Corp",
Cash = 100000m
};
// Добавить магазин
var shop = new Shop
{
Name = "Главный магазин",
CityId = "city_1",
X = 100,
Y = 200
};
company.AddBusinessUnit(shop);
// Добавить фабрику
var factory = new Factory
{
Name = "Текстильная фабрика",
CityId = "city_1",
OutputProductId = "comp_fabric"
};
company.AddBusinessUnit(factory);
Console.WriteLine($"Магазинов: {company.Shops.Count()}");
Console.WriteLine($"Фабрик: {company.Factories.Count()}");
```
### Обновление состояния (тик)
```csharp
// Каждый тик игры:
company.Tick();
// Обновляются:
// - Эффективность единиц
// - Производственные цепочки
// - Статистика
```
### Расчёт прибыли
```csharp
var shop1 = new Shop
{
LastPeriodIncome = 10000m,
LastPeriodExpenses = 7000m
};
var shop2 = new Shop
{
LastPeriodIncome = 15000m,
LastPeriodExpenses = 10000m
};
company.AddBusinessUnit(shop1);
company.AddBusinessUnit(shop2);
var totalProfit = company.CalculateTotalProfit();
// (10000 - 7000) + (15000 - 10000) = 8000m
```
---
## Тесты
📂 **Код:** [`backend/tests/MyBiz.Tests/UserTests.cs`](../../backend/tests/MyBiz.Tests/UserTests.cs)
### UserTests
| Тест | Описание |
|------|----------|
| `User_Creation_ShouldInitializeProperties` | Создание пользователя |
| `User_Company_Assignment_ShouldWork` | Назначение компании |
| `UserStats_ShouldInitializeDefaults` | Инициализация статистики |
### AuthServiceTests
| Тест | Описание |
|------|----------|
| `AuthService_Register_ShouldCreateUser` | Регистрация пользователя |
| `AuthService_Register_DuplicateUsername_ShouldFail` | Дубликат имени |
| `AuthService_Login_CorrectCredentials_ShouldSucceed` | Успешный вход |
| `AuthService_Login_WrongPassword_ShouldFail` | Неверный пароль |
| `AuthService_Login_NonExistentUser_ShouldFail` | Несуществующий пользователь |
| `AuthService_Logout_ShouldInvalidateToken` | Выход из системы |
| `AuthService_ValidateToken_ValidToken_ShouldReturnUser` | Проверка токена |
| `AuthService_GetUserById_ShouldReturnUser` | Поиск по ID |
### BusinessUnitTests
| Тест | Описание |
|------|----------|
| `BusinessUnit_Creation_ShouldInitializeProperties` | Создание единицы |
| `BusinessUnit_HireEmployee_ShouldIncreaseCount` | Найм сотрудника |
| `BusinessUnit_FireEmployee_ShouldDecreaseCount` | Увольнение |
| `BusinessUnit_Upgrade_ShouldIncreaseLevel` | Повышение уровня |
| `BusinessUnit_UpdateEfficiency_*` | Расчёт эффективности |
### ShopTests, FactoryTests, WarehouseTests, ResearchLabTests
| Тест | Описание |
|------|----------|
| `*_Creation_ShouldInitializeDefaults` | Инициализация |
| `*_Calculation_ShouldBeCorrect` | Расчёты |
| `*_AddToInventory/*_RemoveFromInventory` | Операции с запасами |
### CompanyBusinessUnitTests
| Тест | Описание |
|------|----------|
| `Company_AddBusinessUnit_ShouldAddToList` | Добавление единицы |
| `Company_Shops_Filter_ShouldReturnOnlyShops` | Фильтрация магазинов |
| `Company_CalculateTotalProfit_ShouldSumAllUnits` | Расчёт прибыли |
| `Company_Tick_ShouldUpdateAllUnits` | Обновление состояния |
---
## Связанные документы
- [Ядро](core.md) — базовые классы
- [Бизнес-единицы](business-units.md) — детальное описание единиц
- [Здания](buildings.md) — типы зданий
---
**Последнее обновление:** 21.02.2026

39
frontend/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Frontend - Экономический симулятор
Фронтенд на Godot 4+ для экономического симулятора.
## Структура
```
frontend/
├── scenes/ # Godot сцены
│ ├── main/ # Главная сцена
│ ├── ui/ # UI компоненты
│ ├── map/ # Карта, города
│ └── buildings/ # Здания
├── scripts/ # C# скрипты
│ ├── core/ # Ядро
│ ├── economy/ # Экономика
│ ├── ui/ # UI логика
│ └── services/ # Сервисы (сеть, сохранения)
├── assets/ # Ресурсы
│ ├── sprites/ # 2D спрайты
│ ├── models/ # 3D модели (если нужно)
│ ├── audio/ # Звуки, музыка
│ └── fonts/ # Шрифты
└── resources/ # Godot ресурсы
├── data/ # Данные (товары, здания)
└── localization/ # Локализация
```
## Технологии
- Godot 4+
- C# (.NET 8)
- Изометрическая псевдо-3D графика
## Запуск
1. Открыть Godot
2. Указать путь к `frontend/`
3. Запустить проект

4
frontend/icon.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<rect x="10" y="10" width="108" height="108" fill="#4CAF50" rx="20"/>
<text x="64" y="80" font-family="Arial" font-size="60" fill="white" text-anchor="middle">$</text>
</svg>

After

Width:  |  Height:  |  Size: 245 B

37
frontend/icon.svg.import Normal file
View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csoebibtcgpn2"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

51
frontend/project.godot Normal file
View File

@@ -0,0 +1,51 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="MyBiz - Economic Simulator"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
[input]
move_up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
]
}
move_down={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
[layer_names]
2d_physics/layer_1="Default"
2d_physics/layer_2="Buildings"
2d_physics/layer_3="UI"

14
frontend/scenes/main.tscn Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<scene format="3" uid="1" namespaced="false" unique_name_in_tree="true">
<node name="Main" type="Node2D">
<node name="GameManager" instance="res://scripts/core/GameManager.cs" parent="." index="0"/>
<node name="UI" type="CanvasLayer" parent="." index="1">
<node name="TopBar" type="Panel" parent="UI">
<node name="HBoxContainer" type="HBoxContainer" parent="TopBar">
<node name="MoneyLabel" type="Label" parent="HBoxContainer"/>
<node name="DateLabel" type="Label" parent="HBoxContainer"/>
</node>
</node>
</node>
</node>
</scene>

View File

@@ -0,0 +1,13 @@
using Godot;
[GlobalClass]
public partial class GameData : Resource
{
[Export] public string CompanyName { get; set; } = "My Company";
[Export] public int StartYear { get; set; } = 1980;
[Export] public decimal StartingCash { get; set; } = 100000m;
// Настройки карты
[Export] public int CityCount { get; set; } = 10;
[Export] public bool EnableImports { get; set; } = true;
}

View File

@@ -0,0 +1,59 @@
using Godot;
/// <summary>
/// Менеджер игры - управляет состоянием игры
/// </summary>
public partial class GameManager : Node
{
public static GameManager Instance { get; private set; } = null!;
[Signal] public delegate void MoneyChangedEventHandler(decimal newAmount);
[Signal] public delegate void DateChangedEventHandler(int year, int month, int day);
public decimal Money { get; private set; } = 100000m;
public int GameYear { get; private set; } = 1980;
public int GameMonth { get; private set; } = 1;
public int GameDay { get; private set; } = 1;
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
}
public void AddMoney(decimal amount)
{
Money += amount;
EmitSignal(SignalName.MoneyChanged, Money);
}
public void SpendMoney(decimal amount)
{
if (Money >= amount)
{
Money -= amount;
EmitSignal(SignalName.MoneyChanged, Money);
}
}
public void AdvanceTime(int ticks)
{
GameDay += ticks;
if (GameDay > 30)
{
GameDay = 1;
GameMonth++;
if (GameMonth > 12)
{
GameMonth = 1;
GameYear++;
}
}
EmitSignal(SignalName.DateChanged, GameYear, GameMonth, GameDay);
}
}

View File

@@ -0,0 +1,13 @@
using Godot;
public partial class Main : Node2D
{
public override void _Ready()
{
GD.Print("MyBiz - Game Started!");
}
public override void _Process(double delta)
{
}
}

View File

@@ -0,0 +1,35 @@
using Godot;
/// <summary>
/// Базовый класс для всех UI элементов
/// </summary>
public partial class GameUI : Control
{
protected GameManager GameManager => GameManager.Instance;
public override void _Ready()
{
ConnectSignals();
}
protected virtual void ConnectSignals()
{
if (GameManager != null)
{
GameManager.MoneyChanged += OnMoneyChanged;
GameManager.DateChanged += OnDateChanged;
}
}
protected virtual void OnMoneyChanged(decimal newAmount) { }
protected virtual void OnDateChanged(int year, int month, int day) { }
public override void _ExitTree()
{
if (GameManager != null)
{
GameManager.MoneyChanged -= OnMoneyChanged;
GameManager.DateChanged -= OnDateChanged;
}
}
}

18
shared/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Shared - Общие компоненты
Общие модели, контракты и утилиты для бэкенда и фронтенда.
## Структура
```
shared/
├── models/ # Общие модели данных
├── contracts/ # Контракты API
└── utils/ # Утилиты
```
## Назначение
- Общие DTO для клиент-серверного взаимодействия
- Сериализация/десериализация
- Константы, перечисления