using System; using System.Collections.Generic; using UnityEngine; // МОДУЛЬ ПОСЕЛЕНИЯ. Документ: Docs/Modules/Settlement.md // Стройка или улучшение здания, которое ещё идёт (01.11, числа -- ТЕСТ). // - Постройка (Kind.Build): здание стоит полупрозрачным и не работает -- его скрипты выключены // (не добывает, не даёт жильё и места, Казармы не учат). Тап по нему -- окно стройки. // - Улучшение (Kind.Upgrade): здание работает на старом уровне, новый уровень -- когда время выйдет. // - Одновременно идёт не больше Builders строек (строители). Ратуша -- тоже стройка. // - Время -- реальное (GameClock.UnixNowMs): идёт и пока игра закрыта, и при перемотке DEV. // - Офлайн (04.11): метка конца хранится в сохранении; после входа стройка завершается в тот момент расчёта // добычи, когда время дошло до её конца (App/ConstructionClock) -- добыча здания идёт с момента завершения. // - Сохраняет модуль Поселения вместе со зданием (startMs, endMs, вид, целевой уровень). // Сколько длится стройка и сколько строителей -- задаёт слой приложения (EconomyConfig): модуль чисел не знает. public class BuildTimer : MonoBehaviour { public enum Kind { Build = 1, Upgrade = 2 } public static readonly List All = new List(); // Задаёт слой приложения (App/GameModules) из EconomyConfig public static int Builders = 2; // Можно ли завершить стройку, которая кончилась в этот момент (мс). Слой приложения отвечает "да", только когда // расчёт добычи уже дошёл до этого момента (GameTimeSimulatedEvent): иначе после офлайна здание считалось бы // готовым с самого начала офлайна. null -- завершать сразу. public static Func MayComplete; // Длительность в мс: (это Ратуша?, до какого уровня; 1 -- постройка). 0 -- мгновенно public static Func DurationMs = (townHall, level) => 0; public static int Busy => All.Count; public static int FreeBuilders => Mathf.Max(0, Builders - Busy); // Тап по зданию, которое строится (его собственные скрипты выключены) -- интерфейс откроет окно public static event Action Tapped; // Стройка закончилась: интерфейс пересчитает город и покажет сообщение public static event Action Completed; public Kind kind; public long startMs; public long endMs; public int targetLevel = 1; readonly List paused = new List(); readonly List> tinted = new List>(); bool finished; Vector3 downPos; public bool IsConstruction => kind == Kind.Build; public double SecondsLeft => Math.Max(0, endMs - GameClock.UnixNowMs) / 1000.0; public float Progress => endMs <= startMs ? 1f : Mathf.Clamp01((float)(GameClock.UnixNowMs - startMs) / (endMs - startMs)); public static BuildTimer Of(GameObject building) => building != null ? building.GetComponent() : null; public static bool IsConstructing(Component building) { var t = building != null ? building.GetComponent() : null; return t != null && !t.finished && t.kind == Kind.Build; } // Начать стройку или улучшение. Цену платит тот, кто вызывает. null -- уже идёт стройка этого здания public static BuildTimer Begin(GameObject building, Kind kind, int targetLevel, long startMs, long endMs) { if (building == null) return null; var old = building.GetComponent(); if (old != null && !old.finished) return null; if (old != null) DestroyImmediate(old); // закончившийся, ещё не убранный Destroy var t = building.AddComponent(); t.kind = kind; t.targetLevel = Mathf.Max(1, targetLevel); t.startMs = startMs; t.endMs = Math.Max(startMs, endMs); if (kind == Kind.Build) t.PauseBuilding(); if (!All.Contains(t)) All.Add(t); return t; } // Закончить сейчас (ускорение за кристаллы или время вышло) public void Complete() { if (finished) return; finished = true; All.Remove(this); ResumeBuilding(); if (kind == Kind.Upgrade) { var townHall = GetComponent(); if (townHall != null) { // уровень только растёт: повторное или устаревшее завершение не понизит и не "докупит" уровни townHall.currentLevel = Mathf.Min(townHall.maxLevel, Mathf.Max(townHall.currentLevel, targetLevel)); EventBus.Publish(new TownHallLevelChangedEvent { Level = townHall.currentLevel }); } else { var leveled = GetComponent(); if (leveled != null) leveled.Level = Mathf.Max(leveled.Level, targetLevel); } } Completed?.Invoke(this); Destroy(this); } // Убрать без результата (загрузка другого сохранения, новая игра) public void Cancel() { finished = true; All.Remove(this); ResumeBuilding(); DestroyImmediate(this); } // Здание сносится вместе со стройкой: строитель свободен сразу, скрипты не включаем public void Discard() { finished = true; All.Remove(this); } void OnEnable() { if (!finished && !All.Contains(this)) All.Add(this); } void OnDestroy() { All.Remove(this); } void Update() { if (!finished && GameClock.UnixNowMs >= endMs && (MayComplete == null || MayComplete(endMs))) Complete(); } // ---------------- Здание, которое строится, не работает ---------------- void PauseBuilding() { foreach (var mb in GetComponents()) { if (mb == null || mb == this || !mb.enabled) continue; if (mb is PlacedBuildingId || mb is BuildingSlotLink) continue; mb.enabled = false; paused.Add(mb); } foreach (var sr in GetComponentsInChildren(true)) { tinted.Add(new KeyValuePair(sr, sr.color)); var c = sr.color; sr.color = new Color(c.r * 0.7f, c.g * 0.7f, c.b * 0.7f, c.a * 0.55f); } } void ResumeBuilding() { foreach (var mb in paused) if (mb != null) mb.enabled = true; paused.Clear(); foreach (var kv in tinted) if (kv.Key != null) kv.Key.color = kv.Value; tinted.Clear(); } // ---------------- Тап по зданию, которое строится ---------------- void OnMouseDown() { downPos = Input.mousePosition; } void OnMouseUpAsButton() { if (kind != Kind.Build || finished) return; // при улучшении окно открывает само здание if (UIBlocker.IsPointerOverUI()) return; if ((Input.mousePosition - downPos).magnitude > 25f) return; Tapped?.Invoke(this); } }