Compare commits
9 Commits
e119669f28
...
c0c0bf7d7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c0bf7d7e | ||
|
|
69baca4e65 | ||
|
|
be0c844672 | ||
|
|
c80afaaeaa | ||
|
|
ec3da03bba | ||
|
|
f320aa50ed | ||
|
|
fc3ad9f6db | ||
|
|
94003f67e5 | ||
|
|
67b1e394e5 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Git
|
||||
|
||||
.gitignore
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,8 +5,6 @@ export_presets.cfg
|
||||
|
||||
# Mono/Godot C#
|
||||
mono/
|
||||
*.csproj
|
||||
*.sln
|
||||
obj/
|
||||
bin/
|
||||
*.pidb
|
||||
|
||||
36
README.md
Normal file
36
README.md
Normal 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
11
backend/MyBiz.slnx
Normal 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
36
backend/README.md
Normal 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
|
||||
```
|
||||
176
backend/src/MyBiz.Core/AuthService.cs
Normal file
176
backend/src/MyBiz.Core/AuthService.cs
Normal 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
|
||||
}
|
||||
184
backend/src/MyBiz.Core/Building.cs
Normal file
184
backend/src/MyBiz.Core/Building.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
437
backend/src/MyBiz.Core/BusinessUnit.cs
Normal file
437
backend/src/MyBiz.Core/BusinessUnit.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
backend/src/MyBiz.Core/City.cs
Normal file
47
backend/src/MyBiz.Core/City.cs
Normal 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();
|
||||
}
|
||||
130
backend/src/MyBiz.Core/Company.cs
Normal file
130
backend/src/MyBiz.Core/Company.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
202
backend/src/MyBiz.Core/DefaultProducts.cs
Normal file
202
backend/src/MyBiz.Core/DefaultProducts.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
backend/src/MyBiz.Core/MyBiz.Core.csproj
Normal file
13
backend/src/MyBiz.Core/MyBiz.Core.csproj
Normal 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>
|
||||
104
backend/src/MyBiz.Core/Product.cs
Normal file
104
backend/src/MyBiz.Core/Product.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
backend/src/MyBiz.Core/ProductRegistry.cs
Normal file
116
backend/src/MyBiz.Core/ProductRegistry.cs
Normal 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 "{}";
|
||||
}
|
||||
}
|
||||
105
backend/src/MyBiz.Core/ProductType.cs
Normal file
105
backend/src/MyBiz.Core/ProductType.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
125
backend/src/MyBiz.Core/ProductionChain.cs
Normal file
125
backend/src/MyBiz.Core/ProductionChain.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
backend/src/MyBiz.Core/User.cs
Normal file
90
backend/src/MyBiz.Core/User.cs
Normal 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; }
|
||||
}
|
||||
13
backend/src/MyBiz.Economy/MyBiz.Economy.csproj
Normal file
13
backend/src/MyBiz.Economy/MyBiz.Economy.csproj
Normal 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>
|
||||
13
backend/src/MyBiz.Production/MyBiz.Production.csproj
Normal file
13
backend/src/MyBiz.Production/MyBiz.Production.csproj
Normal 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>
|
||||
13
backend/src/MyBiz.Trade/MyBiz.Trade.csproj
Normal file
13
backend/src/MyBiz.Trade/MyBiz.Trade.csproj
Normal 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>
|
||||
81
backend/tests/MyBiz.Tests/BuildingTests.cs
Normal file
81
backend/tests/MyBiz.Tests/BuildingTests.cs
Normal 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)
|
||||
}
|
||||
}
|
||||
67
backend/tests/MyBiz.Tests/DefaultProductsTests.cs
Normal file
67
backend/tests/MyBiz.Tests/DefaultProductsTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
28
backend/tests/MyBiz.Tests/MyBiz.Tests.csproj
Normal file
28
backend/tests/MyBiz.Tests/MyBiz.Tests.csproj
Normal 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>
|
||||
193
backend/tests/MyBiz.Tests/ProductTests.cs
Normal file
193
backend/tests/MyBiz.Tests/ProductTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
561
backend/tests/MyBiz.Tests/UserTests.cs
Normal file
561
backend/tests/MyBiz.Tests/UserTests.cs
Normal 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%
|
||||
}
|
||||
}
|
||||
30
docs/TZ.md
30
docs/TZ.md
@@ -75,6 +75,21 @@ project/
|
||||
- [x] **Электроника** — бытовая техника, гаджеты, компьютеры
|
||||
- [x] **Авто** — автомобили, запчасти
|
||||
|
||||
### 3.2.2. Цепочки производства (MVP)
|
||||
|
||||
**🍞 Еда:**
|
||||
- Ферма → Пищекомбинат → Магазин
|
||||
- Альтернатива: Закупка у местных поставщиков → Магазин
|
||||
|
||||
**👕 Одежда:**
|
||||
- Хлопок → Ткань → Швейная фабрика → Магазин
|
||||
|
||||
**📱 Электроника:**
|
||||
- Пластик/металл → Компоненты → Сборка → Магазин
|
||||
|
||||
**🚗 Авто:**
|
||||
- Сталь → Детали → Автозавод → Автосалон
|
||||
|
||||
### 3.3. Система исследований
|
||||
|
||||
- Дерево технологий (tech tree)
|
||||
@@ -107,13 +122,14 @@ project/
|
||||
|
||||
1. **Экономика:**
|
||||
- Система спроса/предложения
|
||||
- Динамическое ценообразование
|
||||
- Динамическое ценообразование (цены меняются в реальном времени)
|
||||
- Цепочки производства
|
||||
- Конкуренция между компаниями
|
||||
|
||||
2. **Бизнес:**
|
||||
- Покупка/строительство зданий
|
||||
- Наём/управление персоналом
|
||||
- Закупка сырья
|
||||
- Закупка сырья (мировой рынок, местные поставщики, собственная добыча)
|
||||
- Производство товаров
|
||||
- Продажа через магазины
|
||||
|
||||
@@ -133,10 +149,10 @@ project/
|
||||
|
||||
### 4.2. Нефункциональные
|
||||
|
||||
- **Производительность:** [TBD]
|
||||
- **Производительность:** Без жёстких требований к FPS
|
||||
- **Масштабируемость:** Поддержка расширения
|
||||
- **Моддинг:** API для модов
|
||||
- **Локализация:** [TBD]
|
||||
- **Локализация:** Русский + архитектура i18n
|
||||
|
||||
---
|
||||
|
||||
@@ -144,10 +160,8 @@ project/
|
||||
|
||||
| Раздел | Вопрос |
|
||||
|--------|--------|
|
||||
| Производство | Какие цепочки производства для каждого товара? |
|
||||
| Экономика | Баланс цен, спроса/предложения? |
|
||||
| Локализация | Какие ещё языки добавить в будущем? |
|
||||
| Производительность | Целевые FPS, масштаб карты? |
|
||||
| Экономика | Детальный баланс цен, эластичность спроса? |
|
||||
| Производительность | Максимальный размер карты (кол-во городов)? |
|
||||
|
||||
---
|
||||
|
||||
|
||||
51
docs/tech/README.md
Normal file
51
docs/tech/README.md
Normal 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
196
docs/tech/buildings.md
Normal 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
387
docs/tech/business-units.md
Normal 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
165
docs/tech/core.md
Normal 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
220
docs/tech/production.md
Normal 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
235
docs/tech/products.md
Normal 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
|
||||
341
docs/tech/user-management.md
Normal file
341
docs/tech/user-management.md
Normal 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
39
frontend/README.md
Normal 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
4
frontend/icon.svg
Normal 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
37
frontend/icon.svg.import
Normal 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
51
frontend/project.godot
Normal 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
14
frontend/scenes/main.tscn
Normal 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>
|
||||
13
frontend/scripts/core/GameData.cs
Normal file
13
frontend/scripts/core/GameData.cs
Normal 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;
|
||||
}
|
||||
59
frontend/scripts/core/GameManager.cs
Normal file
59
frontend/scripts/core/GameManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
13
frontend/scripts/core/Main.cs
Normal file
13
frontend/scripts/core/Main.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
35
frontend/scripts/ui/GameUI.cs
Normal file
35
frontend/scripts/ui/GameUI.cs
Normal 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
18
shared/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Shared - Общие компоненты
|
||||
|
||||
Общие модели, контракты и утилиты для бэкенда и фронтенда.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
shared/
|
||||
├── models/ # Общие модели данных
|
||||
├── contracts/ # Контракты API
|
||||
└── utils/ # Утилиты
|
||||
```
|
||||
|
||||
## Назначение
|
||||
|
||||
- Общие DTO для клиент-серверного взаимодействия
|
||||
- Сериализация/десериализация
|
||||
- Константы, перечисления
|
||||
Reference in New Issue
Block a user