3D: общий HUD, здания игры в 3D (окна, рабочие, улучшения, стройка по таймеру, сохранение), окна зданий с прокруткой
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
// 3D-здание игры: его тип в 3D-сцене и подпись над ним -- имя, уровень, идёт ли стройка (05.11, Валдемар).
|
||||
// Вешает City3DGameplay.Attach. Подпись обновляется дважды в секунду.
|
||||
public class City3DBuildingTag : MonoBehaviour
|
||||
{
|
||||
public string type3d;
|
||||
|
||||
TextMeshPro label;
|
||||
float next;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.unscaledTime < next) return;
|
||||
next = Time.unscaledTime + 0.5f;
|
||||
if (label == null)
|
||||
{
|
||||
var t = transform.Find("Label");
|
||||
if (t != null) label = t.GetComponent<TextMeshPro>();
|
||||
if (label == null) return;
|
||||
}
|
||||
string name = type3d;
|
||||
int level = City3DGameplay.LevelOf(gameObject);
|
||||
var timer = BuildTimer.Of(gameObject);
|
||||
if (timer != null && BuildTimer.All.Contains(timer))
|
||||
{
|
||||
string left = GameData.FormatTime(timer.SecondsLeft);
|
||||
label.text = timer.IsConstruction ? $"{name}\n<size=70%>стройка {left}</size>" : $"{name} {level} ур.\n<size=70%>→ {timer.targetLevel} ур. {left}</size>";
|
||||
label.color = new Color(1f, 0.85f, 0.3f);
|
||||
}
|
||||
else
|
||||
{
|
||||
label.text = $"{name} {level} ур.";
|
||||
label.color = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0bcf6ea4f7ae10b4686b3fd647b0c02f
|
||||
@@ -62,6 +62,7 @@ public class City3DCamera : MonoBehaviour
|
||||
{
|
||||
// Колесо мыши (редактор, ПК)
|
||||
float wheel = Input.mouseScrollDelta.y;
|
||||
if (UIBlocker.IsPointerOverUI()) wheel = 0f; // колесо над окнами HUD -- не приближать камеру (05.11)
|
||||
if (Mathf.Abs(wheel) > 0.01f) Zoom(wheel * 0.1f);
|
||||
|
||||
// Два пальца -- приближение
|
||||
|
||||
@@ -16,9 +16,17 @@ using UnityEngine;
|
||||
// - зоны по уровню Ратуши (кнопки «Ратуша -/+» -- ТОЛЬКО ДЛЯ ТЕСТА), камера плавно показывает новую зону.
|
||||
// Правила места -- в модуле Поселения (Bogatyri.Settlement.Placement, тесты PlacementTests), здесь только показ и ввод.
|
||||
//
|
||||
// Общий HUD (05.11, Валдемар): в сцене стоит префаб HUD_Canvas -- тот же, что в 2D-городе. Нажатия по HUD
|
||||
// в сцену не проходят; кнопка "СТРОЙКА" HUD открывает список зданий (BuildModeController.ExternalToggle);
|
||||
// временные кнопки этой сцены сдвинуты, чтобы не налезать на HUD.
|
||||
//
|
||||
// Здания игры (05.11, Валдемар, City3DGameplay): у каждого 3D-здания те же компоненты, что у 2D-префабов --
|
||||
// тап открывает окно здания (рабочие, добыча, улучшение по таймеру), стройка -- с ценой, лимитом, строителями и
|
||||
// таймером (BuildTimer), Ратуша -- настоящая (TownHall, её уровень открывает зоны). Город сохраняется (City3DSave).
|
||||
//
|
||||
// Пока НЕ подключено (этапы II-III): цена и таймер стройки, Экономика и награда за расчистку, сохранение,
|
||||
// дороги, настоящие модели. Кнопки -- временные (IMGUI); окна согласуются с Володей.
|
||||
public class City3DController : MonoBehaviour
|
||||
public class City3DController : MonoBehaviour, City3DSave.ILive
|
||||
{
|
||||
// Тип здания для прототипа: максимальный габарит (клеток) с первого уровня, высота куба, цвет
|
||||
struct BuildingType
|
||||
@@ -79,6 +87,112 @@ public class City3DController : MonoBehaviour
|
||||
|
||||
// ---------------- Построение сцены ----------------
|
||||
|
||||
bool buildMenuOpen; // список "что построить" открывается кнопкой "СТРОЙКА" общего HUD
|
||||
|
||||
// HUD закрывает верх (ресурсы) и низ (кнопки) экрана -- временные кнопки сцены ставим между ними (в единицах 720)
|
||||
const float HudTop = 150f, HudBottom = 150f, HudLeft = 160f;
|
||||
|
||||
void ToggleBuildMenu()
|
||||
{
|
||||
if (mode == Mode.Place || mode == Mode.Move) { CancelGhost(); buildMenuOpen = false; return; }
|
||||
buildMenuOpen = !buildMenuOpen;
|
||||
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
|
||||
}
|
||||
|
||||
bool stateApplied; // сохранённый город уже поставлен (в первом кадре, после загрузки сохранения в Start)
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
BuildModeController.ExternalToggle = ToggleBuildMenu;
|
||||
BuildModeController.ExternalMove = MoveFromWindow;
|
||||
CitySync.ExternalCity = true;
|
||||
City3DSave.Live = this;
|
||||
EventBus.Subscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (BuildModeController.ExternalToggle == (System.Action)ToggleBuildMenu) BuildModeController.ExternalToggle = null;
|
||||
if (BuildModeController.ExternalMove == (System.Action<GameObject>)MoveFromWindow) BuildModeController.ExternalMove = null;
|
||||
CitySync.ExternalCity = false;
|
||||
// Сохраняемся до ухода из сцены (SceneNavigator, выход из игры) -- здесь здания могут быть уже убраны
|
||||
if (City3DSave.Live == (City3DSave.ILive)this) City3DSave.Live = null;
|
||||
EventBus.Unsubscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
|
||||
}
|
||||
|
||||
// ---------------- Сохранение (City3DSave) ----------------
|
||||
|
||||
public City3DSave.Data CaptureState()
|
||||
{
|
||||
var d = new City3DSave.Data { nextId = nextBuildingId };
|
||||
foreach (var b in map.Buildings)
|
||||
{
|
||||
if (!buildingViews.TryGetValue(b.Id, out var view) || view == null) continue;
|
||||
var e = new City3DSave.Entry { id = b.Id, type = b.Type, x = b.X, y = b.Y, rot = b.Rotation, level = City3DGameplay.LevelOf(view) };
|
||||
City3DSave.WriteTimer(e, view);
|
||||
d.buildings.Add(e);
|
||||
}
|
||||
d.cleared.AddRange(map.Cleared);
|
||||
return d;
|
||||
}
|
||||
|
||||
// Загрузили другое сохранение или сбросили -- город уже стоит, перестраиваем сцену целиком
|
||||
public void OnSaveLoaded()
|
||||
{
|
||||
if (stateApplied) UnityEngine.SceneManagement.SceneManager.LoadScene(gameObject.scene.name);
|
||||
}
|
||||
|
||||
void ApplySaved()
|
||||
{
|
||||
stateApplied = true;
|
||||
var d = City3DSave.Cached;
|
||||
int c = City3DLayout.Size / 2;
|
||||
bool hasTownHall = false;
|
||||
if (d != null)
|
||||
{
|
||||
nextBuildingId = Mathf.Max(1, d.nextId);
|
||||
map.RestoreCleared(d.cleared);
|
||||
foreach (var id in d.cleared)
|
||||
if (obstacleViews.TryGetValue(id, out var ov)) { Destroy(ov); obstacleViews.Remove(id); }
|
||||
// Ратуша первой: её уровень открывает зоны
|
||||
var list = new List<City3DSave.Entry>(d.buildings);
|
||||
list.Sort((a, b) => (b.type == "Ратуша").CompareTo(a.type == "Ратуша"));
|
||||
foreach (var e in list)
|
||||
{
|
||||
int type = System.Array.FindIndex(Types, t => t.Name == e.type);
|
||||
if (type < 0) continue;
|
||||
var t = Types[type];
|
||||
var rect = new PlacedRect { Id = e.id, Type = t.Name, X = e.x, Y = e.y, SizeW = t.W, SizeH = t.H, Rotation = e.rot };
|
||||
if (!map.Restore(rect)) continue;
|
||||
buildingType[e.id] = type;
|
||||
buildingViews[e.id] = CreateBuildingView(e.id, type);
|
||||
City3DGameplay.Attach(buildingViews[e.id], t.Name, e.level);
|
||||
City3DSave.ReadTimer(e, buildingViews[e.id]);
|
||||
UpdateBuildingView(e.id);
|
||||
if (type == 0) { hasTownHall = true; townHallLevel = Mathf.Clamp(e.level, 1, 5); }
|
||||
}
|
||||
}
|
||||
if (!hasTownHall) PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре
|
||||
UpdateGrass();
|
||||
RefreshGround();
|
||||
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
|
||||
}
|
||||
|
||||
// Кнопка "Перенести" в окне здания
|
||||
void MoveFromWindow(GameObject view)
|
||||
{
|
||||
foreach (var pair in buildingViews)
|
||||
if (pair.Value == view)
|
||||
{
|
||||
if (mode == Mode.Place || mode == Mode.Move) CancelGhost();
|
||||
buildMenuOpen = false;
|
||||
StartMoving(pair.Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void OnTownHallLevel(TownHallLevelChangedEvent e) => ApplyTownHallLevel(e.Level);
|
||||
|
||||
void Start()
|
||||
{
|
||||
cam = Camera.main;
|
||||
@@ -89,6 +203,7 @@ public class City3DController : MonoBehaviour
|
||||
cameraRig.Apply();
|
||||
|
||||
Map3D.SetUpLook(cam, 30f, 85f); // солнце, небо, дымка вдали
|
||||
City3DGameplay.PutHudInFront(cam); // общий HUD -- поверх холмов и деревьев
|
||||
|
||||
layout = City3DLayout.Create();
|
||||
map = layout.Map;
|
||||
@@ -96,9 +211,8 @@ public class City3DController : MonoBehaviour
|
||||
BuildObstacles();
|
||||
BuildGrass();
|
||||
|
||||
// Ратуша стоит в центре с самого начала
|
||||
int c = City3DLayout.Size / 2;
|
||||
PlaceNew(0, c - 2, c - 2, 0);
|
||||
// Ратуша и остальные здания -- из сохранения (или новая Ратуша в центре): в первом кадре, когда сохранение
|
||||
// уже загружено (оно грузится в Start), см. ApplySaved
|
||||
RefreshGround();
|
||||
}
|
||||
|
||||
@@ -259,6 +373,7 @@ public class City3DController : MonoBehaviour
|
||||
nextBuildingId++;
|
||||
buildingType[id] = type;
|
||||
buildingViews[id] = CreateBuildingView(id, type);
|
||||
City3DGameplay.Attach(buildingViews[id], t.Name, 1); // здание игры: окно, рабочие, уровни (без цены и таймера)
|
||||
UpdateBuildingView(id);
|
||||
UpdateGrass();
|
||||
return true;
|
||||
@@ -464,8 +579,15 @@ public class City3DController : MonoBehaviour
|
||||
{
|
||||
int type = placingType;
|
||||
int x = ghostX, y = ghostY, rot = ghostRot;
|
||||
string problem = City3DGameplay.CanBuild(Types[type].Name); // уровень Ратуши, лимит, строители, ресурсы
|
||||
if (problem != null) { Say(problem); return; }
|
||||
string id = "b_" + nextBuildingId;
|
||||
CancelGhost();
|
||||
if (PlaceNew(type, x, y, rot)) Say(Types[type].Name + " построено (прототип: без цены и таймера)");
|
||||
if (PlaceNew(type, x, y, rot))
|
||||
{
|
||||
City3DGameplay.StartConstruction(buildingViews[id]); // цена и стройка по времени (BuildTimer)
|
||||
Say(Types[type].Name + ": стройка началась");
|
||||
}
|
||||
}
|
||||
else if (mode == Mode.Move)
|
||||
{
|
||||
@@ -508,6 +630,7 @@ public class City3DController : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!stateApplied && map != null) ApplySaved();
|
||||
if (cam == null) return;
|
||||
bool down, held, up;
|
||||
Vector3 pos;
|
||||
@@ -530,7 +653,7 @@ public class City3DController : MonoBehaviour
|
||||
|
||||
if (down)
|
||||
{
|
||||
pointerOverGui = OverGui(pos);
|
||||
pointerOverGui = OverGui(pos) || UIBlocker.IsPointerOverUI(); // и кнопки/окна общего HUD
|
||||
if (pointerOverGui) return;
|
||||
pointerDown = pos;
|
||||
pointerMoved = false;
|
||||
@@ -585,6 +708,14 @@ public class City3DController : MonoBehaviour
|
||||
}
|
||||
string building = map.BuildingAt(cx, cy);
|
||||
if (mode == Mode.Edit && building != null) { StartMoving(building); return; }
|
||||
// Тап по зданию -- окно здания игры (как в 2D): рабочие, добыча, улучшение, перенос
|
||||
if (building != null && buildingViews.TryGetValue(building, out var tapped) && tapped != null)
|
||||
{
|
||||
selectedBuilding = selectedObstacle = null;
|
||||
buildMenuOpen = false;
|
||||
BuildingWindow.Open(tapped);
|
||||
return;
|
||||
}
|
||||
selectedBuilding = building;
|
||||
selectedObstacle = building == null ? map.ObstacleAt(cx, cy) : null;
|
||||
}
|
||||
@@ -623,10 +754,24 @@ public class City3DController : MonoBehaviour
|
||||
if (ghost != null) { var l = ghost.transform.Find("Label"); if (l != null) l.rotation = cam.transform.rotation; }
|
||||
}
|
||||
|
||||
// Кнопки "Ратуша −/+" -- только для теста: меняют уровень настоящей Ратуши (TownHall)
|
||||
void ChangeTownHall(int delta)
|
||||
{
|
||||
var th = TownHall.Instance;
|
||||
int level = Mathf.Clamp((th != null ? th.currentLevel : townHallLevel) + delta, 1, th != null ? th.maxLevel : 5);
|
||||
if (th != null)
|
||||
{
|
||||
th.currentLevel = level;
|
||||
EventBus.Publish(new TownHallLevelChangedEvent { Level = level }); // зоны обновит OnTownHallLevel
|
||||
}
|
||||
else ApplyTownHallLevel(level);
|
||||
}
|
||||
|
||||
// Уровень Ратуши изменился (улучшение в окне Ратуши или тест): открыть зону
|
||||
void ApplyTownHallLevel(int level)
|
||||
{
|
||||
int before = townHallLevel;
|
||||
townHallLevel = Mathf.Clamp(townHallLevel + delta, 1, 5);
|
||||
townHallLevel = Mathf.Clamp(level, 1, 5);
|
||||
if (townHallLevel == before) return;
|
||||
RefreshGround();
|
||||
if (ghost != null) UpdateGhost();
|
||||
@@ -685,13 +830,16 @@ public class City3DController : MonoBehaviour
|
||||
}
|
||||
if (Event.current.type == EventType.Layout) guiRects.Clear();
|
||||
|
||||
// Верх: режим, Ратуша (тест), редактирование
|
||||
Box(new Rect(10, 10, 330, 44), $"3D-поселение (прототип) • Ратуша {townHallLevel} ур.");
|
||||
if (Button(new Rect(350, 10, 60, 44), "−")) ChangeTownHall(-1);
|
||||
if (Button(new Rect(415, 10, 60, 44), "+")) ChangeTownHall(+1);
|
||||
// Окно HUD открыто -- временные кнопки сцены не показываем (и не ловим нажатия)
|
||||
if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") return;
|
||||
|
||||
// Под верхней панелью HUD, справа: Ратуша (тест), редактирование
|
||||
Box(new Rect(w - 480, HudTop, 290, 44), $"3D (прототип) • Ратуша {townHallLevel} ур.");
|
||||
if (Button(new Rect(w - 185, HudTop, 55, 44), "−")) ChangeTownHall(-1);
|
||||
if (Button(new Rect(w - 125, HudTop, 55, 44), "+")) ChangeTownHall(+1);
|
||||
if (mode == Mode.View || mode == Mode.Edit)
|
||||
{
|
||||
if (Button(new Rect(w - 250, 10, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
|
||||
if (Button(new Rect(w - 250, HudTop + 50, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
|
||||
{
|
||||
mode = mode == Mode.Edit ? Mode.View : Mode.Edit;
|
||||
selectedBuilding = selectedObstacle = null;
|
||||
@@ -700,13 +848,13 @@ public class City3DController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Слева: что построить
|
||||
if (mode == Mode.View || mode == Mode.Edit)
|
||||
// Слева, правее кнопок HUD: что построить -- открывается кнопкой "СТРОЙКА" общего HUD
|
||||
if (buildMenuOpen && (mode == Mode.View || mode == Mode.Edit))
|
||||
{
|
||||
float y = 64;
|
||||
float y = HudTop;
|
||||
for (int i = 1; i < Types.Length; i++)
|
||||
{
|
||||
if (Button(new Rect(10, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) StartPlacing(i);
|
||||
if (Button(new Rect(HudLeft, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) { buildMenuOpen = false; StartPlacing(i); }
|
||||
y += 44;
|
||||
}
|
||||
}
|
||||
@@ -717,39 +865,39 @@ public class City3DController : MonoBehaviour
|
||||
string status = ghostError == PlaceError.None
|
||||
? (ghostFoundation > 0 ? $"Можно ставить (фундамент {ghostFoundation / 1000f:0.0} м)" : "Можно ставить")
|
||||
: SettlementMap.ErrorText(ghostError);
|
||||
Box(new Rect(w / 2 - 300, h - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
|
||||
if (Button(new Rect(w / 2 - 300, h - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
|
||||
if (Button(new Rect(w / 2 - 95, h - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
|
||||
if (Button(new Rect(w / 2 + 110, h - 70, 190, 60), "Отмена")) CancelGhost();
|
||||
Box(new Rect(w / 2 - 300, h - HudBottom - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
|
||||
if (Button(new Rect(w / 2 - 300, h - HudBottom - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
|
||||
if (Button(new Rect(w / 2 - 95, h - HudBottom - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
|
||||
if (Button(new Rect(w / 2 + 110, h - HudBottom - 70, 190, 60), "Отмена")) CancelGhost();
|
||||
}
|
||||
|
||||
// Меню здания: перенос
|
||||
if (mode == Mode.View && selectedBuilding != null && map.TryGetBuilding(selectedBuilding, out var b))
|
||||
{
|
||||
Box(new Rect(w / 2 - 250, h - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
|
||||
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
|
||||
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedBuilding = null;
|
||||
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
|
||||
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
|
||||
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedBuilding = null;
|
||||
}
|
||||
|
||||
// Меню дерева / валуна: расчистка без цены и ожидания
|
||||
if (mode == Mode.View && selectedObstacle != null)
|
||||
{
|
||||
bool tree = selectedObstacle.StartsWith("tree");
|
||||
Box(new Rect(w / 2 - 250, h - 120, 500, 44), tree ? "Дерево" : "Валун");
|
||||
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Расчистить"))
|
||||
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), tree ? "Дерево" : "Валун");
|
||||
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Расчистить"))
|
||||
{
|
||||
if (map.ClearObstacle(selectedObstacle) && obstacleViews.TryGetValue(selectedObstacle, out var view))
|
||||
{
|
||||
Destroy(view);
|
||||
Say((tree ? "Дерево" : "Валун") + " расчищено. Награда " + (tree ? "деревом" : "камнем") +
|
||||
" -- после подключения сохранений (этап III)");
|
||||
City3DGameplay.RewardClearing(tree); // +10 дерева или камня, один раз (решение 04.11)
|
||||
Say((tree ? "Дерево" : "Валун") + " расчищено: +10 " + (tree ? "дерева" : "камня"));
|
||||
}
|
||||
selectedObstacle = null;
|
||||
}
|
||||
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedObstacle = null;
|
||||
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedObstacle = null;
|
||||
}
|
||||
|
||||
if (Time.unscaledTime < messageUntil)
|
||||
GUI.Label(new Rect(w / 2 - 350, 64, 700, 60), message, label);
|
||||
GUI.Label(new Rect(w / 2 - 350, HudTop, 700, 60), message, label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
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;
|
||||
default: return type3d; // "Ратуша"
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
default: return Cost();
|
||||
}
|
||||
}
|
||||
|
||||
// С какого уровня Ратуши можно строить (как в 2D)
|
||||
public static int RequiredTownHall(string type3d)
|
||||
{
|
||||
switch (type3d)
|
||||
{
|
||||
case "Шахта": return 3;
|
||||
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());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Награда за расчистку (решение 04.11): +10 дерева за дерево или +10 камня за валун, один раз, в пределах склада
|
||||
public static void RewardClearing(bool tree)
|
||||
{
|
||||
if (GameServices.TryGet(out IEconomy economy)) economy.Add(tree ? ResourceType.Wood : ResourceType.Stone, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b010df83fccefa4aa9c394f84b8214f
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// Сохранение 3D-поселения: секция "settlement3d" (05.11, Валдемар). 2D-город сохраняется отдельно ("settlement"),
|
||||
// поэтому 3D-сцена его не трогает. Здания: id, тип, клетка, поворот, уровень, стройка или улучшение, которое идёт;
|
||||
// Ратуша: уровень и её улучшение; расчищенные деревья и валуны.
|
||||
// Регистрирует App/GameModules. Пока сцена City3D открыта, данные берутся у неё (Live), иначе -- последние известные.
|
||||
public sealed class City3DSave : ISaveParticipant
|
||||
{
|
||||
[Serializable]
|
||||
public class Entry
|
||||
{
|
||||
public string id;
|
||||
public string type; // тип 3D-сцены: "Ратуша", "Дом", "Ферма"...
|
||||
public int x, y, rot;
|
||||
public int level = 1;
|
||||
public int timerKind; // 0 -- нет, 1 -- постройка, 2 -- улучшение (BuildTimer.Kind)
|
||||
public long timerStartMs, timerEndMs;
|
||||
public int timerTargetLevel;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Data
|
||||
{
|
||||
public int nextId = 1;
|
||||
public List<Entry> buildings = new List<Entry>();
|
||||
public List<string> cleared = new List<string>();
|
||||
}
|
||||
|
||||
public interface ILive
|
||||
{
|
||||
Data CaptureState();
|
||||
void OnSaveLoaded(); // загрузили другое сохранение или сбросили -- сцена должна перестроиться
|
||||
}
|
||||
|
||||
public static readonly City3DSave Instance = new City3DSave();
|
||||
public static Data Cached { get; private set; }
|
||||
public static ILive Live;
|
||||
|
||||
public string SaveKey => "settlement3d";
|
||||
|
||||
public string Capture()
|
||||
{
|
||||
if (Live != null) Cached = Live.CaptureState();
|
||||
return Cached != null ? JsonUtility.ToJson(Cached) : null;
|
||||
}
|
||||
|
||||
public void Restore(string json)
|
||||
{
|
||||
Cached = string.IsNullOrEmpty(json) ? null : JsonUtility.FromJson<Data>(json);
|
||||
if (Cached != null)
|
||||
{
|
||||
if (Cached.buildings == null) Cached.buildings = new List<Entry>();
|
||||
if (Cached.cleared == null) Cached.cleared = new List<string>();
|
||||
}
|
||||
Live?.OnSaveLoaded();
|
||||
}
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
Cached = null;
|
||||
Live?.OnSaveLoaded();
|
||||
}
|
||||
|
||||
// Запись таймера здания в Entry и обратно
|
||||
public static void WriteTimer(Entry e, GameObject view)
|
||||
{
|
||||
var t = BuildTimer.Of(view);
|
||||
bool on = t != null && BuildTimer.All.Contains(t);
|
||||
e.timerKind = on ? (int)t.kind : 0;
|
||||
e.timerStartMs = on ? t.startMs : 0;
|
||||
e.timerEndMs = on ? t.endMs : 0;
|
||||
e.timerTargetLevel = on ? t.targetLevel : 0;
|
||||
}
|
||||
|
||||
// Таймер из файла -- только осмысленный: постройка у 1 уровня, улучшение до уровня выше текущего
|
||||
public static void ReadTimer(Entry e, GameObject view)
|
||||
{
|
||||
bool build = e.timerKind == (int)BuildTimer.Kind.Build && e.level <= 1;
|
||||
bool upgrade = e.timerKind == (int)BuildTimer.Kind.Upgrade && e.timerTargetLevel > Mathf.Max(1, e.level);
|
||||
if ((build || upgrade) && e.timerEndMs > 0)
|
||||
BuildTimer.Begin(view, (BuildTimer.Kind)e.timerKind, Mathf.Max(1, e.timerTargetLevel), e.timerStartMs, e.timerEndMs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 037c1ed1fee52f74da1eaaeb4c20e3b1
|
||||
Reference in New Issue
Block a user