using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using BaseGames.Core; using BaseGames.Core.Assets; using BaseGames.Core.Events; using BaseGames.Enemies; using BaseGames.Player; namespace BaseGames.Challenge { /// /// 挑战房间流程管理器(架构 22_QuestChallengeModule §12)。 /// 挂在挑战房间场景的 [ChallengeManager] GameObject 上,场景加载时自动启动挑战。 /// public class ChallengeRoomManager : MonoBehaviour { [SerializeField] private ChallengeRoomSO _challengeData; [SerializeField] private PlayerStats _player; // 由场景 Inspector 绑定 [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onChallengeCompleted; // EVT_ChallengeCompleted [SerializeField] private StringEventChannelSO _onChallengeFailed; // EVT_ChallengeFailed private int _currentEncounterIndex; private int _remainingEnemies; private float _elapsedTime; private bool _isRunning; private bool _noHitViolated; // 预加载句柄(挑战开始前预热全部敌人资源,结束时释放) private readonly List> _preloadHandles = new(); private void OnEnable() { if (_player != null) _player.OnDamaged += OnPlayerDamaged; } private void OnDisable() { if (_player != null) _player.OnDamaged -= OnPlayerDamaged; ReleasePreloadedAssets(); } private void OnPlayerDamaged() => _noHitViolated = true; private void Start() => StartChallenge(); private void Update() { if (!_isRunning) return; _elapsedTime += Time.deltaTime; if (_challengeData.timeLimit > 0 && _elapsedTime >= _challengeData.timeLimit) FailChallenge(); } private void StartChallenge() { // 自动快速存档(失败后读档返回挑战入口) ServiceLocator.GetOrDefault()?.QuickSave(); _isRunning = true; _currentEncounterIndex = 0; _elapsedTime = 0f; _noHitViolated = false; // 预加载所有敌人资源,全部缓存就绪后再开始生成第一波 PreloadEnemyAssets(() => SpawnWave(0)); } /// /// 预加载本次挑战所有敌人的 Addressable 资源。 /// 确保所有资源均已内存驻留后才调用 . /// 无敌人配置时直接回调。 /// private void PreloadEnemyAssets(Action onComplete) { var keys = new HashSet(); if (_challengeData.encounters != null) foreach (var enc in _challengeData.encounters) if (enc.enemies != null) foreach (var entry in enc.enemies) if (!string.IsNullOrEmpty(entry.enemyAddressKey)) keys.Add(entry.enemyAddressKey); if (keys.Count == 0) { onComplete?.Invoke(); return; } int remaining = keys.Count; foreach (var key in keys) { var handle = AssetLoader.LoadHandle(key); _preloadHandles.Add(handle); handle.Completed += _ => { if (--remaining == 0) onComplete?.Invoke(); }; } } /// 释放预加载的所有资源句柄。 private void ReleasePreloadedAssets() { foreach (var h in _preloadHandles) if (h.IsValid()) AssetLoader.Release(h); _preloadHandles.Clear(); } private void SpawnWave(int index) { if (_challengeData.encounters == null || index >= _challengeData.encounters.Length) { CompleteChallenge(); return; } var enc = _challengeData.encounters[index]; _remainingEnemies = 0; foreach (var entry in enc.enemies) { for (int i = 0; i < entry.count; i++) { _remainingEnemies++; Vector3 pos; if (entry.spawnPoint != null) { pos = entry.spawnPoint.position; } else { Debug.LogWarning($"[ChallengeRoomManager] encounter[{index}] 中的 enemyAddressKey='{entry.enemyAddressKey}' 未配置 spawnPoint,将在 Vector3.zero 生成。请在 ChallengeRoomSO 中补全配置。", this); pos = Vector3.zero; } AssetLoader.InstantiateAsync(entry.enemyAddressKey, pos, Quaternion.identity) .Completed += handle => { if (handle.Result != null && handle.Result.TryGetComponent(out var enemy)) { enemy.OnDied += OnEnemyDefeated; } }; } } } private void OnEnemyDefeated() { _remainingEnemies = Mathf.Max(0, _remainingEnemies - 1); if (_remainingEnemies > 0) return; _currentEncounterIndex++; if (_currentEncounterIndex >= _challengeData.encounters.Length) CompleteChallenge(); else StartCoroutine(DelayedNextWave(_challengeData.encounters[_currentEncounterIndex].waveDelay)); } private IEnumerator DelayedNextWave(float delay) { yield return new WaitForSeconds(delay); SpawnWave(_currentEncounterIndex); } private void CompleteChallenge() { _isRunning = false; // requireNoHit 挑战:受到伤害则判定失败(架构 §12) if (_challengeData.requireNoHit && _noHitViolated) { FailChallenge(); return; } var reward = ServiceLocator.GetOrDefault() is { } sm && sm.IsFirstClear(_challengeData.challengeId) ? _challengeData.firstClearReward : _challengeData.repeatedReward; reward?.Apply(_player); _onChallengeCompleted?.Raise(_challengeData.challengeId); } private void FailChallenge() { _isRunning = false; _onChallengeFailed?.Raise(_challengeData.challengeId); ServiceLocator.GetOrDefault()?.QuickLoad(); } } }