Refactor products and buildings to support modding
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ export_presets.cfg
|
|||||||
mono/
|
mono/
|
||||||
*.csproj
|
*.csproj
|
||||||
*.sln
|
*.sln
|
||||||
|
*.slnx
|
||||||
obj/
|
obj/
|
||||||
bin/
|
bin/
|
||||||
*.pidb
|
*.pidb
|
||||||
|
|||||||
@@ -1,51 +1,29 @@
|
|||||||
namespace MyBiz.Core;
|
namespace MyBiz.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Тип здания
|
/// Конфигурация типа здания - для моддинга
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum BuildingType
|
public class BuildingTypeConfig
|
||||||
{
|
{
|
||||||
// Добыча сырья
|
/// <summary>
|
||||||
Farm, // Ферма
|
/// Уникальный идентификатор типа здания
|
||||||
CottonField, // Хлопковое поле
|
/// </summary>
|
||||||
Mine, // Шахта
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Производство
|
|
||||||
FoodFactory, // Пищекомбинат
|
|
||||||
TextileFactory, // Текстильная фабрика
|
|
||||||
SteelMill, // Сталелитейный завод
|
|
||||||
PlasticPlant, // Завод пластика
|
|
||||||
ElectronicsFactory, // Завод электроники
|
|
||||||
AutoFactory, // Автозавод
|
|
||||||
|
|
||||||
// Торговля
|
|
||||||
GroceryStore, // Продуктовый магазин
|
|
||||||
ClothingStore, // Магазин одежды
|
|
||||||
ElectronicsStore, // Магазин электроники
|
|
||||||
AutoDealer, // Автосалон
|
|
||||||
Mall, // Торговый центр
|
|
||||||
|
|
||||||
// Исследования
|
|
||||||
Laboratory, // Лаборатория
|
|
||||||
|
|
||||||
// Склад
|
|
||||||
Warehouse // Склад
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Здание
|
|
||||||
/// </summary>
|
|
||||||
public class Building
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
public BuildingType Type { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string CityId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Уровень здания (влияет на эффективность)
|
/// Отображаемое имя
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Level { get; set; } = 1;
|
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>
|
||||||
/// Стоимость постройки
|
/// Стоимость постройки
|
||||||
@@ -67,8 +45,140 @@ public class Building
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int WorkerSlots { get; set; }
|
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>
|
||||||
/// Заполненность рабочими
|
/// Заполненность рабочими
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Workers { get; set; }
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +1,104 @@
|
|||||||
namespace MyBiz.Core;
|
namespace MyBiz.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Категория продукта
|
/// Продукт - экземпляр товара в игре
|
||||||
/// </summary>
|
|
||||||
public enum ProductCategory
|
|
||||||
{
|
|
||||||
RawMaterial, // Сырьё
|
|
||||||
Component, // Компоненты
|
|
||||||
ConsumerGoods // Товары народного потребления
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Тип продукта
|
|
||||||
/// </summary>
|
|
||||||
public enum ProductType
|
|
||||||
{
|
|
||||||
// Сырьё
|
|
||||||
Cotton, // Хлопок
|
|
||||||
Steel, // Сталь
|
|
||||||
Plastic, // Пластик
|
|
||||||
FoodRaw, // Сырьё для еды
|
|
||||||
|
|
||||||
// Компоненты
|
|
||||||
Fabric, // Ткань
|
|
||||||
MetalParts, // Металлические детали
|
|
||||||
PlasticParts, // Пластиковые детали
|
|
||||||
ElectronicsComponents, // Электронные компоненты
|
|
||||||
|
|
||||||
// Товары
|
|
||||||
Food, // Еда
|
|
||||||
Clothing, // Одежда
|
|
||||||
Electronics, // Электроника
|
|
||||||
Automobile // Автомобили
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Продукт
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Product
|
public class Product
|
||||||
{
|
{
|
||||||
public ProductType Type { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public ProductCategory Category { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая цена продукта
|
/// Уникальный идентификатор экземпляра продукта
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal BasePrice { get; set; }
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Текущая цена (с учётом спроса/предложения)
|
/// Тип продукта (ссылка на конфигурацию)
|
||||||
|
/// </summary>
|
||||||
|
public ProductType Type { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Количество продукта
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текущая цена за единицу
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal CurrentPrice { get; set; }
|
public decimal CurrentPrice { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Доступное количество на рынке
|
/// Дата создания (для отслеживания срока годности)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int AvailableQuantity { get; set; }
|
public int CreatedAtTick { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Спрос на продукт
|
/// Качество продукта (0-100, 100 = идеальное)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Demand { get; set; }
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ namespace MyBiz.Core;
|
|||||||
public class ProductionStep
|
public class ProductionStep
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Требуемый продукт
|
/// ID требуемого продукта
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProductType InputProduct { get; set; }
|
public string InputProductId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Количество требуемого продукта
|
/// Количество требуемого продукта
|
||||||
@@ -22,16 +22,104 @@ public class ProductionStep
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Производственная цепочка
|
/// Конфигурация производственной цепочки - для моддинга
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProductionChain
|
public class ProductionChainConfig
|
||||||
{
|
{
|
||||||
public ProductType OutputProduct { get; set; }
|
/// <summary>
|
||||||
public BuildingType RequiredBuilding { get; set; }
|
/// Уникальный идентификатор цепочки
|
||||||
public List<ProductionStep> Steps { get; set; } = new();
|
/// </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>
|
||||||
/// Количество выходного продукта за цикл
|
/// Количество выходного продукта за цикл
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int OutputQuantity { get; set; }
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,40 +2,192 @@ using MyBiz.Core;
|
|||||||
|
|
||||||
namespace MyBiz.Tests;
|
namespace MyBiz.Tests;
|
||||||
|
|
||||||
public class ProductTests
|
public class ProductTypeTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Product_Creation_ShouldInitializeProperties()
|
public void ProductType_Creation_ShouldInitializeProperties()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var product = new Product
|
var productType = new ProductType
|
||||||
{
|
{
|
||||||
Type = ProductType.Food,
|
Id = "food_bread",
|
||||||
Name = "Bread",
|
Name = "Bread",
|
||||||
|
Description = "Fresh baked bread",
|
||||||
Category = ProductCategory.ConsumerGoods,
|
Category = ProductCategory.ConsumerGoods,
|
||||||
BasePrice = 10m
|
BasePrice = 10m,
|
||||||
|
BaseDemand = 100,
|
||||||
|
AvailableFromYear = 1950
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(ProductType.Food, product.Type);
|
Assert.Equal("food_bread", productType.Id);
|
||||||
Assert.Equal("Bread", product.Name);
|
Assert.Equal("Bread", productType.Name);
|
||||||
Assert.Equal(ProductCategory.ConsumerGoods, product.Category);
|
Assert.Equal(ProductCategory.ConsumerGoods, productType.Category);
|
||||||
Assert.Equal(10m, product.BasePrice);
|
Assert.Equal(10m, productType.BasePrice);
|
||||||
|
Assert.Equal(1950, productType.AvailableFromYear);
|
||||||
|
Assert.False(productType.IsPerishable);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Product_CurrentPrice_ShouldDefaultToBasePrice()
|
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
|
// Arrange
|
||||||
var product = new Product
|
var product = new Product
|
||||||
{
|
{
|
||||||
BasePrice = 25m
|
Type = new ProductType { Id = "test", BasePrice = 10m },
|
||||||
|
Quantity = 5
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
product.CurrentPrice = product.BasePrice;
|
product.Add(10);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(25m, product.CurrentPrice);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
@@ -12,7 +12,7 @@ config_version=5
|
|||||||
|
|
||||||
config/name="MyBiz - Economic Simulator"
|
config/name="MyBiz - Economic Simulator"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main.tscn"
|
||||||
config/features=PackedStringArray("4.0", "Forward Plus")
|
config/features=PackedStringArray("4.3", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
@@ -25,22 +25,22 @@ window/stretch/mode="canvas_items"
|
|||||||
|
|
||||||
move_up={
|
move_up={
|
||||||
"deadzone": 0.5,
|
"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,"echo":false,"script":null)
|
"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={
|
move_down={
|
||||||
"deadzone": 0.5,
|
"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,"echo":false,"script":null)
|
"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={
|
move_left={
|
||||||
"deadzone": 0.5,
|
"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,"echo":false,"script":null)
|
"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={
|
move_right={
|
||||||
"deadzone": 0.5,
|
"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,"echo":false,"script":null)
|
"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)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,3 @@ move_right={
|
|||||||
2d_physics/layer_1="Default"
|
2d_physics/layer_1="Default"
|
||||||
2d_physics/layer_2="Buildings"
|
2d_physics/layer_2="Buildings"
|
||||||
2d_physics/layer_3="UI"
|
2d_physics/layer_3="UI"
|
||||||
|
|
||||||
[rendering]
|
|
||||||
|
|
||||||
renderer/rendering_method="forward_plus"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user