2026-11-02 00:01:51 +03:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
// МОДУЛЬ ПОСЕЛЕНИЯ. Документ: Docs/Modules/Settlement.md
|
|
|
|
|
// Стройка или улучшение здания, которое ещё идёт (01.11, числа -- ТЕСТ).
|
|
|
|
|
// - Постройка (Kind.Build): здание стоит полупрозрачным и не работает -- его скрипты выключены
|
|
|
|
|
// (не добывает, не даёт жильё и места, Казармы не учат). Тап по нему -- окно стройки.
|
|
|
|
|
// - Улучшение (Kind.Upgrade): здание работает на старом уровне, новый уровень -- когда время выйдет.
|
|
|
|
|
// - Одновременно идёт не больше Builders строек (строители). Ратуша -- тоже стройка.
|
2026-11-04 23:36:14 +03:00
|
|
|
// - Время -- реальное (GameClock.UnixNowMs): идёт и пока игра закрыта, и при перемотке DEV.
|
|
|
|
|
// - Офлайн (04.11): метка конца хранится в сохранении; после входа стройка завершается в тот момент расчёта
|
|
|
|
|
// добычи, когда время дошло до её конца (App/ConstructionClock) -- добыча здания идёт с момента завершения.
|
2026-11-02 00:01:51 +03:00
|
|
|
// - Сохраняет модуль Поселения вместе со зданием (startMs, endMs, вид, целевой уровень).
|
|
|
|
|
// Сколько длится стройка и сколько строителей -- задаёт слой приложения (EconomyConfig): модуль чисел не знает.
|
|
|
|
|
public class BuildTimer : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
public enum Kind { Build = 1, Upgrade = 2 }
|
|
|
|
|
|
|
|
|
|
public static readonly List<BuildTimer> All = new List<BuildTimer>();
|
|
|
|
|
|
|
|
|
|
// Задаёт слой приложения (App/GameModules) из EconomyConfig
|
|
|
|
|
public static int Builders = 2;
|
2026-11-04 23:36:14 +03:00
|
|
|
// Можно ли завершить стройку, которая кончилась в этот момент (мс). Слой приложения отвечает "да", только когда
|
|
|
|
|
// расчёт добычи уже дошёл до этого момента (GameTimeSimulatedEvent): иначе после офлайна здание считалось бы
|
|
|
|
|
// готовым с самого начала офлайна. null -- завершать сразу.
|
|
|
|
|
public static Func<long, bool> MayComplete;
|
|
|
|
|
|
2026-11-02 00:01:51 +03:00
|
|
|
// Длительность в мс: (это Ратуша?, до какого уровня; 1 -- постройка). 0 -- мгновенно
|
|
|
|
|
public static Func<bool, int, long> DurationMs = (townHall, level) => 0;
|
|
|
|
|
public static int Busy => All.Count;
|
|
|
|
|
public static int FreeBuilders => Mathf.Max(0, Builders - Busy);
|
|
|
|
|
|
|
|
|
|
// Тап по зданию, которое строится (его собственные скрипты выключены) -- интерфейс откроет окно
|
|
|
|
|
public static event Action<BuildTimer> Tapped;
|
|
|
|
|
// Стройка закончилась: интерфейс пересчитает город и покажет сообщение
|
|
|
|
|
public static event Action<BuildTimer> Completed;
|
|
|
|
|
|
|
|
|
|
public Kind kind;
|
|
|
|
|
public long startMs;
|
|
|
|
|
public long endMs;
|
|
|
|
|
public int targetLevel = 1;
|
|
|
|
|
|
|
|
|
|
readonly List<MonoBehaviour> paused = new List<MonoBehaviour>();
|
|
|
|
|
readonly List<KeyValuePair<SpriteRenderer, Color>> tinted = new List<KeyValuePair<SpriteRenderer, Color>>();
|
|
|
|
|
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<BuildTimer>() : null;
|
|
|
|
|
|
|
|
|
|
public static bool IsConstructing(Component building)
|
|
|
|
|
{
|
|
|
|
|
var t = building != null ? building.GetComponent<BuildTimer>() : 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<BuildTimer>();
|
|
|
|
|
if (old != null && !old.finished) return null;
|
|
|
|
|
if (old != null) DestroyImmediate(old); // закончившийся, ещё не убранный Destroy
|
|
|
|
|
var t = building.AddComponent<BuildTimer>();
|
|
|
|
|
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<TownHall>();
|
|
|
|
|
if (townHall != null)
|
|
|
|
|
{
|
2026-11-04 23:36:14 +03:00
|
|
|
// уровень только растёт: повторное или устаревшее завершение не понизит и не "докупит" уровни
|
|
|
|
|
townHall.currentLevel = Mathf.Min(townHall.maxLevel, Mathf.Max(townHall.currentLevel, targetLevel));
|
2026-11-02 00:01:51 +03:00
|
|
|
EventBus.Publish(new TownHallLevelChangedEvent { Level = townHall.currentLevel });
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var leveled = GetComponent<ILeveledBuilding>();
|
2026-11-04 23:36:14 +03:00
|
|
|
if (leveled != null) leveled.Level = Mathf.Max(leveled.Level, targetLevel);
|
2026-11-02 00:01:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
{
|
2026-11-04 23:36:14 +03:00
|
|
|
if (!finished && GameClock.UnixNowMs >= endMs && (MayComplete == null || MayComplete(endMs))) Complete();
|
2026-11-02 00:01:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------- Здание, которое строится, не работает ----------------
|
|
|
|
|
|
|
|
|
|
void PauseBuilding()
|
|
|
|
|
{
|
|
|
|
|
foreach (var mb in GetComponents<MonoBehaviour>())
|
|
|
|
|
{
|
|
|
|
|
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<SpriteRenderer>(true))
|
|
|
|
|
{
|
|
|
|
|
tinted.Add(new KeyValuePair<SpriteRenderer, Color>(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);
|
|
|
|
|
}
|
|
|
|
|
}
|