Feature/3d hud #18
@@ -85,11 +85,118 @@ public class DebugPanel : MonoBehaviour
|
||||
Btn(row4, "Рабочие", UIKit.Theme.Blue, () => BuildingWindow.Open(null));
|
||||
gridButton = UIKit.Button(row4, GridLabel, UIKit.Theme.Blue, ToggleGrid, -1, 70, 24);
|
||||
UIKit.Size(gridButton, -1, 70, 1);
|
||||
Btn(row4, "Закрыть", UIKit.Theme.Gray, () => panel.SetActive(false));
|
||||
Btn(row4, "Камера", UIKit.Theme.Blue, ToggleCameraCard);
|
||||
Btn(row4, "Закрыть", UIKit.Theme.Gray, () => { panel.SetActive(false); if (cameraCard != null) cameraCard.SetActive(false); });
|
||||
|
||||
panel.SetActive(false);
|
||||
}
|
||||
|
||||
// ---------------- Камера 3D-поселения (решение 05.11: перспектива, числа подбираем в Unity и на телефоне) ----------------
|
||||
|
||||
GameObject cameraCard;
|
||||
TextMeshProUGUI cameraInfo;
|
||||
|
||||
// Ползунок: подпись, значение, диапазон. get/set -- поле камеры
|
||||
struct CamSlider { public string Label, Format; public float Min, Max; public System.Func<City3DCamera, float> Get; public System.Action<City3DCamera, float> Set; public string Pref; }
|
||||
|
||||
static readonly CamSlider[] CamSliders =
|
||||
{
|
||||
new CamSlider { Label = "Наклон", Format = "0°", Min = 25, Max = 85, Pref = City3DCamera.PrefPitch,
|
||||
Get = c => c.pitch, Set = (c, v) => c.pitch = v },
|
||||
new CamSlider { Label = "Перспектива", Format = "0°", Min = 15, Max = 75, Pref = City3DCamera.PrefFov,
|
||||
Get = c => c.fieldOfView, Set = (c, v) => c.SetFieldOfView(v) },
|
||||
new CamSlider { Label = "Ближе всего", Format = "0", Min = 3, Max = 30, Pref = City3DCamera.PrefMin,
|
||||
Get = c => c.minDistance, Set = (c, v) => { c.minDistance = Mathf.Min(v, c.maxDistance - 1); c.distance = Mathf.Max(c.distance, c.minDistance); } },
|
||||
new CamSlider { Label = "Дальше всего", Format = "0", Min = 20, Max = 120, Pref = City3DCamera.PrefMax,
|
||||
Get = c => c.maxDistance, Set = (c, v) => { c.maxDistance = Mathf.Max(v, c.minDistance + 1); c.distance = Mathf.Min(c.distance, c.maxDistance); } },
|
||||
new CamSlider { Label = "Сейчас", Format = "0", Min = 3, Max = 120, Pref = null,
|
||||
Get = c => c.distance, Set = (c, v) => c.distance = Mathf.Clamp(v, c.minDistance, c.maxDistance) },
|
||||
};
|
||||
|
||||
static City3DCamera Cam3D => Object.FindAnyObjectByType<City3DCamera>();
|
||||
|
||||
void ToggleCameraCard()
|
||||
{
|
||||
if (cameraCard != null) { bool show = !cameraCard.activeSelf; if (show) BuildCameraCard(); else cameraCard.SetActive(false); return; }
|
||||
BuildCameraCard();
|
||||
}
|
||||
|
||||
void BuildCameraCard()
|
||||
{
|
||||
if (cameraCard != null) Destroy(cameraCard);
|
||||
var cam = Cam3D;
|
||||
var safe = panel.transform.parent;
|
||||
var card = UIKit.Card(safe, new Color(0.08f, 0.08f, 0.11f, 0.95f), 20, 10);
|
||||
card.anchorMin = card.anchorMax = new Vector2(0, 0.5f);
|
||||
card.pivot = new Vector2(0, 0.5f);
|
||||
card.anchoredPosition = new Vector2(135 + 640, 0);
|
||||
card.sizeDelta = new Vector2(640, 0);
|
||||
card.gameObject.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
cameraCard = card.gameObject;
|
||||
|
||||
var title = UIKit.Text(card, "КАМЕРА 3D-ПОСЕЛЕНИЯ (ТЕСТ)", 28, UIKit.Theme.Gold, TextAlignmentOptions.Center);
|
||||
title.fontStyle = FontStyles.Bold;
|
||||
if (cam == null)
|
||||
{
|
||||
UIKit.Text(card, "Откройте 3D-город (сцена City3D)", 24, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var def in CamSliders)
|
||||
{
|
||||
var d = def;
|
||||
var row = UIKit.Row(card, 60, 12);
|
||||
var label = UIKit.Text(row, d.Label, 24);
|
||||
UIKit.Size(label, 170);
|
||||
var value = UIKit.Text(row, d.Get(cam).ToString(d.Format), 24, UIKit.Theme.TextDim, TextAlignmentOptions.Right);
|
||||
UIKit.Slider(row, Mathf.InverseLerp(d.Min, d.Max, d.Get(cam)), t =>
|
||||
{
|
||||
var c = Cam3D;
|
||||
if (c == null) return;
|
||||
d.Set(c, Mathf.Lerp(d.Min, d.Max, t));
|
||||
value.text = d.Get(c).ToString(d.Format);
|
||||
if (d.Pref != null) PlayerPrefs.SetFloat(d.Pref, d.Get(c)); // запомнить на этом устройстве
|
||||
c.Apply();
|
||||
UpdateCameraInfo();
|
||||
});
|
||||
value.transform.SetAsLastSibling();
|
||||
UIKit.Size(value, 80);
|
||||
}
|
||||
}
|
||||
cameraInfo = UIKit.Text(card, "", 22, UIKit.Theme.TextDim, TextAlignmentOptions.Center);
|
||||
UpdateCameraInfo();
|
||||
|
||||
var row2 = UIKit.Row(card, 64, 10);
|
||||
Btn(row2, "Сбросить", UIKit.Theme.Gray, ResetCamera);
|
||||
Btn(row2, "Закрыть", UIKit.Theme.Gray, () => cameraCard.SetActive(false));
|
||||
}
|
||||
|
||||
// Строка, которую удобно переписать в отчёт / в сцену
|
||||
void UpdateCameraInfo()
|
||||
{
|
||||
var c = Cam3D;
|
||||
if (cameraInfo == null) return;
|
||||
cameraInfo.text = c == null ? "" :
|
||||
$"pitch {c.pitch:0} • fieldOfView {c.fieldOfView:0} • minDistance {c.minDistance:0} • maxDistance {c.maxDistance:0} • distance {c.distance:0}\n" +
|
||||
"Подобранное запоминается на этом устройстве. В сцену (для всех) -- вписать числа в Main Camera → City3DCamera";
|
||||
}
|
||||
|
||||
void ResetCamera()
|
||||
{
|
||||
foreach (var d in CamSliders) if (d.Pref != null) PlayerPrefs.DeleteKey(d.Pref);
|
||||
var c = Cam3D;
|
||||
if (c != null)
|
||||
{
|
||||
c.pitch = c.DefaultPitch;
|
||||
c.SetFieldOfView(c.DefaultFov);
|
||||
c.minDistance = c.DefaultMin;
|
||||
c.maxDistance = c.DefaultMax;
|
||||
c.distance = Mathf.Clamp(c.distance, c.minDistance, c.maxDistance);
|
||||
c.Apply();
|
||||
}
|
||||
BuildCameraCard();
|
||||
}
|
||||
|
||||
// Отладочная сетка стройки (3D-поселение и 2D-город): видна всегда, ярче, каждая 5-я линия жёлтая (05.11, просьба Миши)
|
||||
void ToggleGrid()
|
||||
{
|
||||
|
||||
@@ -29,9 +29,38 @@ public class City3DCamera : MonoBehaviour
|
||||
public bool perspective = true;
|
||||
public float fieldOfView = 40f;
|
||||
|
||||
// Подбор камеры в панели DEV (окно «Камера»): наклон, поле зрения, ближе / дальше всего. Запоминается на этом
|
||||
// устройстве -- только в редакторе и отладочной сборке; в обычной сборке -- числа из сцены (05.11)
|
||||
public const string PrefPitch = "dev_cam3d_pitch", PrefFov = "dev_cam3d_fov", PrefMin = "dev_cam3d_min", PrefMax = "dev_cam3d_max";
|
||||
|
||||
// Числа по умолчанию (из сцены) -- для кнопки «Сбросить» в DEV
|
||||
public float DefaultPitch { get; private set; }
|
||||
public float DefaultFov { get; private set; }
|
||||
public float DefaultMin { get; private set; }
|
||||
public float DefaultMax { get; private set; }
|
||||
|
||||
public void SetFieldOfView(float fov)
|
||||
{
|
||||
fieldOfView = fov;
|
||||
if (cam != null && !cam.orthographic) cam.fieldOfView = fov;
|
||||
}
|
||||
|
||||
void LoadDevTuning()
|
||||
{
|
||||
DefaultPitch = pitch; DefaultFov = fieldOfView; DefaultMin = minDistance; DefaultMax = maxDistance;
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
pitch = PlayerPrefs.GetFloat(PrefPitch, pitch);
|
||||
fieldOfView = PlayerPrefs.GetFloat(PrefFov, fieldOfView);
|
||||
minDistance = PlayerPrefs.GetFloat(PrefMin, minDistance);
|
||||
maxDistance = Mathf.Max(minDistance + 1f, PlayerPrefs.GetFloat(PrefMax, maxDistance));
|
||||
distance = Mathf.Clamp(distance, minDistance, maxDistance);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
cam = GetComponent<Camera>();
|
||||
LoadDevTuning();
|
||||
if (cam != null && perspective)
|
||||
{
|
||||
cam.orthographic = false;
|
||||
|
||||
@@ -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, которые ждали места
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ public sealed class City3DSave : ISaveParticipant
|
||||
public int nextId = 1;
|
||||
public List<Entry> buildings = new List<Entry>();
|
||||
public List<string> cleared = new List<string>();
|
||||
// Здания 2D-города, которым при переносе не нашлось места (зона закрыта, всё занято): ждут и ставятся,
|
||||
// как только место появится. Не теряются (05.11)
|
||||
public List<Entry> pendingFrom2D = new List<Entry>();
|
||||
}
|
||||
|
||||
public interface ILive
|
||||
@@ -53,6 +56,7 @@ public sealed class City3DSave : ISaveParticipant
|
||||
{
|
||||
if (Cached.buildings == null) Cached.buildings = new List<Entry>();
|
||||
if (Cached.cleared == null) Cached.cleared = new List<string>();
|
||||
if (Cached.pendingFrom2D == null) Cached.pendingFrom2D = new List<Entry>();
|
||||
}
|
||||
Live?.OnSaveLoaded();
|
||||
}
|
||||
|
||||
@@ -152,6 +152,29 @@ namespace Bogatyri.Settlement.Placement
|
||||
// Проверить место: прямоугольник sizeW x sizeH с поворотом rotation, левый нижний угол (x, y).
|
||||
// ignoreId -- здание, которое переносят: его прежний след не мешает. foundationMm -- высота фундамента
|
||||
public PlaceError Validate(int x, int y, int sizeW, int sizeH, int rotation, int townHallLevel, string ignoreId, out int foundationMm)
|
||||
=> ValidateCore(x, y, sizeW, sizeH, rotation, townHallLevel, ignoreId, false, out foundationMm);
|
||||
|
||||
// То же, но деревья и валуны не мешают: можно ли поставить здесь, если расчистить (перенос 2D-города, 05.11)
|
||||
public PlaceError ValidateIfCleared(int x, int y, int sizeW, int sizeH, int rotation, int townHallLevel, string ignoreId, out int foundationMm)
|
||||
=> ValidateCore(x, y, sizeW, sizeH, rotation, townHallLevel, ignoreId, true, out foundationMm);
|
||||
|
||||
// Деревья и валуны (id) под прямоугольником -- что придётся расчистить. Каждый -- один раз
|
||||
public List<string> ObstaclesUnder(int x, int y, int sizeW, int sizeH, int rotation)
|
||||
{
|
||||
rotation = NormalizeRotation(rotation);
|
||||
int w = rotation == 90 || rotation == 270 ? sizeH : sizeW;
|
||||
int h = rotation == 90 || rotation == 270 ? sizeW : sizeH;
|
||||
var ids = new List<string>();
|
||||
for (int cy = y; cy < y + h; cy++)
|
||||
for (int cx = x; cx < x + w; cx++)
|
||||
{
|
||||
string id = ObstacleAt(cx, cy);
|
||||
if (id != null && !ids.Contains(id)) ids.Add(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
PlaceError ValidateCore(int x, int y, int sizeW, int sizeH, int rotation, int townHallLevel, string ignoreId, bool ignoreObstacles, out int foundationMm)
|
||||
{
|
||||
foundationMm = 0;
|
||||
rotation = NormalizeRotation(rotation);
|
||||
@@ -167,7 +190,7 @@ namespace Bogatyri.Settlement.Placement
|
||||
if (kind[i] == CellKind.Water) return PlaceError.Water;
|
||||
if (kind[i] == CellKind.Mountain) return PlaceError.Mountain;
|
||||
if (!IsOpen(cx, cy, townHallLevel)) return PlaceError.ZoneLocked;
|
||||
if (obstacleAt.ContainsKey(i)) return PlaceError.Obstacle;
|
||||
if (!ignoreObstacles && obstacleAt.ContainsKey(i)) return PlaceError.Obstacle;
|
||||
min = Math.Min(min, heightMm[i]);
|
||||
max = Math.Max(max, heightMm[i]);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,24 @@ public class PlacementTests
|
||||
Assert.AreEqual(PlaceError.None, map.Validate(15, 2, 2, 2, 0, 2, null, out _), "11: Ратуша 2 открыла зону");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Placement_IfCleared_IgnoresOnlyTreesAndListsThem()
|
||||
{
|
||||
var map = Map();
|
||||
Assert.AreEqual(PlaceError.Obstacle, map.Validate(4, 4, 2, 2, 0, 1, null, out _), "дерево мешает");
|
||||
Assert.AreEqual(PlaceError.None, map.ValidateIfCleared(4, 4, 2, 2, 0, 1, null, out _), "если расчистить -- можно");
|
||||
var under = map.ObstaclesUnder(4, 4, 2, 2, 0);
|
||||
Assert.AreEqual(1, under.Count, "под зданием -- одно дерево");
|
||||
Assert.AreEqual("tree_1", under[0]);
|
||||
Assert.AreEqual(0, map.ObstaclesUnder(1, 1, 2, 2, 0).Count, "рядом деревьев нет");
|
||||
Assert.AreEqual(PlaceError.Water, map.ValidateIfCleared(9, 2, 2, 2, 0, 1, null, out _), "река остаётся рекой");
|
||||
Assert.AreEqual(PlaceError.ZoneLocked, map.ValidateIfCleared(15, 2, 2, 2, 0, 1, null, out _), "закрытая зона -- закрыта");
|
||||
map.Place("a", "Дом", 1, 1, 2, 2, 0, 1);
|
||||
Assert.AreEqual(PlaceError.Occupied, map.ValidateIfCleared(1, 1, 2, 2, 0, 1, null, out _), "чужое здание мешает");
|
||||
Assert.IsTrue(map.ClearObstacle("tree_1"));
|
||||
Assert.AreEqual(PlaceError.None, map.Place("b", "Дом", 4, 4, 2, 2, 0, 1), "после расчистки ставится");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Placement_GapBetweenBuildings()
|
||||
{
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 05.11.2026 | Claude (Саша) | Перенос 2D-города надёжнее: если места нет, деревья и валуны под зданием расчищаются (бесплатно, без награды — `SettlementMap.ValidateIfCleared`, `ObstaclesUnder`, тест `Placement_IfCleared_IgnoresOnlyTreesAndListsThem`); если места нет и так — здание не теряется, а ждёт в `settlement3d.pendingFrom2D` и ставится при входе в город или когда Ратуша открывает зону. DEV → «Камера»: ползунки наклона, перспективы (поле зрения), ближе / дальше всего и текущего отдаления; подобранное запоминается на устройстве (только редактор и отладочная сборка), «Сбросить» — числа из сцены. |
|
||||
| 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`) — не нужно закрывать и открывать окно. |
|
||||
|
||||
Reference in New Issue
Block a user