diff --git a/Assets/Scripts/City3D/City3DCamera.cs b/Assets/Scripts/City3D/City3DCamera.cs index d753b1b..7425fd0 100644 --- a/Assets/Scripts/City3D/City3DCamera.cs +++ b/Assets/Scripts/City3D/City3DCamera.cs @@ -24,9 +24,19 @@ public class City3DCamera : MonoBehaviour // Перемещение камеры пальцем можно выключить на время перетаскивания здания public bool AllowPan { get; set; } = true; + // Камера перспективная (решение 05.11): поселение объёмнее, есть глубина. Угол, высота и поле зрения -- ТЕСТ, + // подбираются в Unity и на телефоне. Ортографическую можно вернуть галочкой (тогда зум -- размер кадра, см. Apply) + public bool perspective = true; + public float fieldOfView = 40f; + void Awake() { cam = GetComponent(); + if (cam != null && perspective) + { + cam.orthographic = false; + cam.fieldOfView = fieldOfView; + } } public void FocusOn(Rect area) diff --git a/Assets/Scripts/City3D/City3DController.cs b/Assets/Scripts/City3D/City3DController.cs index c599a9d..f483cda 100644 --- a/Assets/Scripts/City3D/City3DController.cs +++ b/Assets/Scripts/City3D/City3DController.cs @@ -49,6 +49,10 @@ public class City3DController : MonoBehaviour, City3DSave.ILive new BuildingType { Name = "Амбар", W = 2, H = 3, Height = 1.8f, Color = new Color(0.8f, 0.65f, 0.3f) }, new BuildingType { Name = "Казармы", W = 3, H = 3, Height = 2.0f, Color = new Color(0.75f, 0.3f, 0.3f) }, new BuildingType { Name = "Госпиталь", W = 3, H = 3, Height = 1.8f, Color = new Color(0.85f, 0.95f, 1f) }, + // Из 2D-города (решение 05.11): золото, кристаллы, дом героя. Новые типы -- только в конец: сохранение хранит имя + new BuildingType { Name = "Золотой рудник", W = 2, H = 2, Height = 1.5f, Color = new Color(0.95f, 0.8f, 0.3f) }, + new BuildingType { Name = "Кристалл", W = 2, H = 2, Height = 1.2f, Color = new Color(0.5f, 0.8f, 0.95f) }, + new BuildingType { Name = "Терем", W = 3, H = 3, Height = 2.4f, Color = new Color(0.9f, 0.75f, 0.45f) }, }; enum Mode { View, Edit, Place, Move } @@ -186,12 +190,64 @@ public class City3DController : MonoBehaviour, City3DSave.ILive if (type == 0) { hasTownHall = true; townHallLevel = Mathf.Clamp(e.level, 1, 5); } } } - if (!hasTownHall) PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре + if (!hasTownHall) + { + PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре + if (d == null) MoveFrom2D(); // 3D-города в сохранении ещё нет, а 2D есть -- перенести постройки игрока + } UpdateGrass(); RefreshGround(); if (CitySync.Instance != null) CitySync.Instance.SyncNow(); } + // ---------------- Перенос 2D-города (решение 05.11: 2D-город больше не развиваем) ---------------- + // Один раз, когда в сохранении нет 3D-города: Ратуша получает уровень (и улучшение, которое идёт) из 2D, + // остальные здания встают на ближайшие свободные места от центра с теми же уровнем и стройкой. + // 2D-данные в файле не меняются. Здание, которому не нашлось места или типа, пишется в Console + void MoveFrom2D() + { + var old = SettlementModule.Saved2D; + if (old == null || (old.townHallLevel <= 1 && (old.placedBuildings == null || old.placedBuildings.Count == 0))) return; + + string hallId = null; + foreach (var pair in buildingType) if (pair.Value == 0) hallId = pair.Key; + var th = hallId != null ? buildingViews[hallId].GetComponent() : null; + if (th != null) + { + th.currentLevel = Mathf.Clamp(old.townHallLevel, 1, th.maxLevel); + ApplyTownHallLevel(th.currentLevel); // зоны -- до расстановки, чтобы здания поместились + if (old.townHallTimerEndMs > 0 && old.townHallTimerTargetLevel > th.currentLevel) + BuildTimer.Begin(th.gameObject, BuildTimer.Kind.Upgrade, old.townHallTimerTargetLevel, old.townHallTimerStartMs, old.townHallTimerEndMs); + } + + int c = City3DLayout.Size / 2, moved = 0, lost = 0; + foreach (var e in old.placedBuildings ?? new List()) + { + string type3d = City3DGameplay.Type3DFor(e.buildingName); + int type = type3d != null ? System.Array.FindIndex(Types, t => t.Name == type3d) : -1; + if (type <= 0) { Debug.LogWarning($"[3D] Перенос 2D-города: здания «{e.buildingName}» в 3D нет -- пропущено"); lost++; continue; } + var t = Types[type]; + if (!FindFreeSpot(t, c - t.W / 2, c - t.H / 2, out int x, out int y, out int rot) || !PlaceNew(type, x, y, rot)) + { + Debug.LogWarning($"[3D] Перенос 2D-города: для «{e.buildingName}» нет свободного места -- пропущено"); + lost++; + continue; + } + string id = "b_" + (nextBuildingId - 1); + var view = buildingViews[id]; + var leveled = view.GetComponent(); + if (leveled != null) leveled.Level = Mathf.Max(1, e.level); + City3DSave.ReadTimer(new City3DSave.Entry + { + level = Mathf.Max(1, e.level), timerKind = e.timerKind, timerStartMs = e.timerStartMs, + timerEndMs = e.timerEndMs, timerTargetLevel = e.timerTargetLevel, + }, view); + moved++; + } + Debug.Log($"[3D] Перенос 2D-города: Ратуша {old.townHallLevel} ур., зданий перенесено {moved}" + (lost > 0 ? $", пропущено {lost}" : "")); + if (moved > 0 || lost > 0) Say(lost == 0 ? $"Город перенесён в 3D: {moved} зданий" : $"Город перенесён в 3D: {moved} зданий, {lost} не поместились"); + } + // Кнопка "Перенести" в окне здания void MoveFromWindow(GameObject view) { @@ -425,7 +481,10 @@ public class City3DController : MonoBehaviour, City3DSave.ILive case "Госпиталь": return new Color(0.45f, 0.55f, 0.65f); case "Казармы": return new Color(0.4f, 0.15f, 0.12f); case "Каменоломня": - case "Шахта": return new Color(0.35f, 0.35f, 0.38f); + case "Шахта": + case "Золотой рудник": return new Color(0.35f, 0.35f, 0.38f); + case "Кристалл": return new Color(0.25f, 0.4f, 0.6f); + case "Терем": return new Color(0.75f, 0.2f, 0.15f); default: return Color.Lerp(Types[type].Color, new Color(0.3f, 0.15f, 0.1f), 0.55f); } } @@ -465,6 +524,19 @@ public class City3DController : MonoBehaviour, City3DSave.ILive case "Шахта": Unit(body, PrimitiveType.Cube, new Vector3(0, 0.2f, -0.505f), new Vector3(0.35f, 0.4f, 0.02f), new Color(0.1f, 0.1f, 0.12f)); break; + case "Золотой рудник": + Unit(body, PrimitiveType.Cube, new Vector3(0, 0.2f, -0.505f), new Vector3(0.35f, 0.4f, 0.02f), new Color(0.1f, 0.1f, 0.12f)); + Unit(body, PrimitiveType.Cube, new Vector3(0.32f, 0.08f, -0.62f), new Vector3(0.18f, 0.16f, 0.18f), new Color(1f, 0.82f, 0.2f)); // самородок у входа + break; + case "Кристалл": + var gem = Map3D.Shape(body, "Gem", Map3D.Pyramid, new Color(0.35f, 0.85f, 1f), Vector3.zero, Vector2.one * 0.45f, 0.7f, 45f).transform; + gem.localPosition = new Vector3(0, 1.0f, 0); + break; + case "Терем": + Unit(body, PrimitiveType.Cube, new Vector3(-0.3f, 0.75f, -0.3f), new Vector3(0.3f, 1.5f, 0.3f), walls); + var dome = Map3D.Shape(body, "Dome", Map3D.Pyramid, new Color(0.95f, 0.75f, 0.2f), Vector3.zero, Vector2.one * 0.42f, 0.45f, 45f).transform; + dome.localPosition = new Vector3(-0.3f, 1.5f, -0.3f); + break; case "Ферма": // грядки перед амбаром for (int i = 0; i < 3; i++) @@ -1008,10 +1080,11 @@ public class City3DController : MonoBehaviour, City3DSave.ILive // Слева, правее кнопок HUD: что построить -- открывается кнопкой "СТРОЙКА" общего HUD if (buildMenuOpen && (mode == Mode.View || mode == Mode.Edit)) { - float y = HudTop; + float y = HudTop, x = HudLeft; for (int i = 1; i < Types.Length; i++) { - if (Button(new Rect(HudLeft, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) { buildMenuOpen = false; StartPlacing(i); } + if (y + 40 > h - HudBottom) { y = HudTop; x += 206; } // не помещается до нижней панели -- следующий столбец + if (Button(new Rect(x, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) { buildMenuOpen = false; StartPlacing(i); } y += 44; } } diff --git a/Assets/Scripts/City3D/City3DGameplay.cs b/Assets/Scripts/City3D/City3DGameplay.cs index 12b22c6..e28bd89 100644 --- a/Assets/Scripts/City3D/City3DGameplay.cs +++ b/Assets/Scripts/City3D/City3DGameplay.cs @@ -23,10 +23,27 @@ public static class City3DGameplay 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) @@ -46,6 +63,10 @@ public static class City3DGameplay 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(); } } @@ -56,6 +77,8 @@ public static class City3DGameplay switch (type3d) { case "Шахта": return 3; + case "Золотой рудник": return 2; + case "Кристалл": return 6; case "Госпиталь": return Mathf.Max(1, HospitalConfig.Load().requiredTownHallLevel); default: return 1; } @@ -151,6 +174,9 @@ public static class City3DGameplay case "Амбар": Storage(view, StorageBuilding.Kind.Granary, level); break; case "Казармы": view.AddComponent().Level = Mathf.Max(1, level); break; case "Госпиталь": view.AddComponent().Level = Mathf.Max(1, level); break; + case "Золотой рудник": Collector(view, ResourceType.Gold, level); break; + case "Кристалл": Collector(view, ResourceType.Gems, level); break; // рабочих нет, как в 2D + case "Терем": view.AddComponent(); break; // окно героя -- BuildingWindow } } diff --git a/Assets/Scripts/Meta/Build/HeroCastle.cs b/Assets/Scripts/Meta/Build/HeroCastle.cs index 11251d4..1d7c11b 100644 --- a/Assets/Scripts/Meta/Build/HeroCastle.cs +++ b/Assets/Scripts/Meta/Build/HeroCastle.cs @@ -26,7 +26,8 @@ public class HeroCastle : MonoBehaviour void Start() { - if (Main == this) HeroWalker.Spawn(this); + // Герой-фигурка ходит по клеткам 2D-города (BuildGrid). В 3D-поселении сетки нет -- Терем работает без прогулки + if (Main == this && BuildGrid.Instance != null) HeroWalker.Spawn(this); } void OnDestroy() diff --git a/Assets/Scripts/Modules/Settlement/SettlementModule.cs b/Assets/Scripts/Modules/Settlement/SettlementModule.cs index 655f8ba..04a80e4 100644 --- a/Assets/Scripts/Modules/Settlement/SettlementModule.cs +++ b/Assets/Scripts/Modules/Settlement/SettlementModule.cs @@ -36,6 +36,10 @@ public static class SettlementModule } static SettlementSaveData cached; // последнее состояние города (из сцены или из файла) + + // 2D-город из сохранения -- только для чтения: по нему 3D-поселение один раз переносит здания игрока (05.11). + // null -- 2D-города в сохранении нет. Сами данные 2D не меняются и остаются в файле + public static SettlementSaveData Saved2D => cached; static BuildManager appliedToBuildManager; // BuildManager сцены, для которой город уже восстановлен static readonly Service service = new Service(); diff --git a/Assets/Scripts/WorldMap/SceneNavigator.cs b/Assets/Scripts/WorldMap/SceneNavigator.cs index c8d9c11..472ea26 100644 --- a/Assets/Scripts/WorldMap/SceneNavigator.cs +++ b/Assets/Scripts/WorldMap/SceneNavigator.cs @@ -4,7 +4,10 @@ using UnityEngine.SceneManagement; // Переходы город <-> карта мира public static class SceneNavigator { - public const string CitySceneName = "MVP1"; + // Город -- 3D-поселение (решение 05.11). Старый 2D-город (MVP1) исключён из сборки; если его открыть в редакторе, + // с карты мира вернёт в него же (lastCityScene) + public const string CitySceneName = "City3D"; + public const string OldCitySceneName = "MVP1"; public const string WorldMapSceneName = "WorldMap"; // Из какого города ушли на карту мира: 2D (MVP1) или 3D (City3D) -- туда и возвращаемся (05.11) @@ -30,7 +33,8 @@ public static class SceneNavigator public static void ReturnToCity() { if (SaveManager.Instance != null) SaveManager.Instance.SaveGame(); - string city = Application.CanStreamedLevelBeLoaded(lastCityScene) ? lastCityScene : CitySceneName; + string city = Application.CanStreamedLevelBeLoaded(lastCityScene) ? lastCityScene + : Application.CanStreamedLevelBeLoaded(CitySceneName) ? CitySceneName : OldCitySceneName; SceneManager.LoadScene(city); } } diff --git a/Assets/Scripts/WorldMap/WorldMapController.cs b/Assets/Scripts/WorldMap/WorldMapController.cs index 5f9d0d6..2c93a98 100644 --- a/Assets/Scripts/WorldMap/WorldMapController.cs +++ b/Assets/Scripts/WorldMap/WorldMapController.cs @@ -121,7 +121,7 @@ public class WorldMapController : MonoBehaviour hintText.text = DefaultHint; if (ResourceManager.Instance == null) - hintText.text = "Запусти игру из сцены города (MVP1), иначе войско неоткуда брать"; + hintText.text = "Запусти игру из сцены города (City3D), иначе войско неоткуда брать"; } void OnEnable() diff --git a/Docs/Modules/Settlement.md b/Docs/Modules/Settlement.md index 645c412..b10a480 100644 --- a/Docs/Modules/Settlement.md +++ b/Docs/Modules/Settlement.md @@ -119,6 +119,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 05.11.2026 | Claude (Саша) | **Решение команды: только 3D-поселение, камера перспективная.** (1) В 3D перенесены Золотой рудник (золото, Ратуша 2, 50 камня), Кристалл (Ратуша 6, цена из `Data_Kristall`) и Терем (`HeroCastle`, окно героя; фигурка героя гуляет только по 2D-сетке, в 3D — нет). (2) Перенос 2D-города: если в сохранении нет `settlement3d`, а есть 2D-город, при первом входе в 3D Ратуша получает уровень и улучшение из 2D, остальные здания встают на ближайшие свободные места с теми же уровнями и стройками (`City3DController.MoveFrom2D`, итог — в Console «[3D] Перенос 2D-города»). Секция `settlement` в файле не меняется. (3) `SceneNavigator.CitySceneName` = City3D; MVP1 выключена в Build Settings (сцена и код 2D остаются в репозитории до конца проверки). (4) `City3DCamera.perspective` (по умолчанию да, поле зрения 40°) — угол, высота и поле зрения ТЕСТ. Меню стройки — в два столбца. | | 05.11.2026 | Claude (Саша) | 3D: камера сцены City3D ортографическая — у неё расстояние не меняет размер города, поэтому «+ / −» и щипок не приближали. `City3DCamera.Apply` для ортографической камеры ставит `orthographicSize` по расстоянию (как видно перспективной камерой с этого расстояния). | | 05.11.2026 | Claude (Саша) | 3D: дымка идёт за камерой (`City3DCamera.Apply`: начало и конец — от расстояния камеры), поэтому при отдалении город больше не уходит в серую дымку. Окно здания: кнопка «Улучшить» (Ратуша и уровни зданий) сама становится зелёной, когда ресурсов стало хватать (`BuildingWindow.LiveAfford`) — не нужно закрывать и открывать окно. | | 05.11.2026 | Claude (Саша) | 3D: здание, которое ещё строится, теперь видно — оно серее, над ним «Стройка 0:45» (`BuildTimer` затемнял только 2D-спрайты, поэтому Казармы выглядели готовыми, а «Войска» писали «казарм: 0»). Окно «Войска»: пока Казармы строятся — «Казармы строятся, осталось …», после стройки окно перерисовывается само; «Построить Казармы» в 3D ставит призрак Казарм (`City3DController.PlaceBuilding`). Кнопки «+ / −» справа — приближение камеры (в Device Simulator колесо мыши не доходит до игры). | diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index 877b17e..50d65e8 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -8,7 +8,7 @@ EditorBuildSettings: - enabled: 1 path: Assets/Scenes/City3D.unity guid: c1c34719f0605c247a3575d077f3d656 - - enabled: 1 + - enabled: 0 path: Assets/Scenes/MVP1.unity guid: beae4025d64976c4f93af3b9233183da - enabled: 1