using BulletHellTemplate.Core.Events; using BulletHellTemplate.VFX; using Cysharp.Threading.Tasks; #if FUSION2 using Fusion; #endif using UnityEngine; namespace BulletHellTemplate { /// /// Networked singleton object that replicates global gameplay counters. /// Place it in the scene or let the Host spawn it via SpawnAsync. /// #if FUSION2 [RequireComponent(typeof(NetworkObject))] #endif public sealed class GameplaySync : #if FUSION2 NetworkBehaviour #else MonoBehaviour #endif { public static GameplaySync Instance { get; private set; } private GameplayManager GM => GameplayManager.Singleton; public enum PerkKind : byte { SkillPerk = 0, StatPerk = 1, BaseSkill = 2 } private void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); } else Destroy(gameObject); } private void OnDestroy() { if (Instance == this) Instance = null; } #if FUSION2 // ───── Utility Properties ───── [Networked, OnChangedRender(nameof(OnMatchStarted))] public NetworkBool MatchStarted { get; set; } [Networked, OnChangedRender(nameof(OnTimerChanged))] public int TimerSecs { get; set; } private float _secAcc = 0f; public bool RunnerActive => Runner && Runner.IsRunning; public bool IsHost => RunnerActive && HasStateAuthority; public override void FixedUpdateNetwork() { if (!HasStateAuthority || !MatchStarted) return; _secAcc += Runner.DeltaTime; if (_secAcc >= 1f) { _secAcc -= 1f; if (TimerSecs > 0) { TimerSecs--; } } } // ───── RPCs called on all clients ───── private void OnMatchStarted() { if (!MatchStarted) return; if (GameplayManager.Singleton) GameplayManager.Singleton.upgradeMode = UpgradeMode.UpgradeOnButtonClick; GameplayManager.Singleton?.StartGameplay(); if (HasStateAuthority) TimerSecs = GameplayManager.Singleton.GetSurvivalTime(); } private void OnTimerChanged() { if (GameplayManager.Singleton) GameplayManager.Singleton.SetTimeRemaining(TimerSecs); EventBus.Publish(new GameTimerTickEvent(TimerSecs)); } [Rpc(RpcSources.All, RpcTargets.All, InvokeLocal = false)] private void RPC_PlayerDied(NetworkId playerId) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var ce = nobj.GetComponent(); if (ce && !ce.IsDead) ce.OnDeath(); } public void SyncPlayerDied(CharacterEntity ce) { if (!RunnerActive) return; var nob = ce.GetComponent(); if (!nob) return; RPC_PlayerDied(nob.Id); } [Rpc(RpcSources.All, RpcTargets.All, InvokeLocal = false)] private void RPC_PlayerRevived(NetworkId playerId) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var ce = nobj.GetComponent(); if (ce && ce.IsDead) ce.CharacterRevive(); // fechar UI de revive no dono (se for local e é ele) if (nobj.HasInputAuthority) UIGameplay.Singleton?.HideRevivePanel(); } public void SyncPlayerRevived(CharacterEntity ce) { if (!RunnerActive) return; var nob = ce.GetComponent(); if (!nob) return; RPC_PlayerRevived(nob.Id); if (HasStateAuthority) RPC_DeathEnd(nob.Id); } // UI de revive para dono do personagem [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_StartReviveCountdown(NetworkId playerId, int seconds) { if (!Runner.TryFindObject(playerId, out var nobj)) return; if (nobj.HasInputAuthority) UIGameplay.Singleton?.ShowRevivePanel(seconds); } public void NotifyStartReviveCountdown(CharacterEntity ce, int seconds) { if (!RunnerActive || !HasStateAuthority) return; var nob = ce.GetComponent(); if (!nob) return; RPC_StartReviveCountdown(nob.Id, seconds); RPC_DeathStart(nob.Id, seconds); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_ReviveTick(NetworkId playerId, int remain) { if (!Runner.TryFindObject(playerId, out var nobj)) return; if (nobj.HasInputAuthority) UIGameplay.Singleton?.UpdateReviveCountdown(remain); } public void NotifyReviveTick(CharacterEntity ce, int remain) { if (!RunnerActive || !HasStateAuthority) return; var nob = ce.GetComponent(); if (!nob) return; RPC_ReviveTick(nob.Id, remain); RPC_DeathTick(nob.Id, remain); } /// /// Broadcast a team defeat (no revive online). Called only by StateAuthority. /// [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_TeamDefeat() { GameplayManager.Singleton.SetMatchFinished(); GameplayManager.Singleton.CancelAllRevives(); GameplayManager.Singleton.PauseGame(); UIGameplay.Singleton.DisplayEndGameScreen(false); UIGameplay.Singleton.ClearAllDeathEntries(); } [Rpc(RpcSources.All, RpcTargets.All, InvokeLocal = false)] private void RPC_ApplyPerk(NetworkId playerId, PerkKind kind, int index) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var ce = nobj.GetComponent(); if (!ce) return; var gm = GameplayManager.Singleton; switch (kind) { case PerkKind.SkillPerk: var sp = gm.skillPerkData[index]; gm.SetPerkLevel(sp); ce.GetComponent()?.ApplySkillPerk(sp, /*isReplica*/ true); break; case PerkKind.StatPerk: var st = gm.statPerkData[index]; gm.SetPerkLevel(st); ce.ApplyStatPerk(st, gm.GetPerkLevel(st)); break; case PerkKind.BaseSkill: var bs = ce.GetCharacterData().skills[index]; gm.LevelUpBaseSkill(bs); break; } UIGameplay.Singleton?.UpdateSkillPerkUI(); UIGameplay.Singleton?.UpdateStatPerkUI(); } // ── Death feed ───────────────────────────────── [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_DeathStart(NetworkId playerId, int seconds) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var ce = nobj.GetComponent(); UIGameplay.Singleton?.ShowDeathEntry(nobj, ce, seconds); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_DeathTick(NetworkId playerId, int remain) { if (!Runner.TryFindObject(playerId, out var nobj)) return; UIGameplay.Singleton?.UpdateDeathCountdown(nobj, remain); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_DeathEnd(NetworkId playerId) { if (!Runner.TryFindObject(playerId, out var nobj)) return; UIGameplay.Singleton?.HideDeathEntry(nobj); } public void SyncPerkChosen(CharacterEntity ce, PerkKind kind, int index) { if (!RunnerActive) return; var nob = ce.GetComponent(); if (!nob) return; RPC_ApplyPerk(nob.Id, kind, index); } /// /// Host/leader calls this when he detects a full team wipe. /// public void SyncTeamDefeat() { if (!RunnerActive || !HasStateAuthority) return; RPC_TeamDefeat(); } [Rpc(RpcSources.All, RpcTargets.All)] private void RPC_SetGold(int total) { if (!HasStateAuthority) GM.ForceSetGold(total); } [Rpc(RpcSources.All, RpcTargets.All)] private void RPC_SetXP(int total) { if (!HasStateAuthority) GM.ForceSetXP(total); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_SetMonstersKilled(int total) { if (!HasStateAuthority) GM.ForceSetMonstersKilled(total); } // ───── APIs called by GameplayManager ───── public void SyncGold(int total) { if (!RunnerActive) GM.ForceSetGold(total); // offline mode else if (IsHost) RPC_SetGold(total); // host sends } public void SyncXP(int total) { if (!RunnerActive) GM.ForceSetXP(total); else if (IsHost) RPC_SetXP(total); } public void SyncMonsters(int total) { if (!RunnerActive) GM.ForceSetMonstersKilled(total); else if (IsHost) RPC_SetMonstersKilled(total); } /// /// RPC that tells _all_ clients to spawn a drop for the given monster. /// [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_SpawnDropForMonster(NetworkId monsterId, bool isGold, Vector3 position, int amount) { if (!Runner.TryFindObject(monsterId, out var nobj)) return; var me = nobj.GetComponent(); if (me == null) return; var prefab = isGold ? me.GoldDropPrefab : me.ExpDropPrefab; if (prefab == null) return; var drop = DropPool.Spawn(prefab, position); drop.SetValue(amount); } /// /// Called by Host to sync a drop on all clients. /// public void SyncSpawnDrop(BaseMonsterEntity monster, bool isGold, Vector3 position, int amount) { if (!RunnerActive || !HasStateAuthority) return; var nob = monster.GetComponent(); if (nob == null) return; RPC_SpawnDropForMonster(nob.Id, isGold, position, amount); } /// /// RPC to configure monster values after it has been spawned. /// Called on all clients. The target monster is identified by its NetworkId. /// /// Network ID of the monster /// Gold reward value /// XP reward value [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_ConfigMonster(NetworkId netId, int gold, int xp) { if (Runner.TryFindObject(netId, out var nobj)) { var me = nobj.GetComponent(); me?.ConfigureMonster(gold, xp); } } /// /// Called by the Host after SpawnAsync to synchronize monster configuration. /// Does nothing in offline mode. Only the Host should call this. /// /// Spawned NetworkObject /// Gold reward value /// XP reward value public void SyncConfigureMonster(NetworkObject nobj, int gold, int xp) { if (!RunnerActive) return; if (IsHost) RPC_ConfigMonster(nobj.Id, gold, xp); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_MonsterSkillCast(NetworkId monsterId, int skillIndex, Vector3 dir) { if (Runner.TryFindObject(monsterId, out var nobj)) { var me = nobj.GetComponent(); me?.SkillRunner?.ExecuteSkill_Remote(skillIndex, dir); me?.PlaySkillAnimNet(skillIndex); } } public void SyncMonsterSkillCast(MonsterEntity monster, int skillIndex, Vector3 dir) { if (!RunnerActive || !IsHost) return; var nob = monster.GetComponent(); if (!nob) return; RPC_MonsterSkillCast(nob.Id, skillIndex, dir); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_MonsterHp(NetworkId monsterId, float hp) { if (!Runner.TryFindObject(monsterId, out var nobj)) return; if (nobj.HasStateAuthority) return; var mh = nobj.GetComponent(); mh?.ApplyRemoteHp(hp); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_BossSpawned(NetworkId bossId) { if (Runner.TryFindObject(bossId, out var nobj)) { var me = nobj.GetComponent(); UIGameplay.Singleton?.SetFinalBoss(me); UIGameplay.Singleton?.ShowFinalBossMessage(); } } public void SyncBossSpawned(MonsterEntity boss) { if (!RunnerActive || !HasStateAuthority) return; var nob = boss.GetComponent(); if (!nob) return; RPC_BossSpawned(nob.Id); } // ─── RPC for auto‐attack replication ─────────────────────────────────── [Rpc(RpcSources.All, RpcTargets.All, InvokeLocal = false)] private void RPC_PlayerAttack(NetworkId playerId, Vector3 dir) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var comp = nobj.GetComponent(); if (comp == null) return; bool replica = !nobj.HasStateAuthority; comp.AttackInternal(dir, replica); } public void SyncPlayerAttack(CharacterEntity ce, Vector3 dir) { if (!RunnerActive) return; var nob = ce.GetComponent(); if (!nob) return; RPC_PlayerAttack(nob.Id, dir); } // ───────── SKILL ─────────────────────────────────────────────── [Rpc(RpcSources.All, RpcTargets.All, InvokeLocal = false)] private void RPC_PlayerUseSkill(NetworkId playerId, int idx, Vector2 input) { if (!Runner.TryFindObject(playerId, out var nobj)) return; var atk = nobj.GetComponent(); if (atk == null) return; var skill = atk.CharacterOwner.GetCharacterData().skills[idx]; bool replica = !nobj.HasStateAuthority; // host atk.UseSkillInternal(idx, skill, input, replica); } public void SyncPlayerUseSkill(CharacterEntity ce, int idx, Vector2 input) { if (!RunnerActive) return; var nob = ce.GetComponent(); if (!nob) return; RPC_PlayerUseSkill(nob.Id, idx, input); } [Rpc(RpcSources.StateAuthority, RpcTargets.All)] private void RPC_EndGame() { if (!HasStateAuthority) GameplayManager.Singleton.EndGame(); } /// /// API pública: apenas o Host Shared‑Mode deve chamar para encerrar o jogo. /// public void SyncEndGame() { if (!Runner || !Runner.IsRunning || !HasStateAuthority) return; RPC_EndGame(); } #endif } }