2025-09-19 19:43:49 +05:00
|
|
|
|
using BulletHellTemplate.Core.Events;
|
|
|
|
|
using BulletHellTemplate.VFX;
|
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
|
using System;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
using System.Collections.Generic;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
using System.Threading;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
using UnityEngine;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
using Fusion;
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
namespace BulletHellTemplate
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Manages the gameplay, including the survival timer, spawning waves of monsters, handling perk levels, and pooling system.
|
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
///
|
2025-09-19 14:56:58 +05:00
|
|
|
|
public class GameplayManager : MonoBehaviour
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
// The selected win condition for this game
|
|
|
|
|
[Header("Revive")]
|
|
|
|
|
public int reviveSeconds = 20;
|
|
|
|
|
public int reviveLimit = 1;
|
|
|
|
|
[Header("Game Mode")]
|
|
|
|
|
public WinCondition winCondition;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
public UpgradeMode upgradeMode = UpgradeMode.UpgradeOnLevelUp;
|
|
|
|
|
public MonsterEntity bossPrefab; // The boss to spawn in KillBoss or SurvivalTimeAndKillBoss modes
|
|
|
|
|
public int survivalTime = 600; // Survival time in seconds (10 minutes)
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public List<Wave> waves; // List of configured waves
|
|
|
|
|
|
|
|
|
|
[Header("Game EXP settings")]
|
|
|
|
|
public int[] xpToNextLevel;
|
|
|
|
|
public int maxLevel;
|
|
|
|
|
public int minDamage = 5;
|
|
|
|
|
public TimeFormat timeDisplayFormat = TimeFormat.Seconds;
|
|
|
|
|
|
|
|
|
|
[Header("Perks Settings")]
|
|
|
|
|
public int maxLevelStatPerks = 5;
|
|
|
|
|
public int maxLevelSkillPerks = 5;
|
|
|
|
|
public int maxSkills = 5;
|
|
|
|
|
public int maxStats = 5;
|
|
|
|
|
public SkillPerkData[] skillPerkData;
|
|
|
|
|
public StatPerkData[] statPerkData;
|
|
|
|
|
|
|
|
|
|
[Header("Monster Spawn")]
|
|
|
|
|
public List<SpawnPoints> spawnPointsList;
|
|
|
|
|
private CharacterEntity character;
|
|
|
|
|
|
|
|
|
|
[Header("PVP Spawn")]
|
|
|
|
|
public Transform[] teamSpawnSlots;
|
|
|
|
|
[Range(0f, 3f)] public float teamSpawnSpreadRadius = 1.5f;
|
|
|
|
|
|
|
|
|
|
[Header("Pool Settings (not recommended for mobile)")]
|
|
|
|
|
public bool PreloadAllMonsters;
|
|
|
|
|
|
|
|
|
|
//(PVP WIP)
|
|
|
|
|
private bool isPvp = false;
|
|
|
|
|
public bool IsPvp => isPvp;
|
|
|
|
|
|
|
|
|
|
private Dictionary<object, int> perkLevels = new Dictionary<object, int>();
|
|
|
|
|
private Dictionary<SkillPerkData, int> skillLevels = new Dictionary<SkillPerkData, int>();
|
|
|
|
|
private Dictionary<SkillData, int> skillBaseLevels = new Dictionary<SkillData, int>();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
private int currentWaveIndex = 0; // Index of the current wave
|
|
|
|
|
private float timeRemaining; // Time remaining for the next wave or win condition
|
|
|
|
|
private bool gameRunning = true; // Control the game state
|
|
|
|
|
private bool isPaused = false; // Tracks if the game is paused
|
|
|
|
|
private float waveStartTime; // Time when the current wave started
|
|
|
|
|
private int monstersKilled; // Tracks the number of monsters killed
|
|
|
|
|
private int goldGain; // Tracks the total gold gained
|
|
|
|
|
private int xpGain; // Tracks the total XP gained
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public static GameplayManager Singleton { get; private set; }
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public readonly HashSet<Transform> ActiveCharactersList = new();
|
|
|
|
|
public readonly List<Transform> ActiveMonstersList = new();
|
|
|
|
|
public readonly List<Transform> ActiveBoxesList = new();
|
|
|
|
|
private readonly HashSet<MonsterEntity> _finalBosses = new();
|
|
|
|
|
private bool _bossSeenAliveOnce = false;
|
|
|
|
|
private readonly HashSet<CharacterEntity> _knownCharacters = new();
|
|
|
|
|
private bool _rosterReady = false;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
//PVP
|
|
|
|
|
private readonly Dictionary<int, int> _teamScore = new();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stores currently alive player characters for wipe checks.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private readonly HashSet<CharacterEntity> _aliveCharacters = new HashSet<CharacterEntity>();
|
|
|
|
|
private readonly Dictionary<CharacterEntity, CancellationTokenSource> _reviveTimers = new();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// True when the Fusion runner is up (network match).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsRunnerActive =>
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance && GameplaySync.Instance.RunnerActive;
|
|
|
|
|
#else
|
|
|
|
|
false;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// True only on the Shared‑Mode master (the one allowed to decide team defeat).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsLeader =>
|
|
|
|
|
#if FUSION2
|
|
|
|
|
IsRunnerActive && GameplaySync.Instance && GameplaySync.Instance.HasStateAuthority;
|
|
|
|
|
#else
|
|
|
|
|
true;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
private bool matchFinished = false;
|
|
|
|
|
|
|
|
|
|
//Cancel Tokens
|
|
|
|
|
private CancellationTokenSource _survivalCts;
|
|
|
|
|
private CancellationTokenSource _waveCts;
|
|
|
|
|
private CancellationTokenSource _bossCheckCts;
|
|
|
|
|
/* ─────────────────────────── Helpers ─────────────────────────── */
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns true when this client is the Shared‑Mode Master Client
|
|
|
|
|
/// otherwise, false.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsHost()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
NetworkRunner runner = NetworkRunner.GetRunnerForGameObject(gameObject);
|
|
|
|
|
|
|
|
|
|
#if UNITY_6000_0_OR_NEWER
|
|
|
|
|
if (runner == null)
|
|
|
|
|
runner = FindFirstObjectByType<NetworkRunner>();
|
|
|
|
|
#else
|
|
|
|
|
if (runner == null)
|
|
|
|
|
runner = FindObjectOfType<NetworkRunner>();
|
|
|
|
|
#endif
|
|
|
|
|
if (runner == null)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
var masterRef = SharedModeMasterClientTracker.GetSharedModeMasterClientPlayerRef();
|
|
|
|
|
if (masterRef.HasValue)
|
|
|
|
|
return masterRef.Value == runner.LocalPlayer;
|
|
|
|
|
|
|
|
|
|
return runner.IsSharedModeMasterClient;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#endif
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/* ─────────────────────────── Life‑cycle ───────────────────────── */
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
|
|
|
|
if (Singleton == null)
|
|
|
|
|
{
|
|
|
|
|
Singleton = this;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
}
|
|
|
|
|
perkLevels = new Dictionary<object, int>();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private void OnEnable()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private void OnDisable()
|
|
|
|
|
{
|
|
|
|
|
EventBus.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void Start()
|
|
|
|
|
{
|
|
|
|
|
timeRemaining = survivalTime;
|
|
|
|
|
if (IsRunnerActive) upgradeMode = UpgradeMode.UpgradeOnButtonClick;
|
|
|
|
|
#if FUSION2
|
|
|
|
|
bool netRunning = GameplaySync.Instance && GameplaySync.Instance.RunnerActive;
|
|
|
|
|
|
|
|
|
|
if (IsHost() && PreloadAllMonsters)
|
|
|
|
|
await MonsterPool.Instance.PreloadAsync(waves);
|
|
|
|
|
|
|
|
|
|
if (!netRunning)
|
|
|
|
|
StartGameplay();
|
|
|
|
|
#else
|
|
|
|
|
await Task.Yield();
|
|
|
|
|
StartGameplay();
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Asynchronously sets up the CharacterEntity and related gameplay settings.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <param name="characterOwner">The character to configure.</param>
|
|
|
|
|
/// <returns>A UniTask representing the async operation.</returns>
|
|
|
|
|
public async UniTask SetupCharacterEntity(CharacterEntity characterOwner)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
character = characterOwner;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
if (character.GetCharacterData().autoAttack != null)
|
|
|
|
|
{
|
|
|
|
|
var aaSkill = character.GetCharacterData().autoAttack;
|
|
|
|
|
if (!skillBaseLevels.ContainsKey(aaSkill))
|
|
|
|
|
{
|
|
|
|
|
skillBaseLevels[aaSkill] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var skill in character.GetCharacterData().skills)
|
|
|
|
|
{
|
|
|
|
|
if (!skillBaseLevels.ContainsKey(skill))
|
|
|
|
|
{
|
|
|
|
|
skillBaseLevels[skill] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxSkills = (int)character.GetCurrentMaxSkills();
|
|
|
|
|
maxStats = (int)character.GetCurrentMaxStats();
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
// Update gameplay UI
|
2025-09-19 14:56:58 +05:00
|
|
|
|
UIGameplay.Singleton.UpdateSkillPerkUI();
|
|
|
|
|
UIGameplay.Singleton.UpdateStatPerkUI();
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
await UniTask.Yield();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void RegisterCharacter(CharacterEntity ch)
|
|
|
|
|
{
|
|
|
|
|
if (!IsRunnerActive || IsLeader)
|
|
|
|
|
ActiveCharactersList.Add(ch.transform);
|
|
|
|
|
|
|
|
|
|
RegisterAlive(ch);
|
|
|
|
|
_knownCharacters.Add(ch);
|
|
|
|
|
RecomputeRosterReady();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void UnregisterAlive(CharacterEntity ch)
|
|
|
|
|
{
|
|
|
|
|
_aliveCharacters.Remove(ch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RecomputeRosterReady()
|
|
|
|
|
{
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (!IsRunnerActive || !IsLeader) { _rosterReady = false; return; }
|
|
|
|
|
var runner = NetworkRunner.GetRunnerForGameObject(gameObject);
|
|
|
|
|
#if UNITY_6000_0_OR_NEWER
|
|
|
|
|
if (runner == null) runner = FindFirstObjectByType<NetworkRunner>();
|
|
|
|
|
#else
|
|
|
|
|
if (runner == null) runner = FindObjectOfType<NetworkRunner>();
|
|
|
|
|
#endif
|
|
|
|
|
if (runner == null) { _rosterReady = false; return; }
|
|
|
|
|
int expected = runner.ActivePlayers.Count();
|
|
|
|
|
int seen = _knownCharacters.Count;
|
|
|
|
|
_rosterReady = seen >= expected;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override string ToString() => base.ToString();
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
public CharacterEntity GetCharacterEntity()
|
|
|
|
|
{
|
|
|
|
|
return character;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void SetTimeRemaining(int seconds)
|
|
|
|
|
{
|
|
|
|
|
timeRemaining = seconds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int GetSurvivalTime()
|
|
|
|
|
{
|
|
|
|
|
return survivalTime;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Starts the gameplay by initiating the survival countdown and spawning waves.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void StartGameplay()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (IsRunnerActive && !IsLeader)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
StartSurvivalCountdown();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_waveCts?.Cancel();
|
|
|
|
|
_waveCts = new CancellationTokenSource();
|
|
|
|
|
SpawnWavesAsync(_waveCts.Token).Forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void StartSurvivalCountdown()
|
|
|
|
|
{
|
|
|
|
|
if (IsRunnerActive && !IsLeader)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
_survivalCts?.Cancel();
|
|
|
|
|
_survivalCts = new CancellationTokenSource();
|
|
|
|
|
CountdownSurvivalAsync(_survivalCts.Token).Forget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async UniTaskVoid CountdownSurvivalAsync(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
if (IsRunnerActive && !IsLeader) return;
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
EventBus.Publish(new GameTimerTickEvent((int)timeRemaining));
|
|
|
|
|
|
|
|
|
|
while (timeRemaining > 0 && gameRunning && !token.IsCancellationRequested)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (isPaused)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.Delay(1000,
|
|
|
|
|
cancellationToken: token,
|
|
|
|
|
delayTiming: PlayerLoopTiming.Update);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (isPaused || token.IsCancellationRequested) continue;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
timeRemaining--;
|
|
|
|
|
EventBus.Publish(new GameTimerTickEvent((int)timeRemaining));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (!token.IsCancellationRequested &&
|
|
|
|
|
gameRunning &&
|
|
|
|
|
(winCondition == WinCondition.SurvivalTime ||
|
|
|
|
|
winCondition == WinCondition.SurvivalTimeAndKillBoss))
|
|
|
|
|
{
|
|
|
|
|
HandleWinCondition();
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Registers this character as alive (called on spawn/enable/revive).
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void RegisterAlive(CharacterEntity ch)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_aliveCharacters.Add(ch);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Marks a character as dead and checks for a full team wipe.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void MarkCharacterDead(CharacterEntity ch)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_aliveCharacters.Remove(ch);
|
|
|
|
|
if (!IsRunnerActive || IsLeader)
|
|
|
|
|
ActiveCharactersList.Remove(ch.transform);
|
|
|
|
|
|
|
|
|
|
StartReviveCountdownIfOnline(ch);
|
|
|
|
|
TryCheckTeamWipe();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Called when a character revives; puts it back into the alive set.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void MarkCharacterRevived(CharacterEntity ch)
|
|
|
|
|
{
|
|
|
|
|
_aliveCharacters.Add(ch);
|
|
|
|
|
if (!IsRunnerActive || IsLeader)
|
|
|
|
|
ActiveCharactersList.Add(ch.transform);
|
|
|
|
|
|
|
|
|
|
if (matchFinished) return;
|
|
|
|
|
|
|
|
|
|
UIGameplay.Singleton?.HideEndGameScreen();
|
|
|
|
|
ResumeGame();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StartReviveCountdownIfOnline(CharacterEntity ch)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#if FUSION2
|
|
|
|
|
if (PvpSync.IsSpawnedReady && PvpSync.Instance.Mode == PVP.PvpModeType.BattleRoyale)
|
|
|
|
|
return;
|
|
|
|
|
#endif
|
|
|
|
|
if (!IsRunnerActive || !IsLeader) return;
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if(PvpSync.IsSpawnedReady && GameplayManager.Singleton.isPvp) reviveSeconds = (int)PvpSync.Instance.ReviveDelay;
|
|
|
|
|
GameplaySync.Instance?.NotifyStartReviveCountdown(ch, reviveSeconds);
|
|
|
|
|
#endif
|
|
|
|
|
_reviveTimers.TryGetValue(ch, out var old);
|
|
|
|
|
old?.Cancel();
|
|
|
|
|
var cts = new CancellationTokenSource();
|
|
|
|
|
_reviveTimers[ch] = cts;
|
|
|
|
|
ReviveCountdownAsync(ch, reviveSeconds, cts.Token).Forget();
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
//PVP
|
|
|
|
|
public void SetPvpSession(bool enabled)
|
|
|
|
|
{
|
|
|
|
|
isPvp = enabled;
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
foreach (var ch in _knownCharacters)
|
|
|
|
|
if (ch) ch.RefreshTeamObjects();
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if (enabled)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_survivalCts?.Cancel();
|
|
|
|
|
_waveCts?.Cancel();
|
|
|
|
|
_bossCheckCts?.Cancel();
|
|
|
|
|
|
|
|
|
|
if (UIGameplay.Singleton)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (UIGameplay.Singleton.bossHpContainer) UIGameplay.Singleton.bossHpContainer.SetActive(false);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public Vector3 GetTeamSpawnPosition(byte team)
|
|
|
|
|
{
|
|
|
|
|
var t = GetTeamSpawnTransform(team);
|
|
|
|
|
if (!t) return Vector3.zero;
|
|
|
|
|
|
|
|
|
|
var rnd = UnityEngine.Random.insideUnitCircle * teamSpawnSpreadRadius;
|
|
|
|
|
return t.position + new Vector3(rnd.x, 0, rnd.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Quaternion GetTeamSpawnRotation(byte team)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
var t = GetTeamSpawnTransform(team);
|
|
|
|
|
return t ? t.rotation : Quaternion.identity;
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private Transform GetTeamSpawnTransform(byte team)
|
|
|
|
|
{
|
|
|
|
|
if (teamSpawnSlots == null || teamSpawnSlots.Length == 0) return null;
|
|
|
|
|
int idx = Mathf.Abs(team) % teamSpawnSlots.Length;
|
|
|
|
|
return teamSpawnSlots[idx];
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public int GetTeamScore(int team)
|
|
|
|
|
{
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (IsRunnerActive && PvpSync.Instance)
|
|
|
|
|
return PvpSync.Instance.GetTeamScore((byte)team);
|
|
|
|
|
#endif
|
|
|
|
|
return _teamScore.TryGetValue(team, out var s) ? s : 0;
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void SetTeamScore(int team, int value)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#if FUSION2
|
|
|
|
|
if (IsRunnerActive && PvpSync.Instance)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
PvpSync.Instance.SetTeamScore((byte)team, value);
|
|
|
|
|
return;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#endif
|
|
|
|
|
_teamScore[team] = value;
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void AddTeamScore(int team, int delta)
|
|
|
|
|
{
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (IsRunnerActive && PvpSync.Instance)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
PvpSync.Instance.AddTeamScore((byte)team, delta);
|
|
|
|
|
return;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#endif
|
|
|
|
|
if (!_teamScore.ContainsKey(team)) _teamScore[team] = 0;
|
|
|
|
|
_teamScore[team] += delta;
|
|
|
|
|
// EventBus.Publish(new TeamScoreChangedEvent(team, _teamScore[team]));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Called by the leader (host) when the last alive player dies.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void TryCheckTeamWipe()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (matchFinished) return;
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
if (!IsRunnerActive)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (_aliveCharacters.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
if (reviveLimit > 0)
|
|
|
|
|
LocalDefeatWithRevive();
|
|
|
|
|
else
|
|
|
|
|
EndGame();
|
|
|
|
|
}
|
|
|
|
|
return;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (!IsLeader) return;
|
|
|
|
|
|
|
|
|
|
if (!_rosterReady)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (_aliveCharacters.Count == 0)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncTeamDefeat();
|
|
|
|
|
#endif
|
|
|
|
|
CancelAllRevives();
|
|
|
|
|
EndGame();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Offline-only defeat flow: show end screen with revive option.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void LocalDefeatWithRevive()
|
|
|
|
|
{
|
|
|
|
|
if (matchFinished) return;
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
gameRunning = false;
|
|
|
|
|
PauseGame();
|
|
|
|
|
UIGameplay.Singleton.DisplayEndGameScreen(false); // reviveButton
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetMatchFinished()
|
|
|
|
|
{
|
|
|
|
|
matchFinished = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async UniTaskVoid SpawnWavesAsync(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
if (IsRunnerActive && !IsLeader)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (isPvp) return;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
currentWaveIndex = 0;
|
|
|
|
|
|
|
|
|
|
while (currentWaveIndex < waves.Count && gameRunning && !token.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Wave wave = waves[currentWaveIndex];
|
|
|
|
|
float waveStart = Time.time;
|
|
|
|
|
|
|
|
|
|
var nextSpawn = new float[wave.monsters.Count];
|
|
|
|
|
var remain = new int[wave.monsters.Count];
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < wave.monsters.Count; ++i)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
nextSpawn[i] = waveStart + wave.monsters[i].spawnInterval;
|
|
|
|
|
remain[i] = Mathf.FloorToInt(wave.waveDuration / wave.monsters[i].spawnInterval);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
while ((Time.time - waveStart) < wave.waveDuration &&
|
|
|
|
|
!token.IsCancellationRequested &&
|
|
|
|
|
Array.Exists(remain, r => r > 0) &&
|
|
|
|
|
gameRunning)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
|
|
|
|
if (!isPaused)
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
float t = Time.time;
|
|
|
|
|
for (int i = 0; i < wave.monsters.Count; ++i)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (t >= nextSpawn[i] && remain[i] > 0)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
var cfg = wave.monsters[i];
|
|
|
|
|
await UniTask.FromResult(SpawnMonster(cfg.monsterPrefab, cfg.goldPerMonster, cfg.xpPerMonster));
|
|
|
|
|
nextSpawn[i] = t + cfg.spawnInterval;
|
|
|
|
|
remain[i]--;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentWaveIndex++;
|
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (gameRunning && winCondition != WinCondition.SurvivalTime)
|
|
|
|
|
HandleWinCondition();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles the win condition logic based on the selected game mode.
|
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private async void HandleWinCondition()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (IsRunnerActive && !IsLeader)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
if (winCondition == WinCondition.SurvivalTime)
|
|
|
|
|
{
|
|
|
|
|
EndGame();
|
|
|
|
|
}
|
|
|
|
|
else if (winCondition == WinCondition.KillBoss)
|
|
|
|
|
{
|
|
|
|
|
// Spawn the boss immediately and show the final boss message
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.FromResult(SpawnMonster(bossPrefab, 0, 0));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
UIGameplay.Singleton.ShowFinalBossMessage(); // Show the final boss message
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_bossCheckCts?.Cancel();
|
|
|
|
|
_bossCheckCts = new CancellationTokenSource();
|
|
|
|
|
CheckForBossDefeatAsync(_bossCheckCts.Token).Forget();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
else if (winCondition == WinCondition.SurvivalTimeAndKillBoss)
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_waveCts?.Cancel();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
if (timeRemaining <= 0)
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.FromResult(SpawnMonster(bossPrefab, 0, 0));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
UIGameplay.Singleton.ShowFinalBossMessage(); // Show the final boss message
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_bossCheckCts?.Cancel();
|
|
|
|
|
_bossCheckCts = new CancellationTokenSource();
|
|
|
|
|
CheckForBossDefeatAsync(_bossCheckCts.Token).Forget();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
private async UniTaskVoid CheckForBossDefeatAsync(CancellationToken token)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (IsRunnerActive && !IsLeader) return;
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
float guardTimeout = 5f;
|
|
|
|
|
float start = Time.time;
|
|
|
|
|
while (!_bossSeenAliveOnce && !token.IsCancellationRequested && (Time.time - start) < guardTimeout)
|
|
|
|
|
{
|
|
|
|
|
#if UNITY_6000_0_OR_NEWER
|
|
|
|
|
var bosses = FindObjectsByType<MonsterEntity>(FindObjectsSortMode.None);
|
|
|
|
|
#else
|
|
|
|
|
var bosses = FindObjectsOfType<MonsterEntity>();
|
|
|
|
|
#endif
|
|
|
|
|
foreach (var b in bosses)
|
|
|
|
|
{
|
|
|
|
|
if (b && b.IsFinalBoss)
|
|
|
|
|
{
|
|
|
|
|
_finalBosses.Add(b);
|
|
|
|
|
if (b.gameObject.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
_bossSeenAliveOnce = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (!token.IsCancellationRequested)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_finalBosses.RemoveWhere(b => !b || !b.gameObject);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
bool anyAlive = false;
|
|
|
|
|
foreach (var b in _finalBosses)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (b && b.gameObject.activeInHierarchy)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
anyAlive = true;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (!anyAlive)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
|
|
|
|
EndGame();
|
2025-09-19 19:43:49 +05:00
|
|
|
|
break;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private async UniTaskVoid SpawnMonster(MonsterEntity prefab, int gold, int xp)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (isPvp) return;
|
|
|
|
|
Vector3 pos = GetRandomSpawnPoint();
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (!IsHost()) return;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
GameObject go = await MonsterPool.Instance.Spawn(
|
|
|
|
|
prefab.gameObject,
|
|
|
|
|
pos,
|
|
|
|
|
Quaternion.identity);
|
|
|
|
|
|
|
|
|
|
var m = go.GetComponent<MonsterEntity>();
|
|
|
|
|
m.ConfigureMonster(gold, xp);
|
|
|
|
|
if (m.IsFinalBoss)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_finalBosses.Add(m);
|
|
|
|
|
if (m.gameObject.activeInHierarchy) _bossSeenAliveOnce = true;
|
|
|
|
|
UIGameplay.Singleton?.SetFinalBoss(m);
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (IsRunnerActive && IsLeader)
|
|
|
|
|
GameplaySync.Instance?.SyncBossSpawned(m);
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncConfigureMonster(
|
|
|
|
|
go.GetComponent<NetworkObject>(),
|
|
|
|
|
gold, xp);
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets a random spawn point from the available spawn points and returns a random position within that point's radius.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A random Vector3 position from a random spawn point in the list.</returns>
|
|
|
|
|
Vector3 GetRandomSpawnPoint()
|
|
|
|
|
{
|
|
|
|
|
if (spawnPointsList == null || spawnPointsList.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("No spawn points available.");
|
|
|
|
|
return Vector3.zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Select a random spawn point from the list
|
2025-09-19 19:43:49 +05:00
|
|
|
|
int randomIndex = UnityEngine.Random.Range(0, spawnPointsList.Count);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
SpawnPoints selectedSpawnPoint = spawnPointsList[randomIndex];
|
|
|
|
|
|
|
|
|
|
// Get a random position from the selected spawn point
|
|
|
|
|
return selectedSpawnPoint.GetRandomSpawnPoint();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Ends the current game: pauses, waits a delay, syncs network end and shows end screen.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async void EndGame()
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_finalBosses.Clear();
|
|
|
|
|
_bossSeenAliveOnce = false;
|
|
|
|
|
matchFinished = true;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
gameRunning = false;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (IsRunnerActive && IsLeader)
|
|
|
|
|
GameplaySync.Instance?.SyncEndGame();
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
await UniTask.Delay(TimeSpan.FromSeconds(1.5));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
PauseGame();
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (!IsPvp)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
MonsterPool.Instance?.DestroyPool();
|
|
|
|
|
ActiveMonstersList.Clear();
|
|
|
|
|
ActiveBoxesList.Clear();
|
|
|
|
|
await GameManager.Singleton.EndGameAsync(true);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Toggles the pause state of the game, stopping or resuming waves accordingly.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void TogglePause()
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
#if FUSION2
|
|
|
|
|
if (character.IsNetworked) return;
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
isPaused = !isPaused;
|
|
|
|
|
// The SpawnWaves coroutine handles the pause internally, so no need to stop/restart it here.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Explicitly pauses the game, intended to be used for direct calls when pausing is needed without toggling.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void PauseGame()
|
|
|
|
|
{
|
|
|
|
|
if (!isPaused) // Only pause if not already paused.
|
|
|
|
|
{
|
|
|
|
|
isPaused = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Explicitly resumes the game, intended to be used for direct calls when resuming is needed without toggling.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ResumeGame()
|
|
|
|
|
{
|
|
|
|
|
if (isPaused) // Only resume if the game is currently paused.
|
|
|
|
|
{
|
|
|
|
|
isPaused = false;
|
|
|
|
|
// The SpawnWaves coroutine will automatically resume respecting the isPaused state.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Checks if the game is currently paused.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>True if the game is paused; otherwise, false.</returns>
|
|
|
|
|
public bool IsPaused()
|
|
|
|
|
{
|
|
|
|
|
return isPaused;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int GetMonstersKilled()
|
|
|
|
|
{
|
|
|
|
|
return monstersKilled;
|
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public int GetGainGold()
|
|
|
|
|
{
|
|
|
|
|
return goldGain;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void ForceSetGold(int value)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
goldGain = value;
|
|
|
|
|
EventBus.Publish(new GoldChangedEvent(goldGain));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void ForceSetXP(int value)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
character.AddXP(value);
|
|
|
|
|
xpGain = value;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void ForceSetMonstersKilled(int value)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
monstersKilled = value;
|
|
|
|
|
EventBus.Publish(new MonstersKilledChangedEvent(monstersKilled));
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/* ───── MONSTERS KILLED ────────────────────────── */
|
|
|
|
|
|
|
|
|
|
public void IncrementMonstersKilled(int delta = 1)
|
|
|
|
|
{
|
|
|
|
|
monstersKilled += delta;
|
|
|
|
|
EventBus.Publish(new MonstersKilledChangedEvent(monstersKilled));
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncMonsters(monstersKilled);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ───── GOLD ───────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
public void IncrementGainGold(int delta)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
goldGain += delta;
|
|
|
|
|
EventBus.Publish(new GoldChangedEvent(goldGain));
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncGold(goldGain);
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/* ───── XP ─────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
public void IncrementGainXP(int delta)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
xpGain += delta;
|
|
|
|
|
character.AddXP(delta);
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncXP(xpGain);
|
|
|
|
|
#endif
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves a list of available perks, filtering out any skill levels marked as evolved that the player cannot access yet.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>A list of perks available for the player to choose from.</returns>
|
|
|
|
|
public List<object> GetRandomPerks()
|
|
|
|
|
{
|
|
|
|
|
List<object> availablePerks = new List<object>();
|
|
|
|
|
|
|
|
|
|
foreach (var skill in character.GetCharacterData().skills)
|
|
|
|
|
{
|
|
|
|
|
int currentLevel = GetBaseSkillLevel(skill);
|
|
|
|
|
int maxLevelIndex = skill.skillLevels.Count - 1;
|
|
|
|
|
int nextLevel = currentLevel + 1;
|
|
|
|
|
|
|
|
|
|
if (nextLevel <= maxLevelIndex)
|
|
|
|
|
{
|
|
|
|
|
bool isNextLevelEvolved = skill.skillLevels[nextLevel].isEvolved;
|
|
|
|
|
bool hasRequiredStatPerk = skill.requireStatForEvolve == null || character.HasStatPerk(skill.requireStatForEvolve);
|
|
|
|
|
|
|
|
|
|
if (!isNextLevelEvolved || hasRequiredStatPerk)
|
|
|
|
|
{
|
|
|
|
|
availablePerks.Add(skill);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (character.GetCharacterData().autoAttack != null)
|
|
|
|
|
{
|
|
|
|
|
SkillData aaSkill = character.GetCharacterData().autoAttack;
|
|
|
|
|
int currentLevel = GetBaseSkillLevel(aaSkill);
|
|
|
|
|
int maxLevelIndex = aaSkill.skillLevels.Count - 1;
|
|
|
|
|
int nextLevel = currentLevel + 1;
|
|
|
|
|
|
|
|
|
|
if (nextLevel <= maxLevelIndex)
|
|
|
|
|
{
|
|
|
|
|
bool isNextLevelEvolved = aaSkill.skillLevels[nextLevel].isEvolved;
|
|
|
|
|
bool hasRequiredStatPerk = aaSkill.requireStatForEvolve == null || character.HasStatPerk(aaSkill.requireStatForEvolve);
|
|
|
|
|
|
|
|
|
|
if (!isNextLevelEvolved || hasRequiredStatPerk)
|
|
|
|
|
{
|
|
|
|
|
availablePerks.Add(aaSkill);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var skillPerk in skillPerkData)
|
|
|
|
|
{
|
|
|
|
|
int currentLevel = GetSkillLevel(skillPerk);
|
|
|
|
|
int maxLevel = skillPerk.maxLevel;
|
|
|
|
|
|
|
|
|
|
if (currentLevel < maxLevel)
|
|
|
|
|
{
|
|
|
|
|
// SkillPerk can be leveled up
|
|
|
|
|
availablePerks.Add(skillPerk);
|
|
|
|
|
}
|
|
|
|
|
else if (currentLevel == maxLevel)
|
|
|
|
|
{
|
|
|
|
|
if (skillPerk.hasEvolution && character.HasStatPerk(skillPerk.perkRequireToEvolveSkill))
|
|
|
|
|
{
|
|
|
|
|
// SkillPerk can be evolved
|
|
|
|
|
availablePerks.Add(skillPerk);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var statPerk in statPerkData)
|
|
|
|
|
{
|
|
|
|
|
int currentLevel = GetPerkLevel(statPerk);
|
|
|
|
|
if (currentLevel < maxLevelStatPerks)
|
|
|
|
|
{
|
|
|
|
|
availablePerks.Add(statPerk);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ShufflePerks(availablePerks);
|
|
|
|
|
|
|
|
|
|
return availablePerks.GetRange(0, Mathf.Min(3, availablePerks.Count));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ShufflePerks(List<object> perks)
|
|
|
|
|
{
|
|
|
|
|
for (int i = perks.Count - 1; i > 0; i--)
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
int j = UnityEngine.Random.Range(0, i + 1);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
object temp = perks[i];
|
|
|
|
|
perks[i] = perks[j];
|
|
|
|
|
perks[j] = temp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the current level of a given perk.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="perk">The perk whose level is to be retrieved.</param>
|
|
|
|
|
/// <returns>The current level of the perk. Returns 0 if the perk is not found.</returns>
|
|
|
|
|
public int GetPerkLevel(object perk)
|
|
|
|
|
{
|
|
|
|
|
if (perk is SkillPerkData skillPerk)
|
|
|
|
|
{
|
|
|
|
|
if (skillLevels.TryGetValue(skillPerk, out int level))
|
|
|
|
|
{
|
|
|
|
|
return level;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (perk is StatPerkData statPerk)
|
|
|
|
|
{
|
|
|
|
|
if (perkLevels.TryGetValue(statPerk, out int level))
|
|
|
|
|
{
|
|
|
|
|
return level;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Sets the level of a given perk, handling skill and stat perks separately.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="perk">The perk whose level is to be set.</param>
|
|
|
|
|
/// <param name="level">The new level of the perk.</param>
|
|
|
|
|
public void SetPerkLevel(object perk)
|
|
|
|
|
{
|
|
|
|
|
if (perk is SkillPerkData skillPerk)
|
|
|
|
|
{
|
|
|
|
|
LevelUpSkill(skillPerk);
|
|
|
|
|
}
|
|
|
|
|
else if (perk is StatPerkData statPerk)
|
|
|
|
|
{
|
|
|
|
|
if (perkLevels.ContainsKey(statPerk))
|
|
|
|
|
{
|
|
|
|
|
perkLevels[statPerk]++;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
perkLevels.Add(statPerk, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("Unsupported perk type.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Levels up a skill to a specified level, including checks for evolution requirements.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="skillPerk">The skill perk to level up.</param>
|
|
|
|
|
/// <param name="level">The new level to set, if requirements are met.</param>
|
|
|
|
|
private void LevelUpSkill(SkillPerkData skillPerk)
|
|
|
|
|
{
|
|
|
|
|
if (skillPerk == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("SkillPerkData is null.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!skillLevels.ContainsKey(skillPerk))
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
skillLevels[skillPerk] = 0;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int currentLevel = skillLevels[skillPerk];
|
|
|
|
|
|
|
|
|
|
if (currentLevel >= skillPerk.maxLevel)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"Attempting to level up {skillPerk.name} beyond max level {skillPerk.maxLevel}.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentLevel == skillPerk.maxLevel - 1 && skillPerk.hasEvolution && !character.HasStatPerk(skillPerk.perkRequireToEvolveSkill))
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"Cannot level up {skillPerk.name} to max level without having {skillPerk.perkRequireToEvolveSkill.name}.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
skillLevels[skillPerk] = currentLevel + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the current level of a specific skill.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="skillPerk">The skill perk to check the level for.</param>
|
|
|
|
|
/// <returns>The current level of the skill.</returns>
|
|
|
|
|
public int GetSkillLevel(SkillPerkData skillPerk)
|
|
|
|
|
{
|
|
|
|
|
if (skillPerk == null)
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (skillLevels.ContainsKey(skillPerk))
|
|
|
|
|
{
|
|
|
|
|
return skillLevels[skillPerk];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Levels up a base skill of the character.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="skill">The skill to level up.</param>
|
|
|
|
|
public void LevelUpBaseSkill(SkillData skill)
|
|
|
|
|
{
|
|
|
|
|
if (!skillBaseLevels.ContainsKey(skill))
|
|
|
|
|
{
|
|
|
|
|
skillBaseLevels[skill] = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int currentLevel = skillBaseLevels[skill];
|
|
|
|
|
int maxLevelIndex = skill.skillLevels.Count - 1;
|
|
|
|
|
|
|
|
|
|
if (currentLevel <= maxLevelIndex)
|
|
|
|
|
{
|
|
|
|
|
skillBaseLevels[skill]++;
|
|
|
|
|
Debug.Log($"Skill {skill.skillName} leveled up to {skillBaseLevels[skill]}.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the current level of a base skill.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="skill">The skill to check.</param>
|
|
|
|
|
/// <returns>The current level of the skill.</returns>
|
|
|
|
|
public int GetBaseSkillLevel(SkillData skill)
|
|
|
|
|
{
|
|
|
|
|
if (skillBaseLevels.TryGetValue(skill, out int level))
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
return level;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
return 0;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Called when the local player reaches 0 HP. Decides if we show revive or straight defeat,
|
|
|
|
|
/// and guarantees game flow stop.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void OnPlayerDied(PlayerDiedEvent evt)
|
|
|
|
|
{
|
|
|
|
|
var ch = evt.Target;
|
|
|
|
|
if (ch == null) return;
|
|
|
|
|
if (ch.GetCurrentHP() > 0.001f) return;
|
|
|
|
|
#if FUSION2
|
|
|
|
|
if (ch.IsNetworked) return;
|
|
|
|
|
#endif
|
|
|
|
|
if (!ch.IsDead) ch.OnDeath();
|
|
|
|
|
OnPlayerDefeated(ch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Centralizes defeat flow (pause, UI, end flags).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void OnPlayerDefeated(CharacterEntity ch)
|
|
|
|
|
{
|
|
|
|
|
if (!gameRunning)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (isPvp) return;
|
|
|
|
|
|
|
|
|
|
gameRunning = false;
|
|
|
|
|
PauseGame();
|
|
|
|
|
|
|
|
|
|
bool canRevive = reviveLimit > 0 && !ch.IsNetworked;
|
|
|
|
|
UIGameplay.Singleton.DisplayEndGameScreen(false);
|
|
|
|
|
if (canRevive && UIGameplay.Singleton.reviveButton)
|
|
|
|
|
UIGameplay.Singleton.reviveButton.gameObject.SetActive(true);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async UniTaskVoid ReviveCountdownAsync(CharacterEntity ch, int secs, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
int remain = secs;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
while (remain > 0 && !token.IsCancellationRequested && gameRunning)
|
|
|
|
|
{
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.NotifyReviveTick(ch, remain);
|
|
|
|
|
#endif
|
|
|
|
|
await UniTask.Delay(1000, cancellationToken: token);
|
|
|
|
|
remain--;
|
|
|
|
|
}
|
|
|
|
|
if (token.IsCancellationRequested || !gameRunning) return;
|
|
|
|
|
|
|
|
|
|
ch.CharacterRevive();
|
|
|
|
|
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.SyncPlayerRevived(ch);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException) { }
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_reviveTimers.Remove(ch);
|
|
|
|
|
#if FUSION2
|
|
|
|
|
GameplaySync.Instance?.NotifyReviveTick(ch, 0);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void CancelAllRevives()
|
|
|
|
|
{
|
|
|
|
|
foreach (var kv in _reviveTimers)
|
|
|
|
|
kv.Value.Cancel();
|
|
|
|
|
_reviveTimers.Clear();
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
}
|