170 lines
7.9 KiB
C#
170 lines
7.9 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...). Не утверждено
|
||
|
||
// Голод (регламент 31.10, ТЕСТ): добыча всех ресурсов, кроме еды, ×0.5. Еда -- без штрафа.
|
||
// Штраф снимается сразу, как только город снова сыт (модуль Населения сообщает IsStarving).
|
||
public double FamineNonFoodMultiplier = 0.5;
|
||
}
|
||
|
||
// МОДУЛЬ ЭКОНОМИКИ: добыча через рабочих (этап 3). Документ: Docs/Modules/Economy.md
|
||
// Без Unity: правила те же, что будут на сервере.
|
||
//
|
||
// Ресурс производят люди, здание даёт только места и эффективность:
|
||
// добыча в час = min(рабочие, места) × норма × эффективность уровня зданий,
|
||
// а при голоде всё, кроме еды, -- ещё × FamineNonFoodMultiplier (голод считает модуль Населения, IPopulation.IsStarving).
|
||
// Какие здания стоят в городе, сообщает слой приложения (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]);
|
||
double rate = working * rules.PerWorkerPerHour[j] * efficiency[j];
|
||
if (population.IsStarving && WorkJobs.Resource(job) != ResourceType.Food)
|
||
rate *= Math.Max(0, Math.Min(1, rules.FamineNonFoodMultiplier));
|
||
return rate;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|