using System; using UnityEngine; using VRC.SDK3.Data; namespace Marro.PacManUdon { public class GhostManager : SyncedObject { [NonSerialized] public GameManager gameController; private Ghost[] ghosts; private Ghost blinky; private PelletManager pelletManager; // Level constants private float speedDefault; private float speedScared; private float speedReturn; private float speedTunnel; private float speedHome; private float speedElroy1; private float speedElroy2; private int elroy1PelletCount; private int elroy2PelletCount; private float powerPelletDuration; private float[] scatterPattern; private float pelletTimeoutLimit; // Power Pellet logic private bool powerPelletActive; private float powerPelletCountdown; private int powerPelletMultiplier; private DataList ghostScaredQueue; // Blink logic private const float blinkCycleRate = 0.233333333f; private bool blinkingActivated; private float blinkCountdown; private bool blinkCurrentlyWhite; // Scattering logic private float scatterCounter; private int scatterPatternIndex; // Elroy logic public int elroyLevel; // Ghost house logic private bool sharedPelletCounterActive; private int sharedPelletCounter; private readonly int[] sharedPelletCounterReleaseValues = { 0, 7, 17, 32 }; private float pelletTimeout; private bool frozen; private bool kinematic; public void Initialize(Transform[] ghostStarts, Transform[] ghostTargets, PacMan pacMan, PelletManager pelletManager, GameManager gameController) { this.gameController = gameController; this.pelletManager = pelletManager; ghosts = transform.GetComponentsInChildren(true); blinky = ghosts[0]; for (int ghostIndex = 0; ghostIndex < ghosts.Length; ghostIndex++) { Transform startTransform = ghostStarts[ghostIndex]; Vector2 homePosition = ghostTargets[0].localPosition; Vector2 idlePosition1 = ghostTargets[1 + ghostIndex * 3].localPosition; Vector2 idlePosition2 = ghostTargets[2 + ghostIndex * 3].localPosition; Vector2 cornerPosition = ghostTargets[3 + ghostIndex * 3].localPosition; ghosts[ghostIndex].Initialize(pacMan, blinky, startTransform, homePosition, idlePosition1, idlePosition2, cornerPosition, ghostIndex); } } public void RestartLevel(bool afterLifeLost = false) { ghostScaredQueue = new DataList(); powerPelletActive = false; scatterCounter = 0; scatterPatternIndex = 0; sharedPelletCounter = 0; pelletTimeout = 0; elroyLevel = 0; kinematic = false; if (afterLifeLost) { SetSharedPelletCounterActive(true); } foreach (Ghost ghost in ghosts) { ghost.Reset(); } SetScattering(true, reverseDirection: false); } public void NewLevel() { SetSharedPelletCounterActive(false); UpdateElroyLevel(); foreach (Ghost ghost in ghosts) { ghost.ResetHousePelletCounter(); } } public override void SyncedUpdate() { if (frozen || kinematic) { return; } UpdatePelletTimeout(); if (powerPelletActive) { UpdatePowerPellet(); } else { UpdateScattering(); } } void UpdateScattering() { scatterCounter += networkManager.SyncedDeltaTime; if (scatterPatternIndex < scatterPattern.Length && scatterCounter >= scatterPattern[scatterPatternIndex]) { // Debug.Log($"{gameObject} SetScattering: {scatterPatternIndex%1 == 0}, scatterCounter: {scatterCounter}, scatterPattern: {scatterPattern[scatterPatternIndex]}, scatterPatternIndex: {scatterPatternIndex}"); scatterPatternIndex++; SetScattering(scatterPatternIndex % 2 == 0); } } void UpdatePowerPellet() { powerPelletCountdown -= networkManager.SyncedDeltaTime; if (powerPelletCountdown <= 0) { gameController.EndPowerPellet(); // End power pellet SetPowerPellet(false); } else if (!blinkingActivated && powerPelletCountdown <= 2.1166667) { SetGhostBlinking(true); } if (blinkingActivated) { blinkCountdown -= networkManager.SyncedDeltaTime; if (blinkCountdown <= 0) { blinkCountdown += blinkCycleRate; SetGhostBlinkingState(!blinkCurrentlyWhite); } } } void UpdatePelletTimeout() { pelletTimeout += networkManager.SyncedDeltaTime; if (pelletTimeout >= pelletTimeoutLimit) { pelletTimeout -= pelletTimeoutLimit; for (int ghostIndex = 0; ghostIndex < ghosts.Length; ghostIndex++) { Ghost ghost = ghosts[ghostIndex]; if (ghost.GetGhostState() == PacManGhostState.Home) { ghost.Release(); break; } } } } public void GhostCaughtQueue(Ghost ghost) { if (gameController.GameState == PacManGameState.AttractMode) { gameController.GhostCaught(0); return; } // Debug.Log($"{gameObject} GhostCaughtQueue with ghost {ghost}"); networkManager.SendEventSoon(NetworkEventType.GhostUpdate); ghostScaredQueue.Add(ghost); GhostCaughtExecute(ghost); } public void GhostCaughtContinue() { // Debug.Log($"{gameObject} GhostCaughtContinue with ghost queue length {ghostScaredQueue.Count}"); if (!ghostScaredQueue.TryGetValue(0, out DataToken currentGhost)) { Debug.LogError("Called GhostCaughtContinue without a ghost in the queue!"); return; } ((Ghost)currentGhost.Reference).ReturnHome(); ghostScaredQueue.RemoveAt(0); if (ghostScaredQueue.TryGetValue(0, out DataToken nextGhost)) { GhostCaughtExecute((Ghost)nextGhost.Reference); } } void GhostCaughtExecute(Ghost ghost) { int scoreBonus = 200 * (int)Math.Pow(2, powerPelletMultiplier); powerPelletMultiplier += 1; ghost.Caught(scoreBonus); gameController.GhostCaught(scoreBonus); } public void CapturedPacMan() { gameController.PacManCaught(); } public void GhostReturned() { bool noGhostsScared = true; bool noGhostsRetreating = true; foreach (var ghost in ghosts) { var state = ghost.GetGhostState(); if (ghost.IsScared) { noGhostsScared = false; } if (state == PacManGhostState.CaughtScore || state == PacManGhostState.Returning || state == PacManGhostState.Entering) { noGhostsRetreating = false; } } if (noGhostsScared) { gameController.NoGhostsScared(); } if (noGhostsRetreating) { gameController.NoGhostsRetreating(); } } void SetGhostBlinking(bool blinking) { blinkingActivated = blinking; if (blinking == true) { blinkCountdown = blinkCycleRate; SetGhostBlinkingState(true); } } void SetGhostBlinkingState(bool white) { blinkCurrentlyWhite = white; foreach (Ghost ghost in ghosts) { ghost.SetWhite(white); } } public void SetPowerPellet(bool powerPelletActive) { this.powerPelletActive = powerPelletActive; if (powerPelletActive) { powerPelletCountdown = powerPelletDuration; powerPelletMultiplier = 0; SetGhostBlinking(false); foreach (Ghost ghost in ghosts) { ghost.BecomeScared(); } } if (!powerPelletActive || powerPelletDuration == 0) { foreach (Ghost ghost in ghosts) { ghost.CalmDown(); SetGhostBlinking(false); } } } public void SetFrozen(bool frozen, bool ignoreIfCaught = false) { this.frozen = frozen; foreach (Ghost ghost in ghosts) { ghost.SetFrozen(frozen, ignoreIfCaught: ignoreIfCaught); } } public void SetLevel(int level) { Debug.Log($"GhostManager: SetLevel {level}"); SetLevelConstants(level); int[] privatePelletCounterReleaseValues = PacManConstants.GetGhostHousePrivatePelletCounterLimitForLevel(level); for (int i = 0; i < ghosts.Length; i++) { ghosts[i].SetHousePelletCounterLimit(privatePelletCounterReleaseValues[i]); RestartLevel(); // Reset needed to properly apply level } } private void SetLevelConstants(int level) { speedDefault = PacManConstants.GetGhostDefaultSpeedForLevel(level); speedScared = PacManConstants.GetGhostScaredSpeedForLevel(level); speedReturn = 15f; speedTunnel = PacManConstants.GetGhostTunnelSpeedForLevel(level); speedHome = PacManConstants.GetGhostHomeSpeed(); speedElroy1 = PacManConstants.GetBlinkyElroy1SpeedForLevel(level); speedElroy2 = PacManConstants.GetBlinkyElroy2SpeedForLevel(level); elroy1PelletCount = PacManConstants.GetElroy1PelletsRemainingForLevel(level); elroy2PelletCount = PacManConstants.GetElroy2PelletsRemainingForLevel(level); powerPelletDuration = PacManConstants.GetScaredDurationForLevel(level); scatterPattern = PacManConstants.GetScatterPatternForLevel(level); pelletTimeoutLimit = PacManConstants.GetGhostHousePelletTimeoutLimitForLevel(level); } public float GetTargetSpeed(Ghost ghost, PacManGhostState ghostState, bool isScared, bool inTunnel) { if (ghostState == PacManGhostState.Returning || ghostState == PacManGhostState.Entering) { return speedReturn; } if (inTunnel) { return speedTunnel; } if (ghostState == PacManGhostState.Exiting || ghostState == PacManGhostState.Home) { return speedHome; } if (isScared) { return speedScared; } if (ghost == blinky) { if (elroyLevel == 1) { return speedElroy1; } if (elroyLevel == 2) { return speedElroy2; } } return speedDefault; } void SetScattering(bool scattering, bool reverseDirection = true) { Debug.Log($"{gameObject} SetScattering: {scattering}"); foreach (Ghost ghost in ghosts) { if (ghost == blinky && elroyLevel > 0) // Once blinky is elroy he no longer scatters { continue; } ghost.SetScattering(scattering, reverseDirection); } } /// /// Whether to use the shared pellet counter for ghost exiting. /// Should be called before ghosts are reset. /// void SetSharedPelletCounterActive(bool active) { Debug.Log($"{gameObject} SetSharedPelletCounterActive {active}"); sharedPelletCounterActive = active; foreach (Ghost ghost in ghosts) { ghost.SetHousePelletCounterActive(!active); } } public void PelletConsumed() { pelletTimeout = 0; UpdateElroyLevel(); if (sharedPelletCounterActive) { IncrementSharedPelletCounter(); } else { IncrementPrivatePelletCounter(); } } void IncrementSharedPelletCounter() { sharedPelletCounter++; //Debug.Log($"Incremented shared pellet counter to {sharedPelletCounter}"); for (int ghostIndex = 0; ghostIndex < sharedPelletCounterReleaseValues.Length; ghostIndex++) { Ghost ghost = ghosts[ghostIndex]; if (ghost.GetGhostState() == PacManGhostState.Home && sharedPelletCounter == sharedPelletCounterReleaseValues[ghostIndex]) { ghost.Release(); if (ghostIndex == sharedPelletCounterReleaseValues.Length - 1) { SetSharedPelletCounterActive(false); } } } } void IncrementPrivatePelletCounter() { for (int ghostIndex = 0; ghostIndex < ghosts.Length; ghostIndex++) { Ghost ghost = ghosts[ghostIndex]; if (ghost.GetGhostState() == PacManGhostState.Home) { ghost.IncrementHousePelletCounter(); break; } } } void UpdateElroyLevel() { // Debug.Log($"{gameObject} Updating Elroy Level with pelletsRemaining {pelletsRemaining} with elroy2PelletCount {elroy2PelletCount} and elroy1PelletCount {elroy1PelletCount}"); var oldElroyLevel = elroyLevel; var pelletsRemaining = pelletManager.PelletCount - pelletManager.PelletCollectedCount; if (pelletsRemaining < elroy2PelletCount) elroyLevel = 2; else if (pelletsRemaining < elroy1PelletCount) elroyLevel = 1; else elroyLevel = 0; if (elroyLevel != oldElroyLevel) { blinky.SetElroy(elroyLevel); } } public void SetActive(bool active) { gameObject.SetActive(active); foreach (Ghost ghost in ghosts) { ghost.SetActive(active); } } public void SetKinematic(bool kinematic) { this.kinematic = kinematic; foreach (Ghost ghost in ghosts) { ghost.SetKinematic(kinematic); } } public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.GhostUpdate) { return; } // Power Pellet logic data.Append(powerPelletActive, ref index); data.Append(powerPelletCountdown, ref index); data.AppendAsByte(powerPelletMultiplier, ref index); // Blink logic data.Append(blinkingActivated, ref index); data.Append(blinkCountdown, ref index); data.Append(blinkCurrentlyWhite, ref index); // Scattering logic data.Append(scatterCounter, ref index); data.AppendAsByte(scatterPatternIndex, ref index); // Elroy logic data.AppendAsByte(elroyLevel, ref index); // Ghost house logic data.Append(sharedPelletCounterActive, ref index); data.AppendAsByte(sharedPelletCounter, ref index); data.Append(pelletTimeout, ref index); data.Append(frozen, ref index); data.Append(kinematic, ref index); data.AppendAsByte(gameController.Level, ref index); var ghostScaredQueueArray = new byte[ghosts.Length]; for (int i = 0; i < ghostScaredQueueArray.Length; i++) { var add = ghostScaredQueue.TryGetValue(i, out var ghost); if (!add) { ghostScaredQueueArray[i] = byte.MaxValue; // Add a terminator break; } ghostScaredQueueArray[i] = (byte)((Ghost)ghost.Reference).Index; } Debug.Log($"{gameObject} Sent a ghostScareQueue of length {ghostScaredQueue.Count}"); data.Append(ghostScaredQueueArray, ref index); } public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.GhostUpdate) { return true; } // Power Pellet logic powerPelletActive = data.ReadBool(ref index); powerPelletCountdown = data.ReadFloat(ref index); powerPelletMultiplier = data.ReadByte(ref index); // Blink logic blinkingActivated = data.ReadBool(ref index); blinkCountdown = data.ReadFloat(ref index); blinkCurrentlyWhite = data.ReadBool(ref index); // Scattering logic scatterCounter = data.ReadFloat(ref index); scatterPatternIndex = data.ReadByte(ref index); // Elroy logic elroyLevel = data.ReadByte(ref index); // Ghost house logic sharedPelletCounterActive = data.ReadBool(ref index); sharedPelletCounter = data.ReadByte(ref index); pelletTimeout = data.ReadFloat(ref index); frozen = data.ReadBool(ref index); kinematic = data.ReadBool(ref index); var level = data.ReadByte(ref index); SetLevelConstants(level); ghostScaredQueue.Clear(); for (int i = 0; i < ghosts.Length; i++) { var ghostIndex = data[index + i]; if (ghostIndex > ghosts.Length) // Reached terminator { break; } ghostScaredQueue.Add(ghosts[ghostIndex]); } Debug.Log($"{gameObject} Read back a ghostScareQueue of length {ghostScaredQueue.Count}"); return true; } public Ghost[] Ghosts { get => ghosts; } } }