200 lines
7.1 KiB
C#
200 lines
7.1 KiB
C#
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
|
|
{
|
|
/// <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 = AssetLoader.LoadHandle<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()) 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<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();
|
|
}
|
|
}
|
|
}
|