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 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(); fallbackLight.type = LightType.Point; } if (config.overrideLightParameters || config.lightPrefab == null) { var lights = lightGo.GetComponentsInChildren(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(); 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 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(); if (renderer != null) { renderer.material.color = classColor; } } go.name = $"Enemy_{index}_{cls}"; var ai = go.GetComponent(); if (ai == null) ai = go.AddComponent(); ai.enemyClass = cls; ai.profile = BodyProfile.CreateDefault(cls); return ai; } } }