Files
MVP1/Assets/Scripts/City3D/City3DLayout.cs
AlexanderandClaude Opus 5.5 a3c4a99a79 3D-поселение, этап I: прототип на примитивах (ТЗ от 04.11)
Bogatyri.Settlement.Placement (без Unity): сетка, зоны по Ратуше, река/скалы/деревья/валуны,
отступ, уклон и фундамент, поворот 0/90/180/270, атомарный перенос с тем же id, расчистка один раз; 7 тестов.
Сцена City3D (Tools -> Bogatyri -> 3D-город): местность, призрак, перенос, расчистка, камера 2.5D,
второй рендерер URP (3D) только для её камеры -- 2D-сцены не тронуты.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:50:22 +01:00

98 lines
5.5 KiB
C#

using System;
using System.Collections.Generic;
using Bogatyri.Settlement.Placement;
using UnityEngine;
// ТЕСТОВЫЙ УЧАСТОК 3D-ПОСЕЛЕНИЯ (ТЗ «Система строительства поселения» от 04.11.2026, этап I, прототип).
// Холмы, река, скалы, деревья и валуны строятся из seed -- одинаково при каждом запуске.
// Зоны стройки открываются уровнем Ратуши квадратами от центра (ТЕСТ, число и пороги зон -- на согласование).
// Все числа здесь -- предложение для прототипа, не утверждённый баланс.
public sealed class City3DLayout
{
public const int Size = 48; // клеток по каждой стороне
public const int Seed = 4112026;
public struct Obstacle
{
public string Id;
public bool IsTree; // дерево (иначе валун)
public List<(int x, int y)> Cells;
}
public SettlementMap Map;
public readonly List<Obstacle> Obstacles = new List<Obstacle>();
public readonly List<Vector2> Grass = new List<Vector2>(); // мелкий декор: скрывается под зданиями
// Сколько клеток от центра открывает уровень Ратуши: 1 -- 16 x 16, 2 -- 24 x 24, ... 5 -- весь участок
public static int ZoneHalfSize(int townHallLevel) => Mathf.Min(Size / 2, 8 + 4 * (Mathf.Max(1, townHallLevel) - 1));
// Высота земли в точке (мировые единицы, клетка = 1)
public static float HeightAt(float x, float z)
{
float cx = x - Size / 2f, cz = z - Size / 2f;
float fromCenter = Mathf.Sqrt(cx * cx + cz * cz) / (Size / 2f);
// Холмы: в центре почти ровно, к краям -- выше и круче
float hills = (Mathf.Sin(x * 0.27f) * 0.5f + Mathf.Cos(z * 0.21f) * 0.45f + Mathf.PerlinNoise(x * 0.11f, z * 0.11f) * 0.9f)
* Mathf.Lerp(0.25f, 1.6f, Mathf.Clamp01(fromCenter));
if (IsMountain(x, z)) hills += 2.5f + Mathf.PerlinNoise(x * 0.3f, z * 0.3f) * 2f;
if (IsRiver(x, z)) hills = -0.5f;
return hills;
}
static float RiverX(float z) => 37f + 3f * Mathf.Sin(z * 0.17f);
static bool IsRiver(float x, float z) => Mathf.Abs(x - RiverX(z)) < 1.3f;
static bool IsMountain(float x, float z) => x < 9f && z > 34f || x < 4f && z > 24f;
public static City3DLayout Create(PlacementRules rules = null)
{
var layout = new City3DLayout { Map = new SettlementMap(Size, Size, rules) };
var map = layout.Map;
int c = Size / 2;
for (int y = 0; y < Size; y++)
for (int x = 0; x < Size; x++)
{
float px = x + 0.5f, pz = y + 0.5f;
CellKind kind = IsRiver(px, pz) ? CellKind.Water : IsMountain(px, pz) ? CellKind.Mountain : CellKind.Ground;
float dist = Mathf.Max(Mathf.Abs(px - c), Mathf.Abs(pz - c)); // квадратное кольцо от центра
int zone = 0;
for (int lvl = 1; lvl <= 5; lvl++) if (dist < ZoneHalfSize(lvl)) { zone = lvl; break; }
map.SetCell(x, y, kind, Mathf.RoundToInt(HeightAt(px, pz) * 1000f), zone);
}
// Деревья и валуны: не в центральной поляне 10 x 10 (там Ратуша), не на воде и скалах
var rng = new System.Random(Seed);
var taken = new HashSet<int>();
bool Free(int x, int y) => map.InMap(x, y) && map.KindAt(x, y) == CellKind.Ground && !taken.Contains(y * Size + x)
&& (Math.Abs(x - c) > 5 || Math.Abs(y - c) > 5);
int trees = 0, rocks = 0;
for (int attempt = 0; attempt < 4000 && trees < 90; attempt++)
{
int x = rng.Next(Size), y = rng.Next(Size);
if (!Free(x, y)) continue;
taken.Add(y * Size + x);
layout.Obstacles.Add(new Obstacle { Id = "tree_" + (++trees), IsTree = true, Cells = new List<(int, int)> { (x, y) } });
}
for (int attempt = 0; attempt < 4000 && rocks < 22; attempt++)
{
int x = rng.Next(Size - 1), y = rng.Next(Size - 1);
bool big = rng.Next(3) == 0; // каждый третий валун -- 2 x 2
var cells = big ? new List<(int, int)> { (x, y), (x + 1, y), (x, y + 1), (x + 1, y + 1) } : new List<(int, int)> { (x, y) };
bool ok = true;
foreach (var cell in cells) if (!Free(cell.Item1, cell.Item2)) { ok = false; break; }
if (!ok) continue;
foreach (var cell in cells) taken.Add(cell.Item2 * Size + cell.Item1);
layout.Obstacles.Add(new Obstacle { Id = "rock_" + (++rocks), IsTree = false, Cells = cells });
}
foreach (var o in layout.Obstacles) map.AddObstacle(o.Id, o.Cells);
// Трава -- только украшение: не мешает стройке, прячется под зданием и возвращается, когда здание ушло
for (int i = 0; i < 260; i++)
{
float x = (float)rng.NextDouble() * Size, z = (float)rng.NextDouble() * Size;
if (map.KindAt((int)x, (int)z) == CellKind.Ground) layout.Grass.Add(new Vector2(x, z));
}
return layout;
}
}