Files
MVP1/Assets/Scripts/Modules/Economy/Rules/ProductionService.cs
T

162 lines
7.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
// Правила добычи -- только числа (заполняются из EconomyConfig в App/GameModules).
public sealed class ProductionRules
{
// Норма T1 в час на одного рабочего, по профессиям (порядок как в WorkJob)
public double[] PerWorkerPerHour = { 2.0, 2.0, 1.5, 1.0, 0.4 };
public int SlotsPerBuilding = 30; // мест в здании 1 уровня (Лесопилка T1 -- 30)
public int SlotsPerLevel = 10; // + мест за каждый уровень выше первого. Не утверждено
public double EfficiencyPerLevel = 0.1; // + к норме за каждый уровень выше первого (×1.1, ×1.2...). Не утверждено
}
// МОДУЛЬ ЭКОНОМИКИ: добыча через рабочих (этап 3). Документ: Docs/Modules/Economy.md
// Без Unity: правила те же, что будут на сервере.
//
// Ресурс производят люди, здание даёт только места и эффективность:
// добыча в час = min(рабочие, места) × норма × эффективность уровня зданий.
// Какие здания стоят в городе, сообщает слой приложения (App/CitySync) -- модуль их не ищет.
// Места запоминаются в сохранении: на карте мира и офлайн город не открыт, а добыча идёт.
//
// Produce вызывает модуль Населения в своём расчёте, поэтому в игре, офлайн и при перемотке правила одни.
// На склад идут только целые единицы, доли копятся здесь (склад хранит float).
public sealed class ProductionService : IProduction, ISaveParticipant
{
const double Epsilon = 1e-9;
readonly ProductionRules rules;
readonly int[] slots = new int[WorkJobs.Count];
readonly double[] efficiency = new double[WorkJobs.Count];
readonly double[] pending = new double[WorkJobs.Count]; // добыто, но ещё не отдано на склад (< 1)
public ProductionService(ProductionRules rules)
{
this.rules = rules ?? new ProductionRules();
ResetState();
}
public ProductionRules Rules => rules;
// ---------------- Здания города ----------------
// Здания-рабочие места: (профессия, уровень здания). Вызывает App/CitySync, когда город изменился.
public void SetBuildings(IEnumerable<KeyValuePair<WorkJob, int>> buildings)
{
var weighted = new double[WorkJobs.Count];
Array.Clear(slots, 0, slots.Length);
foreach (var b in buildings)
{
int j = (int)b.Key;
if (j < 0 || j >= WorkJobs.Count) continue;
int level = Math.Max(1, b.Value);
int s = BuildingSlots(level);
slots[j] += s;
weighted[j] += s * BuildingEfficiency(level);
}
for (int j = 0; j < WorkJobs.Count; j++)
efficiency[j] = slots[j] > 0 ? weighted[j] / slots[j] : 1.0;
}
public int BuildingSlots(int level) => rules.SlotsPerBuilding + rules.SlotsPerLevel * (Math.Max(1, level) - 1);
public double BuildingEfficiency(int level) => 1.0 + rules.EfficiencyPerLevel * (Math.Max(1, level) - 1);
// ---------------- Контракт IProduction ----------------
public int Slots(WorkJob job) => slots[(int)job];
public float Efficiency(WorkJob job) => (float)efficiency[(int)job];
public float RatePerHour(ResourceType type)
{
if (!WorkJobs.TryFromResource(type, out WorkJob job)) return 0f;
return (float)JobRate(job);
}
double JobRate(WorkJob job)
{
int j = (int)job;
if (!GameServices.TryGet(out IPopulation population)) return 0;
int working = Math.Min(population.GetWorkers(job), slots[j]);
return working * rules.PerWorkerPerHour[j] * efficiency[j];
}
public void Produce(double hours)
{
if (hours <= 0 || !GameServices.TryGet(out IEconomy economy)) return;
for (int j = 0; j < WorkJobs.Count; j++)
{
var job = (WorkJob)j;
pending[j] += JobRate(job) * hours;
int whole = (int)Math.Floor(pending[j] + Epsilon);
if (whole <= 0) continue;
pending[j] = Math.Max(0, pending[j] - whole);
economy.Add(WorkJobs.Resource(job), whole); // сверх лимита склада Экономика не прибавит
}
}
// ---------------- Сохранение (секция "production") ----------------
// slots=<места по профессиям> eff=<эффективность> pending=<доли>
public string SaveKey => "production";
public string Capture()
{
var sb = new StringBuilder();
sb.Append("slots=").Append(Join(slots)).Append('\n');
sb.Append("eff=").Append(Join(efficiency)).Append('\n');
sb.Append("pending=").Append(Join(pending)).Append('\n');
return sb.ToString();
}
public void Restore(string data)
{
ResetState();
if (string.IsNullOrEmpty(data)) return;
foreach (var raw in data.Split('\n'))
{
string line = raw.Trim();
if (line.StartsWith("slots=")) ParseInto(line.Substring(6), v => Math.Max(0, (int)v), slots);
else if (line.StartsWith("eff=")) ParseInto(line.Substring(4), v => Math.Max(0.1, v), efficiency);
else if (line.StartsWith("pending=")) ParseInto(line.Substring(8), v => Math.Max(0, Math.Min(0.999, v)), pending);
}
}
public void ResetState()
{
Array.Clear(slots, 0, slots.Length);
Array.Clear(pending, 0, pending.Length);
for (int j = 0; j < WorkJobs.Count; j++) efficiency[j] = 1.0;
}
static string Join(int[] a)
{
var parts = new string[a.Length];
for (int i = 0; i < a.Length; i++) parts[i] = a[i].ToString(CultureInfo.InvariantCulture);
return string.Join(",", parts);
}
static string Join(double[] a)
{
var parts = new string[a.Length];
for (int i = 0; i < a.Length; i++) parts[i] = a[i].ToString("R", CultureInfo.InvariantCulture);
return string.Join(",", parts);
}
static void ParseInto(string csv, Func<double, int> conv, int[] target)
{
string[] parts = csv.Split(',');
for (int i = 0; i < target.Length && i < parts.Length; i++)
if (double.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out double v)) target[i] = conv(v);
}
static void ParseInto(string csv, Func<double, double> conv, double[] target)
{
string[] parts = csv.Split(',');
for (int i = 0; i < target.Length && i < parts.Length; i++)
if (double.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out double v)) target[i] = conv(v);
}
}