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> 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 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 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); } }