- CrystalMine (без Unity) + 5 тестов: ровно 3 за 7 суток, ×12 не действует, 49/50 -> 1, пауза, перезагрузка и часы назад без двойного начисления, офлайн-стройка - Работает с конца стройки, офлайн досчитывается; вне недельного PvE-предела - Вторая шахта -- Ратуша 9 (ТЕСТ); окно здания: следующий кристалл / пауза - DEV «Ресурсы 100 000» -- обычные ресурсы мимо лимита склада Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
14 KiB
C#
264 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
// 3D-ПОСЕЛЕНИЕ: здания 3D-сцены становятся зданиями игры (05.11, Валдемар).
|
||
// На 3D-здание вешаются те же компоненты, что на префабы 2D-города: рабочее место или Дом (ResourceCollector),
|
||
// Склад / Амбар (StorageBuilding), Казармы, Госпиталь, Ратуша (TownHall) и PlacedBuildingId с именем здания.
|
||
// Поэтому дальше всё работает так же, как в 2D: CitySync даёт места, жильё и лимиты; окно здания (BuildingWindow) --
|
||
// рабочие, добыча, улучшение по таймеру; стройка идёт через BuildTimer (цена, строители, лимиты, ускорение, офлайн).
|
||
// Место на карте проверяет SettlementMap Саши, цену, время и лимиты -- этот код и EconomyConfig.
|
||
public static class City3DGameplay
|
||
{
|
||
// Имя типа 3D-сцены -> имя здания игры (как в 2D: по нему лимиты, окна, сохранение)
|
||
public static string GameName(string type3d)
|
||
{
|
||
switch (type3d)
|
||
{
|
||
case "Дом": return "Население";
|
||
case "Ферма": return "Еда";
|
||
case "Лесопилка": return "Лес";
|
||
case "Каменоломня": return "Камень";
|
||
case "Шахта": return "Металл";
|
||
case "Склад": return StorageBootstrap.WarehouseName;
|
||
case "Амбар": return StorageBootstrap.GranaryName;
|
||
case "Казармы": return BarracksBootstrap.BarracksName;
|
||
case "Госпиталь": return HospitalBootstrap.HospitalName;
|
||
case "Золотой рудник": return "Золото";
|
||
case "Кристалл": return "Кристалл";
|
||
case "Терем": return HeroCastleBootstrap.CastleName;
|
||
default: return type3d; // "Ратуша"
|
||
}
|
||
}
|
||
|
||
// Обратно: имя здания игры (как в 2D-сохранении) -> тип 3D-сцены. null -- такого здания в 3D нет
|
||
public static string Type3DFor(string gameName)
|
||
{
|
||
foreach (var t in AllTypes3D)
|
||
if (GameName(t) == gameName) return t;
|
||
return null;
|
||
}
|
||
|
||
static readonly string[] AllTypes3D =
|
||
{
|
||
"Ратуша", "Дом", "Ферма", "Лесопилка", "Каменоломня", "Шахта", "Склад", "Амбар", "Казармы", "Госпиталь",
|
||
"Золотой рудник", "Кристалл", "Терем",
|
||
};
|
||
|
||
public static bool IsTownHall(string type3d) => type3d == "Ратуша";
|
||
|
||
// Цена постройки -- та же, что у зданий 2D-города (Data_*.asset, Склады -- EconomyConfig, Госпиталь -- HospitalConfig)
|
||
public static ResourceAmount[] BuildCost(string type3d)
|
||
{
|
||
var e = EconomyConfig.Load().storage;
|
||
switch (type3d)
|
||
{
|
||
case "Дом": return Cost(ResourceType.Wood, 100, ResourceType.Food, 200, ResourceType.Stone, 150);
|
||
case "Ферма": return Cost(ResourceType.Wood, 100);
|
||
case "Лесопилка": return Cost();
|
||
case "Каменоломня": return Cost(ResourceType.Wood, 50);
|
||
case "Шахта": return Cost(ResourceType.Wood, 600, ResourceType.Stone, 550);
|
||
case "Склад": return Cost(ResourceType.Wood, e.warehouseWood, ResourceType.Stone, e.warehouseStone);
|
||
case "Амбар": return Cost(ResourceType.Wood, e.granaryWood, ResourceType.Stone, e.granaryStone);
|
||
case "Казармы": return Cost(ResourceType.Wood, 300, ResourceType.Stone, 200);
|
||
case "Госпиталь":
|
||
var h = HospitalConfig.Load();
|
||
return Cost(ResourceType.Wood, h.buildWood, ResourceType.Stone, h.buildStone);
|
||
// Перенесены из 2D-города (решение 05.11): цены -- из Data_GoldMine, Data_Kristall и HeroCastleBootstrap
|
||
case "Золотой рудник": return Cost(ResourceType.Stone, 50);
|
||
case "Кристалл": return Cost(ResourceType.Wood, 2000, ResourceType.Stone, 3200, ResourceType.Gold, 12000, ResourceType.Iron, 1600);
|
||
case "Терем": return Cost(ResourceType.Wood, HeroCastleBootstrap.CostWood, ResourceType.Stone, HeroCastleBootstrap.CostStone);
|
||
default: return Cost();
|
||
}
|
||
}
|
||
|
||
// С какого уровня Ратуши можно строить (как в 2D)
|
||
public static int RequiredTownHall(string type3d)
|
||
{
|
||
switch (type3d)
|
||
{
|
||
case "Шахта": return 3;
|
||
case "Золотой рудник": return 2;
|
||
case "Кристалл": return 6;
|
||
case "Госпиталь": return Mathf.Max(1, HospitalConfig.Load().requiredTownHallLevel);
|
||
default: return 1;
|
||
}
|
||
}
|
||
|
||
static ResourceAmount[] Cost(params object[] pairs)
|
||
{
|
||
var list = new List<ResourceAmount>();
|
||
for (int i = 0; i + 1 < pairs.Length; i += 2)
|
||
list.Add(new ResourceAmount((ResourceType)pairs[i], System.Convert.ToSingle(pairs[i + 1])));
|
||
return list.ToArray();
|
||
}
|
||
|
||
public static int TownHallLevel => GameServices.TryGet(out ISettlement s) ? s.TownHallLevel : 1;
|
||
|
||
// Можно ли строить: уровень Ратуши, лимит зданий, строители, ресурсы. null -- можно, иначе текст для игрока
|
||
public static string CanBuild(string type3d)
|
||
{
|
||
int th = TownHallLevel;
|
||
if (th < RequiredTownHall(type3d)) return $"Откроется на Ратуше {RequiredTownHall(type3d)} ур.";
|
||
string name = GameName(type3d);
|
||
int max = EconomyConfig.Load().BuildingLimitFor(name, th), have = CountBuilt(name);
|
||
if (max >= 0 && have >= max) return $"Лимит: {have} / {max} на Ратуше {th} ур.";
|
||
if (BuildTimer.DurationMs(false, 1) > 0 && BuildTimer.FreeBuilders == 0) return "Все строители заняты";
|
||
var cost = BuildCost(type3d);
|
||
if (cost.Length > 0 && (!GameServices.TryGet(out IEconomy economy) || !economy.CanAfford(cost))) return "Не хватает ресурсов: " + CostText(cost);
|
||
return null;
|
||
}
|
||
|
||
public static string CostText(ResourceAmount[] cost)
|
||
{
|
||
if (cost.Length == 0) return "бесплатно";
|
||
var parts = new List<string>();
|
||
foreach (var c in cost) parts.Add($"{c.Amount:0} {GameData.ResName(c.Type).ToLower()}");
|
||
return string.Join(", ", parts.ToArray());
|
||
}
|
||
|
||
// Кристальные шахты: с какого момента каждая добывает (мс; long.MinValue -- давно достроена; конец стройки -- если
|
||
// ещё строится или достроилась офлайн). Город открыт -- по сцене, иначе -- по сохранению settlement3d.
|
||
// null -- город ещё не загружен: считать рано (иначе офлайн-время шахты пропало бы)
|
||
public static List<long> CrystalMinesActiveFrom()
|
||
{
|
||
var list = new List<long>();
|
||
var city = City3DController.Instance;
|
||
if (city != null)
|
||
{
|
||
if (!city.StateApplied) return null;
|
||
foreach (var id in Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
|
||
{
|
||
if (id.buildingName != "Кристалл") continue;
|
||
var t = BuildTimer.Of(id.gameObject);
|
||
list.Add(t != null && BuildTimer.IsConstructing(id.transform) ? t.endMs : long.MinValue);
|
||
}
|
||
return list;
|
||
}
|
||
if (!City3DSave.Loaded) return null;
|
||
if (City3DSave.Cached != null)
|
||
foreach (var e in City3DSave.Cached.buildings)
|
||
{
|
||
if (e == null || e.type != "Кристалл") continue;
|
||
bool building = e.timerKind == (int)BuildTimer.Kind.Build && e.level <= 1 && e.timerEndMs > 0;
|
||
list.Add(building ? e.timerEndMs : long.MinValue);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
public static int CountBuilt(string gameName)
|
||
{
|
||
int n = 0;
|
||
foreach (var id in Object.FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
|
||
if (id.buildingName == gameName) n++;
|
||
return n;
|
||
}
|
||
|
||
// Новое здание поставлено на карту: списать цену и начать стройку по времени (BuildTimer)
|
||
public static void StartConstruction(GameObject view)
|
||
{
|
||
var tag = view.GetComponent<City3DBuildingTag>();
|
||
if (tag == null) return;
|
||
var cost = BuildCost(tag.type3d);
|
||
if (cost.Length > 0 && GameServices.TryGet(out IEconomy economy)) economy.TrySpend(cost);
|
||
long ms = BuildTimer.DurationMs(false, 1);
|
||
if (ms > 0)
|
||
{
|
||
long now = GameClock.UnixNowMs;
|
||
BuildTimer.Begin(view, BuildTimer.Kind.Build, 1, now, now + ms);
|
||
}
|
||
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
|
||
}
|
||
|
||
// Повесить на 3D-здание компоненты здания игры (как у префабов 2D). Без цены и таймера
|
||
public static void Attach(GameObject view, string type3d, int level)
|
||
{
|
||
var tag = view.GetComponent<City3DBuildingTag>() ?? view.AddComponent<City3DBuildingTag>();
|
||
tag.type3d = type3d;
|
||
|
||
if (IsTownHall(type3d))
|
||
{
|
||
var th = view.GetComponent<TownHall>() ?? view.AddComponent<TownHall>();
|
||
// Та же цена улучшения, что у Ратуши 2D-города (сцена MVP1): 250 дерева, 160 камня, 25 людей, ×1.5 за уровень
|
||
th.baseUpgradeCost = new[]
|
||
{
|
||
new ResourceCost { type = ResourceType.Wood, amount = 250 },
|
||
new ResourceCost { type = ResourceType.Stone, amount = 160 },
|
||
new ResourceCost { type = ResourceType.Population, amount = 25 },
|
||
};
|
||
th.costMultiplierPerLevel = 1.5f;
|
||
th.maxLevel = 10;
|
||
th.currentLevel = Mathf.Max(1, level);
|
||
return;
|
||
}
|
||
|
||
view.AddComponent<PlacedBuildingId>().buildingName = GameName(type3d);
|
||
switch (type3d)
|
||
{
|
||
case "Дом": Collector(view, ResourceType.Population, level); break;
|
||
case "Ферма": Collector(view, ResourceType.Food, level); break;
|
||
case "Лесопилка": Collector(view, ResourceType.Wood, level); break;
|
||
case "Каменоломня": Collector(view, ResourceType.Stone, level); break;
|
||
case "Шахта": Collector(view, ResourceType.Iron, level); break;
|
||
case "Склад": Storage(view, StorageBuilding.Kind.Warehouse, level); break;
|
||
case "Амбар": Storage(view, StorageBuilding.Kind.Granary, level); break;
|
||
case "Казармы": view.AddComponent<Barracks>().Level = Mathf.Max(1, level); break;
|
||
case "Госпиталь": view.AddComponent<Hospital>().Level = Mathf.Max(1, level); break;
|
||
case "Золотой рудник": Collector(view, ResourceType.Gold, level); break;
|
||
case "Кристалл": Collector(view, ResourceType.Gems, level); break; // рабочих нет, как в 2D
|
||
case "Терем": view.AddComponent<HeroCastle>(); break; // окно героя -- BuildingWindow
|
||
}
|
||
}
|
||
|
||
static void Collector(GameObject view, ResourceType type, int level)
|
||
{
|
||
var rc = view.AddComponent<ResourceCollector>();
|
||
rc.resourceType = type;
|
||
rc.Level = Mathf.Max(1, level);
|
||
}
|
||
|
||
static void Storage(GameObject view, StorageBuilding.Kind kind, int level)
|
||
{
|
||
var sb = view.AddComponent<StorageBuilding>();
|
||
sb.kind = kind;
|
||
sb.Level = Mathf.Max(1, level);
|
||
}
|
||
|
||
// Уровень здания (для сохранения и подписи)
|
||
public static int LevelOf(GameObject view)
|
||
{
|
||
var th = view.GetComponent<TownHall>();
|
||
if (th != null) return th.currentLevel;
|
||
var leveled = view.GetComponent<ILeveledBuilding>();
|
||
return leveled != null ? leveled.Level : 1;
|
||
}
|
||
|
||
// HUD 2D-города нарисован на холсте "в камере" (Screen Space - Camera). В 3D-сцене холст стоял бы на расстоянии
|
||
// от камеры, и холмы и деревья перекрывали бы кнопки. Ставим холст вплотную к камере -- он всегда поверх 3D.
|
||
public static void PutHudInFront(Camera cam)
|
||
{
|
||
if (cam == null) return;
|
||
foreach (var canvas in Object.FindObjectsByType<Canvas>(FindObjectsSortMode.None))
|
||
{
|
||
if (!canvas.isRootCanvas || canvas.renderMode != RenderMode.ScreenSpaceCamera) continue;
|
||
canvas.worldCamera = cam;
|
||
canvas.planeDistance = cam.nearClipPlane + 0.05f;
|
||
HideWorldSprites(canvas);
|
||
}
|
||
}
|
||
|
||
// В HUD есть картинка-спрайт мира (SpriteRenderer) -- плашка с мечом по центру нижней панели. На холсте у
|
||
// камеры она рисуется поверх всего HUD, даже поверх открытых окон, а сама картинка без прозрачности
|
||
// ("шахматка"). Решение 05.11 (Миша, Володя): в 3D-сцене её убираем, пока не будет новой картинки или кнопки.
|
||
static void HideWorldSprites(Canvas canvas)
|
||
{
|
||
foreach (var sr in canvas.GetComponentsInChildren<SpriteRenderer>(true))
|
||
sr.enabled = false;
|
||
}
|
||
|
||
// Награда за расчистку (решение 04.11): +10 дерева за дерево или +10 камня за валун, один раз, в пределах склада
|
||
public static void RewardClearing(bool tree)
|
||
{
|
||
if (GameServices.TryGet(out IEconomy economy)) economy.Add(tree ? ResourceType.Wood : ResourceType.Stone, 10);
|
||
}
|
||
}
|