Перенос 2D → 3D без потерь (расчистка места, очередь «ждут места»); DEV «Камера» -- ползунки наклона, перспективы и отдаления

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-05 17:56:38 +01:00
co-authored by Claude Opus 5.5
parent 8a5f7253db
commit f78eb7ea7e
7 changed files with 260 additions and 20 deletions
+76 -18
View File
@@ -151,6 +151,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
d.buildings.Add(e);
}
d.cleared.AddRange(map.Cleared);
d.pendingFrom2D.AddRange(pendingFrom2D);
return d;
}
@@ -169,6 +170,8 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
if (d != null)
{
nextBuildingId = Mathf.Max(1, d.nextId);
pendingFrom2D.Clear();
if (d.pendingFrom2D != null) pendingFrom2D.AddRange(d.pendingFrom2D);
map.RestoreCleared(d.cleared);
foreach (var id in d.cleared)
if (obstacleViews.TryGetValue(id, out var ov)) { Destroy(ov); obstacleViews.Remove(id); }
@@ -195,6 +198,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре
if (d == null) MoveFrom2D(); // 3D-города в сохранении ещё нет, а 2D есть -- перенести постройки игрока
}
else if (pendingFrom2D.Count > 0) PlacePendingFrom2D(); // здания из 2D, которые ждали места
UpdateGrass();
RefreshGround();
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
@@ -220,34 +224,87 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
BuildTimer.Begin(th.gameObject, BuildTimer.Kind.Upgrade, old.townHallTimerTargetLevel, old.townHallTimerStartMs, old.townHallTimerEndMs);
}
int c = City3DLayout.Size / 2, moved = 0, lost = 0;
int unknown = 0;
pendingFrom2D.Clear();
foreach (var e in old.placedBuildings ?? new List<SettlementModule.PlacedBuildingSaveEntry>())
{
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))
if (type3d == null || System.Array.FindIndex(Types, t => t.Name == type3d) <= 0)
{
Debug.LogWarning($"[3D] Перенос 2D-города: для «{e.buildingName}» нет свободного места -- пропущено");
lost++;
Debug.LogWarning($"[3D] Перенос 2D-города: здания «{e.buildingName}» в 3D нет -- пропущено");
unknown++;
continue;
}
string id = "b_" + (nextBuildingId - 1);
var view = buildingViews[id];
var leveled = view.GetComponent<ILeveledBuilding>();
if (leveled != null) leveled.Level = Mathf.Max(1, e.level);
City3DSave.ReadTimer(new City3DSave.Entry
pendingFrom2D.Add(new City3DSave.Entry
{
level = Mathf.Max(1, e.level), timerKind = e.timerKind, timerStartMs = e.timerStartMs,
timerEndMs = e.timerEndMs, timerTargetLevel = e.timerTargetLevel,
}, view);
moved++;
type = type3d, level = Mathf.Max(1, e.level), timerKind = e.timerKind,
timerStartMs = e.timerStartMs, timerEndMs = e.timerEndMs, timerTargetLevel = e.timerTargetLevel,
});
}
Debug.Log($"[3D] Перенос 2D-города: Ратуша {old.townHallLevel} ур., зданий перенесено {moved}" + (lost > 0 ? $", пропущено {lost}" : ""));
if (moved > 0 || lost > 0) Say(lost == 0 ? $"Город перенесён в 3D: {moved} зданий" : $"Город перенесён в 3D: {moved} зданий, {lost} не поместились");
int total = pendingFrom2D.Count;
int placed = PlacePendingFrom2D(false);
Debug.Log($"[3D] Перенос 2D-города: Ратуша {old.townHallLevel} ур., зданий {total}: поставлено {placed}, ждут места {pendingFrom2D.Count}" +
(unknown > 0 ? $", нет в 3D {unknown}" : ""));
if (total > 0)
Say(pendingFrom2D.Count == 0 ? $"Город перенесён в 3D: {placed} зданий"
: $"Город перенесён в 3D: {placed} зданий, {pendingFrom2D.Count} ждут места -- встанут, когда оно появится");
}
// Здания из 2D, которые ещё не стоят: ближайшее к центру место, сначала свободное, потом -- с расчисткой деревьев
// и валунов (бесплатно, без награды: это не стройка игрока). Нет места -- ждут (сохраняются в settlement3d).
// Зовётся при переносе, при входе в город и когда Ратуша открыла зону. Возвращает, сколько поставлено
int PlacePendingFrom2D(bool announce = true)
{
int placed = 0, c = City3DLayout.Size / 2;
for (int i = 0; i < pendingFrom2D.Count; i++)
{
var e = pendingFrom2D[i];
int type = System.Array.FindIndex(Types, t => t.Name == e.type);
if (type <= 0) continue;
var t = Types[type];
int x, y, rot;
bool found = FindFreeSpot(t, c - t.W / 2, c - t.H / 2, out x, out y, out rot)
|| FindSpotIfCleared(t, c - t.W / 2, c - t.H / 2, out x, out y, out rot);
if (!found) continue;
foreach (var obstacle in map.ObstaclesUnder(x, y, t.W, t.H, rot))
if (map.ClearObstacle(obstacle) && obstacleViews.TryGetValue(obstacle, out var ov)) { Destroy(ov); obstacleViews.Remove(obstacle); }
if (!PlaceNew(type, x, y, rot)) continue;
var view = buildingViews["b_" + (nextBuildingId - 1)];
var leveled = view.GetComponent<ILeveledBuilding>();
if (leveled != null) leveled.Level = e.level;
City3DSave.ReadTimer(e, view);
pendingFrom2D.RemoveAt(i--);
placed++;
}
if (placed > 0)
{
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
if (announce) Say($"Из старого города поставлено зданий: {placed}" + (pendingFrom2D.Count > 0 ? $", ещё ждут места: {pendingFrom2D.Count}" : ""));
}
return placed;
}
bool FindSpotIfCleared(BuildingType t, int x0, int y0, out int x, out int y, out int rot)
{
for (int r = 0; r < City3DLayout.Size; r++)
for (int dy = -r; dy <= r; dy++)
for (int dx = -r; dx <= r; dx++)
{
if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dy)) != r) continue;
foreach (int tryRot in new[] { 0, 90 })
if (map.ValidateIfCleared(x0 + dx, y0 + dy, t.W, t.H, tryRot, townHallLevel, null, out _) == PlaceError.None)
{
x = x0 + dx; y = y0 + dy; rot = tryRot;
return true;
}
}
x = x0; y = y0; rot = 0;
return false;
}
readonly List<City3DSave.Entry> pendingFrom2D = new List<City3DSave.Entry>();
// Кнопка "Перенести" в окне здания
void MoveFromWindow(GameObject view)
{
@@ -925,6 +982,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
int half = City3DLayout.ZoneHalfSize(townHallLevel), c = City3DLayout.Size / 2;
cameraRig.FocusOn(new Rect(c - half, c - half, half * 2, half * 2));
Say($"Ратуша {townHallLevel} ур.: открыта новая зона");
if (stateApplied && pendingFrom2D.Count > 0) PlacePendingFrom2D(); // здания из 2D, которые ждали места
}
}