320 lines
14 KiB
C#
320 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using _Project.Scripts.PlayerUtils;
|
||
using UnityEngine;
|
||
|
||
namespace _Project.Scripts.LevelGenerating
|
||
{
|
||
public interface IRoomGenerator
|
||
{
|
||
bool Supports(Room room);
|
||
void Generate(RoomView view, FloorData floor, RoomTheme theme);
|
||
}
|
||
|
||
public class ThemedRoomGenerator : IRoomGenerator
|
||
{
|
||
public bool Supports(Room room) => true;
|
||
|
||
public void Generate(RoomView view, FloorData floor, RoomTheme theme)
|
||
{
|
||
if (theme == null)
|
||
{
|
||
Debug.LogWarning($"{view.name}: для комнаты не назначена тема");
|
||
return;
|
||
}
|
||
|
||
ApplyMaterial(view.FloorRenderers, theme.floorMaterial);
|
||
ApplyMaterial(view.WallRenderers, theme.wallMaterial);
|
||
ApplyMaterial(view.CeilingRenderers, theme.ceilingMaterial);
|
||
|
||
if (theme.lightConfig != null)
|
||
{
|
||
CreateLights(view, theme.lightConfig);
|
||
RuntimeRoomLightmapBaker.BakeRoom(view, theme.lightConfig);
|
||
}
|
||
|
||
if (theme.spawnEnemies)
|
||
{
|
||
SpawnEnemies(view, floor, theme.enemyConfig);
|
||
}
|
||
}
|
||
|
||
private static void ApplyMaterial(List<Renderer> renderers, Material mat)
|
||
{
|
||
if (mat == null) return;
|
||
foreach (var r in renderers) r.sharedMaterial = mat;
|
||
}
|
||
|
||
private static void CreateLights(RoomView view, RoomLightConfig config)
|
||
{
|
||
Vector3 size = view.SizeMeters;
|
||
Vector3 origin = view.OriginMeters;
|
||
|
||
float spacing = Mathf.Max(0.5f, config.spacing);
|
||
int nx = Mathf.Max(1, Mathf.CeilToInt(size.x / spacing));
|
||
int nz = Mathf.Max(1, Mathf.CeilToInt(size.z / spacing));
|
||
float y = view.Height - config.ceilingDrop;
|
||
|
||
var lightsRoot = new GameObject("Lights").transform;
|
||
lightsRoot.SetParent(view.transform, false);
|
||
|
||
Quaternion rot = Quaternion.Euler(config.rotationOffset);
|
||
|
||
for (int ix = 0; ix < nx; ix++)
|
||
{
|
||
for (int iz = 0; iz < nz; iz++)
|
||
{
|
||
Vector3 pos = origin + new Vector3(
|
||
(ix + 0.5f) / nx * size.x,
|
||
y,
|
||
(iz + 0.5f) / nz * size.z);
|
||
|
||
GameObject lightGo;
|
||
|
||
if (config.lightPrefab != null)
|
||
{
|
||
lightGo = Object.Instantiate(config.lightPrefab, pos, rot, lightsRoot);
|
||
lightGo.name = $"{config.lightPrefab.name}_{ix}_{iz}";
|
||
}
|
||
else
|
||
{
|
||
lightGo = new GameObject($"FallbackLight_{ix}_{iz}");
|
||
lightGo.transform.SetParent(lightsRoot, false);
|
||
lightGo.transform.SetPositionAndRotation(pos, rot);
|
||
var fallbackLight = lightGo.AddComponent<Light>();
|
||
fallbackLight.type = LightType.Point;
|
||
}
|
||
|
||
if (config.overrideLightParameters || config.lightPrefab == null)
|
||
{
|
||
var lights = lightGo.GetComponentsInChildren<Light>(true);
|
||
foreach (var l in lights)
|
||
{
|
||
l.color = config.color;
|
||
l.intensity = config.intensity;
|
||
l.range = config.range;
|
||
l.shadows = config.shadows;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- ГЕНЕРАЦИЯ ВРАГОВ В КОМНАТЕ ----------
|
||
|
||
private static void SpawnEnemies(RoomView view, FloorData floor, RoomEnemySpawnConfig config)
|
||
{
|
||
RoomType rt = view.Data.rt;
|
||
|
||
// В коридорах и в лифте врагов не спавним
|
||
if (rt == RoomType.Corridor || rt == RoomType.Lift || rt == RoomType.Undefined)
|
||
return;
|
||
|
||
Vector3 size = view.SizeMeters;
|
||
Vector3 origin = view.OriginMeters;
|
||
float area = size.x * size.z;
|
||
|
||
int count;
|
||
if (rt == RoomType.Start)
|
||
{
|
||
// В стартовой комнате ставим 1 врага (первый носитель, в которого игрок может сразу вселиться Духом)
|
||
count = 1;
|
||
}
|
||
else if (config != null)
|
||
{
|
||
int baseCount = Random.Range(config.minEnemies, config.maxEnemies + 1);
|
||
int areaBonus = config.extraEnemyPerArea > 0f ? Mathf.FloorToInt(area / config.extraEnemyPerArea) : 0;
|
||
count = baseCount + areaBonus;
|
||
}
|
||
else
|
||
{
|
||
// Авто-расчёт по площади комнаты и глубине (чем дальше от старта, тем больше охрана)
|
||
int depth = Mathf.Max(1, floor.GetDepth(view.Data));
|
||
int min = Mathf.Clamp(1 + depth / 2, 1, 3);
|
||
int max = Mathf.Clamp(2 + Mathf.FloorToInt(area / 30f), min, 6);
|
||
count = Random.Range(min, max + 1);
|
||
}
|
||
|
||
if (count <= 0) return;
|
||
|
||
var enemiesRoot = new GameObject("Enemies").transform;
|
||
enemiesRoot.SetParent(view.transform, false);
|
||
|
||
float margin = config != null ? config.wallMargin : 2f;
|
||
float minX = origin.x + Mathf.Min(margin, size.x * 0.35f);
|
||
float maxX = origin.x + size.x - Mathf.Min(margin, size.x * 0.35f);
|
||
float minZ = origin.z + Mathf.Min(margin, size.z * 0.35f);
|
||
float maxZ = origin.z + size.z - Mathf.Min(margin, size.z * 0.35f);
|
||
|
||
var spawnedPositions = new List<Vector3>();
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
BodyClass chosenClass = rt == RoomType.Start
|
||
? BodyClass.Trooper // На старте всегда даём сбалансированного Штурмовика (или Скаута)
|
||
: PickClass(rt, config);
|
||
|
||
Vector3 spawnPos = FindSpawnPosition(minX, maxX, minZ, maxZ, view.CenterMeters, rt == RoomType.Start, spawnedPositions);
|
||
spawnedPositions.Add(spawnPos);
|
||
|
||
GameObject prefab = ResolveEnemyPrefab(chosenClass, config);
|
||
EnemyAI enemy = CreateEnemyInstance(prefab, chosenClass, spawnPos, enemiesRoot, i);
|
||
|
||
view.Enemies.Add(enemy);
|
||
}
|
||
}
|
||
|
||
// Подбирает точку спавна в комнате так, чтобы враги не слипались друг с другом
|
||
// и не спавнились прямо на голове игрока в Start-комнате
|
||
private static Vector3 FindSpawnPosition(float minX, float maxX, float minZ, float maxZ,
|
||
Vector3 roomCenter, bool isStartRoom, List<Vector3> occupied)
|
||
{
|
||
for (int attempt = 0; attempt < 15; attempt++)
|
||
{
|
||
Vector3 candidate = new Vector3(
|
||
Random.Range(minX, maxX),
|
||
0.1f,
|
||
Random.Range(minZ, maxZ));
|
||
|
||
// В стартовой комнате игрок падает в центр, поэтому врага ставим чуть поодаль от центра
|
||
if (isStartRoom && Vector2.Distance(new Vector2(candidate.x, candidate.z), new Vector2(roomCenter.x, roomCenter.z)) < 2.5f)
|
||
continue;
|
||
|
||
bool tooClose = false;
|
||
foreach (var pos in occupied)
|
||
{
|
||
if ((candidate - pos).sqrMagnitude < 3.0f) // минимум ~1.7м между врагами
|
||
{
|
||
tooClose = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!tooClose) return candidate;
|
||
}
|
||
|
||
return new Vector3(Random.Range(minX, maxX), 0.1f, Random.Range(minZ, maxZ));
|
||
}
|
||
|
||
// Выбирает класс врага из конфига ИЛИ автоматически по смыслу комнаты (RoomType)
|
||
private static BodyClass PickClass(RoomType roomType, RoomEnemySpawnConfig config)
|
||
{
|
||
if (config != null && config.enemyPool != null && config.enemyPool.Count > 0)
|
||
{
|
||
float totalWeight = 0f;
|
||
foreach (var e in config.enemyPool) totalWeight += e.weight;
|
||
|
||
float roll = Random.Range(0f, totalWeight);
|
||
float cursor = 0f;
|
||
foreach (var e in config.enemyPool)
|
||
{
|
||
cursor += e.weight;
|
||
if (roll <= cursor) return e.enemyClass;
|
||
}
|
||
return config.enemyPool[0].enemyClass;
|
||
}
|
||
|
||
// Умное распределение классов по ролям комнат по умолчанию:
|
||
float r = Random.value;
|
||
switch (roomType)
|
||
{
|
||
case RoomType.Storage:
|
||
case RoomType.BoilerRoom:
|
||
case RoomType.SwitchboardRoom:
|
||
// Узкие технические комнаты: много Джаггернаутов (35%) и быстрых Скаутов (40%)
|
||
if (r < 0.35f) return BodyClass.Juggernaut;
|
||
if (r < 0.75f) return BodyClass.Scout;
|
||
return BodyClass.Trooper;
|
||
|
||
case RoomType.Penthouse:
|
||
case RoomType.Laboratory:
|
||
// Просторные/чистые залы: Снайперы (35%), Штурмовики (45%), Скауты (20%)
|
||
if (r < 0.35f) return BodyClass.Sniper;
|
||
if (r < 0.80f) return BodyClass.Trooper;
|
||
return BodyClass.Scout;
|
||
|
||
default: // Office и остальные
|
||
// Офисы: в основном Штурмовики (45%) и Скауты (30%), реже Снайпер (15%) и Джаггернаут (10%)
|
||
if (r < 0.45f) return BodyClass.Trooper;
|
||
if (r < 0.75f) return BodyClass.Scout;
|
||
if (r < 0.90f) return BodyClass.Sniper;
|
||
return BodyClass.Juggernaut;
|
||
}
|
||
}
|
||
|
||
private static GameObject ResolveEnemyPrefab(BodyClass cls, RoomEnemySpawnConfig config)
|
||
{
|
||
if (config == null) return null;
|
||
if (config.enemyPool != null)
|
||
{
|
||
foreach (var entry in config.enemyPool)
|
||
{
|
||
if (entry.enemyClass == cls && entry.customPrefab != null)
|
||
return entry.customPrefab;
|
||
}
|
||
}
|
||
return config.defaultEnemyPrefab;
|
||
}
|
||
|
||
// Спавнит префаб врага ИЛИ автоматически собирает боевую капсулу нужного размера и цвета, если префаб ещё не сделан!
|
||
private static EnemyAI CreateEnemyInstance(GameObject prefab, BodyClass cls, Vector3 pos, Transform parent, int index)
|
||
{
|
||
Quaternion rot = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
|
||
GameObject go;
|
||
|
||
if (prefab != null)
|
||
{
|
||
go = Object.Instantiate(prefab, pos, rot, parent);
|
||
}
|
||
else
|
||
{
|
||
// Авто-сборка капсулы врага (чтобы можно было играть сразу без готовых 3D-моделей!)
|
||
go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||
go.transform.SetParent(parent, false);
|
||
|
||
// Подгоняем размер и цвет капсулы под класс врага, чтобы игрок сразу различал их визуально!
|
||
Vector3 scale = Vector3.one;
|
||
Color classColor = Color.blue;
|
||
|
||
switch (cls)
|
||
{
|
||
case BodyClass.Scout:
|
||
scale = new Vector3(0.85f, 0.9f, 0.85f);
|
||
classColor = new Color(1f, 0.85f, 0.1f); // Жёлтый (быстрый)
|
||
break;
|
||
case BodyClass.Trooper:
|
||
scale = new Vector3(1f, 1f, 1f);
|
||
classColor = new Color(0.2f, 0.5f, 1f); // Синий (штурмовик)
|
||
break;
|
||
case BodyClass.Juggernaut:
|
||
scale = new Vector3(1.35f, 1.25f, 1.35f); // Крупный высокий танк
|
||
classColor = new Color(0.9f, 0.15f, 0.15f); // Красный
|
||
break;
|
||
case BodyClass.Sniper:
|
||
scale = new Vector3(0.9f, 1.05f, 0.9f);
|
||
classColor = new Color(0.75f, 0.2f, 1f); // Фиолетовый (снайпер)
|
||
break;
|
||
}
|
||
|
||
go.transform.localScale = scale;
|
||
// Поднимаем капсулу так, чтобы её низ стоял ровно на полу (Y = 0)
|
||
go.transform.SetPositionAndRotation(pos + Vector3.up * scale.y, rot);
|
||
|
||
var renderer = go.GetComponent<Renderer>();
|
||
if (renderer != null)
|
||
{
|
||
renderer.material.color = classColor;
|
||
}
|
||
}
|
||
|
||
go.name = $"Enemy_{index}_{cls}";
|
||
|
||
var ai = go.GetComponent<EnemyAI>();
|
||
if (ai == null) ai = go.AddComponent<EnemyAI>();
|
||
|
||
ai.enemyClass = cls;
|
||
ai.profile = BodyProfile.CreateDefault(cls);
|
||
|
||
return ai;
|
||
}
|
||
}
|
||
} |