Souljack/Assets/_Project/Scripts/LevelGenerating/RoomGenerators.cs

320 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}