|
| 1 | +using MapGeneration; |
| 2 | +using MEC; |
| 3 | +using PlayerRoles.FirstPersonControl; |
| 4 | +using PluginAPI.Core; |
| 5 | +using SCP575.API.Extensions; |
| 6 | +using SCP575.API.Features; |
| 7 | +using System; |
| 8 | +using System.Collections.Generic; |
| 9 | +using UnityEngine; |
| 10 | + |
| 11 | +namespace SCP575.API.Components |
| 12 | +{ |
| 13 | + /// <summary> |
| 14 | + /// SCP-575 main componente that manage chasing the player and kill it. |
| 15 | + /// </summary> |
| 16 | + public class ChaseComponent : MonoBehaviour |
| 17 | + { |
| 18 | + private ReferenceHub? ReferenceHub; |
| 19 | + |
| 20 | + private DummyPlayer DummyPlayer = null!; |
| 21 | + |
| 22 | + /// <summary> |
| 23 | + /// Gets the current chased player. |
| 24 | + /// </summary> |
| 25 | + public Player Player = null!; |
| 26 | + |
| 27 | + /// <summary> |
| 28 | + /// Coroutine Handlers. |
| 29 | + /// </summary> |
| 30 | + private CoroutineHandle _followCoroutine, _checkCoroutine; |
| 31 | + |
| 32 | + private void Start() |
| 33 | + { |
| 34 | + try |
| 35 | + { |
| 36 | + ReferenceHub = GetComponent<ReferenceHub>(); |
| 37 | + |
| 38 | + if (ReferenceHub is null) |
| 39 | + { |
| 40 | + Log.Debug($"{nameof(Start)}: Reference hub is null", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 41 | + Destroy(this); |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + if (Player is null) |
| 46 | + { |
| 47 | + Destroy(this); |
| 48 | + return; |
| 49 | + } |
| 50 | + |
| 51 | + var dummy = Dummies.GetDummy(ReferenceHub); |
| 52 | + |
| 53 | + if (dummy is null) |
| 54 | + { |
| 55 | + Log.Debug($"{nameof(Start)}: Dummy player is null", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 56 | + Destroy(this); |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + DummyPlayer = dummy; |
| 61 | + |
| 62 | + _followCoroutine = Timing.RunCoroutine(Chase().CancelWith(this).CancelWith(gameObject)); |
| 63 | + _checkCoroutine = Timing.RunCoroutine(Checks().CancelWith(this).CancelWith(gameObject)); |
| 64 | + } |
| 65 | + catch (Exception e) |
| 66 | + { |
| 67 | + Log.Error($"{nameof(Start)}: {e}"); |
| 68 | + Destroy(this); |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + /// <summary> |
| 73 | + /// Called when destroying the component |
| 74 | + /// </summary> |
| 75 | + private void OnDestroy() |
| 76 | + { |
| 77 | + if (EntryPoint.Instance.Config.BlackOut.EndBlackoutWhenDisappearing) |
| 78 | + BlackoutExtensions.EndBlackout(); |
| 79 | + |
| 80 | + Dummies.DestroyDummy(DummyPlayer); |
| 81 | + |
| 82 | + Log.Debug($"{nameof(OnDestroy)}: SCP-575 fully destroyed.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 83 | + } |
| 84 | + |
| 85 | + /// <summary> |
| 86 | + /// Move SCP-575 camera. |
| 87 | + /// </summary> |
| 88 | + private void Update() |
| 89 | + { |
| 90 | + if (ReferenceHub is null) |
| 91 | + return; |
| 92 | + |
| 93 | + ((IFpcRole)ReferenceHub.roleManager.CurrentRole).FpcModule.MouseLook.LookAtDirection(Player.Camera.position - DummyPlayer.Position); |
| 94 | + } |
| 95 | + |
| 96 | + /// <summary> |
| 97 | + /// Disables the MonoBehaviour by scheduling its destruction after a specified delay. |
| 98 | + /// </summary> |
| 99 | + /// <param name="delay">The delay, in seconds, before the MonoBehaviour is destroyed.</param> |
| 100 | + public void Disable(float delay = 0f) |
| 101 | + { |
| 102 | + |
| 103 | + if (delay > 0f) |
| 104 | + { |
| 105 | + // Log a debug message if debug mode is enabled. |
| 106 | + Log.Debug($"Calling Destroy in {delay} seconds", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 107 | + |
| 108 | + Timing.CallDelayed(delay, () => |
| 109 | + { |
| 110 | + _destroyed = true; |
| 111 | + // Schedule the destruction of the MonoBehaviour after the specified delay. |
| 112 | + Destroy(this); |
| 113 | + }); |
| 114 | + |
| 115 | + return; |
| 116 | + } |
| 117 | + |
| 118 | + _destroyed = true; |
| 119 | + |
| 120 | + Destroy(this); |
| 121 | + } |
| 122 | + |
| 123 | + private IEnumerator<float> Chase() |
| 124 | + { |
| 125 | + for (; ; ) |
| 126 | + { |
| 127 | + yield return Timing.WaitForSeconds(.1f); |
| 128 | + |
| 129 | + if (_destroyed) |
| 130 | + { |
| 131 | + yield break; |
| 132 | + } |
| 133 | + |
| 134 | + if (_roomIsIlluminated) |
| 135 | + { |
| 136 | + Log.Debug($"{nameof(Chase)}: Room is illuminated, destroying SCP575.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 137 | + // If the room is illuminated, disable SCP-575. |
| 138 | + Disable(); |
| 139 | + yield break; |
| 140 | + } |
| 141 | + |
| 142 | + // If there's a delayed chase, wait for the specified delay. |
| 143 | + if (_delayedChase) |
| 144 | + { |
| 145 | + Log.Debug($"{nameof(Chase)}: delay chase is true, delaying chase for {EntryPoint.Instance.Config.Scp575.DelayChase} seconds.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 146 | + yield return Timing.WaitForSeconds(EntryPoint.Instance.Config.Scp575.DelayChase); |
| 147 | + _delayedChase = false; |
| 148 | + } |
| 149 | + |
| 150 | + // Initialize roles and other properties on the first spawn. |
| 151 | + InitializeOnFirstSpawn(); |
| 152 | + |
| 153 | + // Calculate the distance between the player and the dummy. |
| 154 | + float distance = Vector3.Distance(Player.Position, DummyPlayer.Position); |
| 155 | + |
| 156 | + // Call private methods based on the calculated distance. |
| 157 | + HandleChase(distance); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + private IEnumerator<float> Checks() |
| 162 | + { |
| 163 | + for (; ; ) |
| 164 | + { |
| 165 | + yield return Timing.WaitForSeconds(5.0f); |
| 166 | + |
| 167 | + |
| 168 | + if (_destroyed) |
| 169 | + { |
| 170 | + yield break; |
| 171 | + } |
| 172 | + |
| 173 | + if (!Player.IsAlive) |
| 174 | + Disable(); |
| 175 | + |
| 176 | + cachedScp575Room = DummyPlayer.Room; |
| 177 | + _roomIsIlluminated = BlackoutExtensions.IsRoomIlluminated(cachedScp575Room); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + /// <summary> |
| 182 | + /// Initializes roles and other properties on the first spawn of the SCP-575. |
| 183 | + /// </summary> |
| 184 | + private void InitializeOnFirstSpawn() |
| 185 | + { |
| 186 | + if (_firstSpawn) |
| 187 | + { |
| 188 | + Log.Debug($"{nameof(InitializeOnFirstSpawn)}: Caching fpc roles for the victim and the SCP-575.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 189 | + victimFpc = (IFpcRole)Player.ReferenceHub.roleManager.CurrentRole; |
| 190 | + dummyFpc = (IFpcRole)DummyPlayer.ReferenceHub.roleManager.CurrentRole; |
| 191 | + _firstSpawn = false; |
| 192 | + Timing.WaitForSeconds(0.8f); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + /// <summary> |
| 197 | + /// Handles the SCP-575 chase logic based on the distance between the player and the dummy. |
| 198 | + /// </summary> |
| 199 | + /// <param name="distance">The distance between the player and the dummy.</param> |
| 200 | + private void HandleChase(float distance) |
| 201 | + { |
| 202 | + if (distance >= EntryPoint.Instance.Config.Scp575.MaxDistance) |
| 203 | + { |
| 204 | + Log.Debug($"{nameof(HandleChase)}: Max distance reached, destroying SCP-575.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 205 | + // If the dummy is too far, stop chasing. |
| 206 | + Disable(3); |
| 207 | + } |
| 208 | + else if (distance >= EntryPoint.Instance.Config.Scp575.MediumDistance) |
| 209 | + { |
| 210 | + if (EntryPoint.Instance.Config.Scp575.ChangeMovementSpeedIfRun && victimFpc != null && victimFpc.FpcModule.CurrentMovementState == PlayerMovementState.Sprinting) |
| 211 | + { |
| 212 | + MoveTowardsDummy(EntryPoint.Instance.Config.Scp575.MovementSpeedRunning); |
| 213 | + |
| 214 | + return; |
| 215 | + } |
| 216 | + |
| 217 | + MoveTowardsDummy(EntryPoint.Instance.Config.Scp575.MovementSpeedFast); |
| 218 | + } |
| 219 | + else if (distance > EntryPoint.Instance.Config.Scp575.MinDistance) |
| 220 | + { |
| 221 | + MoveTowardsDummy(EntryPoint.Instance.Config.Scp575.MovementSpeed); |
| 222 | + } |
| 223 | + else if (distance <= EntryPoint.Instance.Config.Scp575.KillDistance) |
| 224 | + { |
| 225 | + Log.Debug($"{nameof(HandleChase)}: Kill distance reached, deboning the victim.", EntryPoint.Instance.Config.DebugMode, EntryPoint.Prefix); |
| 226 | + KillPlayer(); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + /// <summary> |
| 231 | + /// Moves SCP-575 towards the dummy with the specified movement speed. |
| 232 | + /// </summary> |
| 233 | + /// <param name="movementSpeed">The movement speed of SCP-575.</param> |
| 234 | + private void MoveTowardsDummy(float movementSpeed) |
| 235 | + { |
| 236 | + var direction = Player.Position - DummyPlayer.Position; |
| 237 | + direction = direction.normalized; |
| 238 | + var velocity = direction * movementSpeed; |
| 239 | + dummyFpc.FpcModule.CharController.Move(velocity * Time.deltaTime); |
| 240 | + } |
| 241 | + |
| 242 | + /// <summary> |
| 243 | + /// Kills the player and performs associated actions. |
| 244 | + /// </summary> |
| 245 | + private void KillPlayer() |
| 246 | + { |
| 247 | + // Kill the player. |
| 248 | + Player.Kill(EntryPoint.Instance.Config.Scp575.KillFeed); |
| 249 | + |
| 250 | + // Broadcast kill message if configured. |
| 251 | + if (EntryPoint.Instance.Config.Scp575.BroadcastDuration > 0) |
| 252 | + { |
| 253 | + Player.SendBroadcast(EntryPoint.Instance.Config.Scp575.BroadcastKill, EntryPoint.Instance.Config.Scp575.BroadcastDuration); |
| 254 | + } |
| 255 | + |
| 256 | + // Log the kill information. |
| 257 | + Log.Info($"SCP-575 killed player {Player.LogName}", EntryPoint.Prefix); |
| 258 | + |
| 259 | + // Destroy SCP-575 or perform any other cleanup actions. |
| 260 | + Disable(); |
| 261 | + } |
| 262 | + |
| 263 | + /// <summary> |
| 264 | + /// To avoid killing the server by making calls every Frame, I will save the last room in a cache that refreshes every 5 seconds. |
| 265 | + /// </summary> |
| 266 | + private RoomIdentifier? cachedScp575Room; |
| 267 | + |
| 268 | + /// <summary> |
| 269 | + /// Every 5 seconds I will update if the current room has the lights on |
| 270 | + /// </summary> |
| 271 | + private bool _roomIsIlluminated = false; |
| 272 | + |
| 273 | + private bool _firstSpawn = true, _delayedChase = EntryPoint.Instance.Config.Scp575.DelayOnChase, _destroyed = false; |
| 274 | + IFpcRole victimFpc = null!; |
| 275 | + IFpcRole dummyFpc = null!; |
| 276 | + } |
| 277 | +} |
0 commit comments