425 lines
14 KiB
C#

#if FUSION2
using Fusion;
#endif
using UnityEngine;
using Cysharp.Threading.Tasks;
using BulletHellTemplate.PVP;
using System.Collections.Generic;
using System.Linq;
namespace BulletHellTemplate
{
#if FUSION2
[RequireComponent(typeof(NetworkObject))]
#endif
public class PvpSync :
#if FUSION2
NetworkBehaviour
#else
MonoBehaviour
#endif
{
public static PvpSync Instance { get; private set; }
#if FUSION2
/* -------------------- Config -------------------- */
[Networked] public NetworkBool RulesReady { get; private set; }
[Networked] public NetworkBool TeamsAssigned { get; private set; }
[Networked] public PvpModeType Mode { get; private set; }
[Networked] public int TeamCount { get; private set; }
[Networked] public int PlayersPerTeam { get; private set; }
// Team Deathmatch
[Networked] public int MatchTimeSeconds { get; private set; }
[Networked] public int PointsPerKill { get; private set; }
// Arena
public int KillLimit { get; private set; } = 5;
[Networked] public int TimeLeft { get; private set; }
[Networked] public NetworkDictionary<byte, int> TeamScores => default;
[Networked] public int Team0Kills { get; set; }
[Networked] public int Team1Kills { get; set; }
private TickTimer _tickTimer;
[Networked] public float PlayerDamageTakenMultiplier { get; private set; }
public static bool IsSpawnedReady =>
Instance != null && Instance.Object != null && Instance.Object.IsValid;
#endif
public float ReviveDelay { get; private set; } = 3f;
private void Awake() => Instance = this;
/* ----------------------------- API ----------------------------- */
#if FUSION2
public override void Spawned()
{
if (HasStateAuthority)
{
RulesReady = false;
TeamsAssigned = false;
}
}
public int GetTeamScore(byte team)
=> TeamScores.TryGet(team, out var s) ? s : 0;
public void InitializeFrom(PvpModeData data)
{
if (!HasStateAuthority) return;
Mode = data.ModeType;
TeamCount = data.GetTeamCount();
PlayersPerTeam = data.GetPlayersPerTeam();
for (byte i = 0; i < TeamCount; i++)
TeamScores.Set(i, 0);
ReviveDelay = 3f;
PlayerDamageTakenMultiplier = 1f;
switch (Mode)
{
case PvpModeType.TeamDeathmatch:
var tdm = (TdmModeData)data;
MatchTimeSeconds = Mathf.Max(30, tdm.matchTimeSeconds);
PointsPerKill = Mathf.Max(1, tdm.pointsPerKill);
PlayerDamageTakenMultiplier = Mathf.Clamp01(tdm.playerDamageTakenMultiplier);
TimeLeft = MatchTimeSeconds;
_tickTimer = TickTimer.CreateFromSeconds(Runner, 1f);
break;
case PvpModeType.Arena:
var arena = (ArenaModeData)data;
KillLimit = Mathf.Max(1, arena.killLimit);
ReviveDelay = Mathf.Max(0f, arena.reviveDelay);
PlayerDamageTakenMultiplier = Mathf.Clamp01(arena.playerDamageTakenMultiplier);
Team0Kills = 0;
Team1Kills = 0;
break;
case PvpModeType.BattleRoyale:
{
var br = (BattleRoyaleModeData)data;
PlayerDamageTakenMultiplier = Mathf.Clamp01(br.playerDamageTakenMultiplier);
TeamCount = Runner.ActivePlayers.Count();
PlayersPerTeam = 1;
if (HasStateAuthority && br.zonePrefab)
{
var n = Runner.Spawn(br.zonePrefab.gameObject);
var zone = n.GetComponent<SafeZoneController>();
zone.InitializeFrom(br, Vector3.zero);
}
break;
}
}
TeamsAssigned = false;
RulesReady = false;
RPC_SetPvpFlag(true);
}
public void MarkTeamsAssignedAndReady()
{
if (!HasStateAuthority) return;
Debug.Log($"[PVP] MarkTeamsAssignedAndReady() by {Runner.LocalPlayer}");
TeamsAssigned = true;
RulesReady = true;
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
private void RPC_SetPvpFlag(bool enabled)
{
GameplayManager.Singleton?.SetPvpSession(enabled);
}
public void SetTeamScore(byte team, int value)
{
if (Object && Object.HasStateAuthority)
TeamScores.Set(team, value);
else
RPC_SetTeamScore(team, value);
}
public void AddTeamScore(byte team, int delta)
{
if (Object && Object.HasStateAuthority)
AddTeamScoreAuthority(team, delta);
else
RPC_AddTeamScore(team, delta);
}
public void ReportKill(byte killerTeam)
{
RPC_ReportKill(killerTeam);
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
public void RPC_NotifyKill(byte killerCid, NetworkString<_32> killerNick,
byte victimCid, NetworkString<_32> victimNick,
byte killerTeam)
{
switch (Mode)
{
case PvpModeType.TeamDeathmatch:
AddTeamScoreAuthority(killerTeam, Mathf.Max(1, PointsPerKill));
break;
case PvpModeType.Arena:
AddTeamScoreAuthority(killerTeam, 1);
if (killerTeam == 0) Team0Kills++; else Team1Kills++;
if (GetTeamScore(killerTeam) >= KillLimit)
EndMatchWithWinner(killerTeam);
break;
case PvpModeType.BattleRoyale:
AddTeamScoreAuthority(killerTeam, 1);
break;
}
RPC_AnnounceKill(killerCid, killerNick, victimCid, victimNick);
if (Mode == PvpModeType.BattleRoyale)
CheckAliveAndMaybeEnd();
}
public async UniTaskVoid ScheduleRevive(CharacterEntity who)
{
if (!HasStateAuthority || !who) return;
if (Mode == PvpModeType.BattleRoyale) return;
await UniTask.Delay(System.TimeSpan.FromSeconds(ReviveDelay));
if (who && who.IsDead) who.CharacterRevive();
}
/* ----------------------------- RPCs ---------------------------- */
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
private void RPC_SetTeamScore(byte team, int value)
=> TeamScores.Set(team, value);
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
private void RPC_AddTeamScore(byte team, int delta)
=> AddTeamScoreAuthority(team, delta);
private void AddTeamScoreAuthority(byte team, int delta)
{
var cur = TeamScores.TryGet(team, out var s) ? s : 0;
TeamScores.Set(team, cur + delta);
}
[Rpc(RpcSources.All, RpcTargets.StateAuthority)]
private void RPC_ReportKill(byte killerTeam)
{
if (killerTeam >= TeamCount) return;
switch (Mode)
{
case PvpModeType.TeamDeathmatch:
AddTeamScoreAuthority(killerTeam, Mathf.Max(1, PointsPerKill));
break;
case PvpModeType.Arena:
AddTeamScoreAuthority(killerTeam, 1);
if (killerTeam == 0) Team0Kills++; else Team1Kills++;
if (GetTeamScore(killerTeam) >= KillLimit)
EndMatchWithWinner(killerTeam);
break;
case PvpModeType.BattleRoyale:
break;
}
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_AnnounceKill(byte killerCid, NetworkString<_32> killerNick,
byte victimCid, NetworkString<_32> victimNick)
{
var kData = GameInstance.Singleton.GetCharacterDataById(killerCid);
var vData = GameInstance.Singleton.GetCharacterDataById(victimCid);
var kIcon = kData ? kData.icon : null;
var vIcon = vData ? vData.icon : null;
UIGameplay.Singleton?.PushKillFeed(kIcon, killerNick.ToString(), vIcon, victimNick.ToString());
}
public void AnnounceEliminationNow(CharacterEntity victim)
{
if (!HasStateAuthority || Mode != PvpModeType.BattleRoyale || victim == null || !victim.Object)
return;
#if UNITY_6000_0_OR_NEWER
var all = UnityEngine.Object.FindObjectsByType<CharacterEntity>(FindObjectsSortMode.None);
#else
var all = UnityEngine.Object.FindObjectsOfType<CharacterEntity>();
#endif
int alive = all.Count(c => c && !c.IsDead);
int placement = alive + 1;
RPC_AnnounceBattleRoyalePlacement(victim.Object.InputAuthority, placement);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_AnnounceWinnerWithScores(PvpModeType mode, int winnerTeam, int[] scores)
{
UIGameplay.Singleton?.ShowPvpEndScreen(mode, winnerTeam, scores, -1);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_AnnounceBattleRoyalePlacement(PlayerRef target, int myPlacement)
{
if (target != Runner.LocalPlayer) return;
UIGameplay.Singleton?.ShowPvpEndScreen(PvpModeType.BattleRoyale, -1, null, myPlacement);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
public void RPC_AnnounceWinner(int winnerTeam)
{
UIGameplay.Singleton?.ShowPvpEndScreen(Mode, winnerTeam, null, -1);
}
/* -------------------------- Lifecycle -------------------------- */
public override void FixedUpdateNetwork()
{
if (!HasStateAuthority) return;
if (Mode == PvpModeType.TeamDeathmatch && _tickTimer.Expired(Runner))
{
_tickTimer = TickTimer.CreateFromSeconds(Runner, 1f);
if (TimeLeft > 0) TimeLeft--;
if (TimeLeft <= 0) TryEndByTime();
}
}
public void OnTeamEliminatedCheck(int aliveTeams)
{
if (!HasStateAuthority || Mode != PvpModeType.BattleRoyale) return;
if (aliveTeams <= 1)
{
EndMatchWithWinner(-1);
}
}
private void TryEndByTime()
{
if (!HasStateAuthority || Mode != PvpModeType.TeamDeathmatch) return;
if (TeamCount <= 0)
{
EndMatchWithWinner(-1);
return;
}
int bestTeam = 0;
int bestScore = GetTeamScore(0);
for (int i = 1; i < TeamCount; i++)
{
int s = GetTeamScore((byte)i);
if (s > bestScore)
{
bestScore = s;
bestTeam = i;
}
}
EndMatchWithWinner(bestTeam);
}
private static int GetTeamOf(NetworkRunner runner, PlayerRef pref)
{
if (runner == null || !runner.IsRunning)
return 0;
var list = runner.ActivePlayers.OrderBy(p => p.RawEncoded).ToList();
int idx = list.IndexOf(pref);
if (idx < 0) return 0;
int teams = Mathf.Max(1, Instance ? Instance.TeamCount : 1);
return idx % teams;
}
private void CheckAliveAndMaybeEnd()
{
if (!HasStateAuthority || Mode != PvpModeType.BattleRoyale) return;
#if UNITY_6000_0_OR_NEWER
var all = FindObjectsByType<CharacterEntity>(FindObjectsSortMode.None);
#else
var all = FindObjectsOfType<CharacterEntity>();
#endif
var alive = all.Where(c => c && !c.IsDead && (!c.IsNetworked || (c.Object && c.Object.IsValid))).ToList();
if (alive.Count <= 1)
{
int winnerTeam = -1;
if (alive.Count == 1)
{
var ce = alive[0];
if (ce.IsNetworked && ce.Object)
winnerTeam = GetTeamOf(Runner, ce.Object.InputAuthority);
}
EndMatchWithWinner(winnerTeam);
}
}
private void EndMatchWithWinner(int teamIndex)
{
if (!HasStateAuthority) return;
switch (Mode)
{
case PvpModeType.TeamDeathmatch:
{
var scores = new int[TeamCount];
for (int i = 0; i < TeamCount; i++)
scores[i] = GetTeamScore((byte)i);
RPC_AnnounceWinnerWithScores(Mode, teamIndex, scores);
break;
}
case PvpModeType.Arena:
{
var scores = new int[TeamCount];
for (int i = 0; i < TeamCount; i++)
scores[i] = GetTeamScore((byte)i);
RPC_AnnounceWinnerWithScores(Mode, teamIndex, scores);
break;
}
case PvpModeType.BattleRoyale:
{
var rank = Enumerable.Range(0, TeamCount)
.OrderByDescending(t => GetTeamScore((byte)t))
.ToList();
foreach (var p in Runner.ActivePlayers)
{
int myTeam = GetTeamOf(Runner, p);
int placement = rank.IndexOf(myTeam) + 1;
RPC_AnnounceBattleRoyalePlacement(p, placement);
}
break;
}
}
GameplayManager.Singleton?.EndGame();
}
#endif
}
}