Skip to content

Commit ffea3d5

Browse files
authored
2.0.0
* The code was re-structured and improved in both readability and performance. * It is no longer possible to make the SCP-575 disappear by pointing a flashlight at it. This was done because I want to implement another way to recontest or escape from SCP-575 in an "easy" way. * The cassie message has been reworked in the configuracion and the old one is not longer in use. It will be deleted in a nex release
2 parents fe2e457 + 3befeee commit ffea3d5

21 files changed

+1555
-1379
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using PluginAPI.Core;
2+
using System;
3+
using System.IO;
4+
5+
namespace SCP575.API.Extensions
6+
{
7+
/// <summary>
8+
/// A utility class containing audio-related extension methods.
9+
/// </summary>
10+
public static class AudioExtensions
11+
{
12+
13+
/// <summary>
14+
/// Checks for .ogg files in the specified sounds folder.
15+
/// </summary>
16+
/// <returns>
17+
/// True if .ogg files are found; otherwise, false.
18+
/// </returns>
19+
public static bool AudioFilesExist()
20+
{
21+
try
22+
{
23+
string pathToAudios = EntryPoint.Instance.Config.PathToAudios;
24+
25+
// Check if the folder exists before attempting to retrieve files.
26+
if (Directory.Exists(pathToAudios))
27+
{
28+
// Get files with the .ogg extension.
29+
string[] files = Directory.GetFiles(pathToAudios, "*.ogg");
30+
31+
// Check if .ogg files were found.
32+
return files.Length > 0;
33+
}
34+
35+
// The folder doesn't exist, so there are no .ogg files.
36+
return false;
37+
}
38+
catch (Exception e)
39+
{
40+
// Handle any exceptions that may occur during the process.
41+
Log.Error($"Error checking audio files: {e} -- {e.Message}");
42+
return false;
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Gets a random audio file from the specified sounds folder.
48+
/// </summary>
49+
/// <returns>
50+
/// The path of a random .ogg file, or null if no files are found.
51+
/// </returns>
52+
public static string? GetRandomAudioFile()
53+
{
54+
try
55+
{
56+
string[] files = Directory.GetFiles(EntryPoint.Instance.Config.PathToAudios, "*.ogg");
57+
58+
// Return a random .ogg file, or null if no files are found.
59+
return files.Length > 0 ? files[UnityEngine.Random.Range(0, files.Length)] : null;
60+
}
61+
catch (Exception e)
62+
{
63+
// Handle any exceptions that may occur during the process.
64+
Log.Error($"Error getting random audio file: {e} -- {e.Message}");
65+
return null;
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)