Files
zeling_v2/Assets/Scripts/Quest/ChallengeRoomManager.cs
2026-05-13 09:19:54 +08:00

199 lines
7.0 KiB
C#
Raw 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;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Enemies;
using BaseGames.Player;
namespace BaseGames.Challenge
{
/// <summary>
/// 挑战房间流程管理器(架构 22_QuestChallengeModule §12
/// 挂在挑战房间场景的 [ChallengeManager] GameObject 上,场景加载时自动启动挑战。
/// </summary>
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<AsyncOperationHandle<GameObject>> _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<ISaveService>()?.QuickSave();
_isRunning = true;
_currentEncounterIndex = 0;
_elapsedTime = 0f;
_noHitViolated = false;
// 预加载所有敌人资源,全部缓存就绪后再开始生成第一波
PreloadEnemyAssets(() => SpawnWave(0));
}
/// <summary>
/// 预加载本次挑战所有敌人的 Addressable 资源。
/// 确保所有资源均已内存驻留后才调用 <paramref name="onComplete"/>.
/// 无敌人配置时直接回调。
/// </summary>
private void PreloadEnemyAssets(Action onComplete)
{
var keys = new HashSet<string>();
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 = Addressables.LoadAssetAsync<GameObject>(key);
_preloadHandles.Add(handle);
handle.Completed += _ =>
{
if (--remaining == 0)
onComplete?.Invoke();
};
}
}
/// <summary>释放预加载的所有资源句柄。</summary>
private void ReleasePreloadedAssets()
{
foreach (var h in _preloadHandles)
if (h.IsValid()) Addressables.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;
}
Addressables.InstantiateAsync(entry.enemyAddressKey, pos, Quaternion.identity)
.Completed += handle =>
{
if (handle.Result != null &&
handle.Result.TryGetComponent<EnemyBase>(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<ISaveService>() 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<ISaveService>()?.QuickLoad();
}
}
}