diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..5f6a34d
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "csharpier": {
+ "version": "1.0.2",
+ "commands": [
+ "csharpier"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/CustomClasses/CustomClasses.cs b/CustomClasses/CustomClasses.cs
index 3423d52..8c40172 100644
--- a/CustomClasses/CustomClasses.cs
+++ b/CustomClasses/CustomClasses.cs
@@ -2,6 +2,7 @@ using Interactables.Interobjects.DoorUtils;
using InventorySystem.Items;
using InventorySystem.Items.Firearms.Modules;
using LabApi.Events.Arguments.PlayerEvents;
+using LabApi.Events.Arguments.Scp914Events;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
@@ -17,48 +18,65 @@ using Vector3 = UnityEngine.Vector3;
namespace CustomClasses;
-public class CustomClasses : Plugin
+///
+/// Main plugin class for CustomClasses. Handles plugin lifecycle and event subscriptions.
+///
+public sealed class CustomClasses : Plugin
{
- private readonly CustomClassManager _classManager = new();
+ private readonly CustomClassManager _classManager;
+ public CustomClasses()
+ {
+ _classManager = new CustomClassManager();
+ }
+
+ ///
public override string Name => "CustomClasses";
+ ///
public override string Author => "Code002Lover";
+ ///
public override Version Version { get; } = new(1, 0, 0);
+ ///
public override string Description => "Adds custom classes to the game";
+ ///
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
- public JanitorConfig JanitorConfig { get; set; } = new();
- public ResearchSubjectConfig ResearchSubjectConfig { get; set; } = new();
- public HeadGuardConfig HeadGuardConfig { get; set; } = new();
- public MedicConfig MedicConfig { get; set; } = new();
- public GamblerConfig GamblerConfig { get; set; } = new();
+ ///
+ /// Configuration for the Janitor class.
+ ///
+ public JanitorConfig JanitorConfig { get; private set; } = new();
+ ///
+ /// Configuration for the Research Subject class.
+ ///
+ public ResearchSubjectConfig ResearchSubjectConfig { get; private set; } = new();
+ ///
+ /// Configuration for the Head Guard class.
+ ///
+ public HeadGuardConfig HeadGuardConfig { get; private set; } = new();
+ ///
+ /// Configuration for the Medic class.
+ ///
+ public MedicConfig MedicConfig { get; private set; } = new();
+ ///
+ /// Configuration for the Gambler class.
+ ///
+ public GamblerConfig GamblerConfig { get; private set; } = new();
+ ///
public override void Enable()
{
PlayerEvents.Spawned += OnPlayerSpawned;
ServerEvents.RoundEnded += OnRoundEnded;
- Scp914Events.ProcessingPickup += ev =>
- {
- if (ev.Pickup.Type < ItemType.KeycardCustomTaskForce) return;
- //process custom upgrade
- var keycard = (KeycardPickup)ev.Pickup;
- var pickup = Pickup.Create(ItemType.KeycardMTFCaptain, keycard.Position);
- keycard.Destroy();
- pickup!.Spawn();
- };
- Scp914Events.ProcessingInventoryItem += ev =>
- {
- if (ev.Item.Type < ItemType.KeycardCustomTaskForce) return;
- //process custom upgrade
- var keycard = (KeycardItem)ev.Item;
- ev.Player.RemoveItem(keycard);
- ev.Player.AddItem(ItemType.KeycardMTFCaptain, ItemAddReason.Scp914Upgrade);
- };
+ Scp914Events.ProcessingPickup += OnScp914ProcessingPickup;
+ Scp914Events.ProcessingInventoryItem += OnScp914ProcessingInventoryItem;
}
+ ///
public override void Disable()
{
PlayerEvents.Spawned -= OnPlayerSpawned;
ServerEvents.RoundEnded -= OnRoundEnded;
+ Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup;
+ Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem;
}
private void OnRoundEnded(RoundEndedEventArgs ev)
@@ -74,8 +92,30 @@ public class CustomClasses : Plugin
if (_classManager.TryHandleSpawn(ev.Player, MedicConfig, typeof(MedicConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, GamblerConfig, typeof(GamblerConfig))) return;
}
+
+ private static void OnScp914ProcessingPickup(Scp914ProcessingPickupEventArgs ev)
+ {
+ if (ev.Pickup.Type < ItemType.KeycardCustomTaskForce) return;
+ // Process custom upgrade
+ if (ev.Pickup is not KeycardPickup keycard) return;
+ var pickup = Pickup.Create(ItemType.KeycardMTFCaptain, keycard.Position);
+ keycard.Destroy();
+ pickup?.Spawn();
+ }
+
+ private static void OnScp914ProcessingInventoryItem(Scp914ProcessingInventoryItemEventArgs ev)
+ {
+ if (ev.Item.Type < ItemType.KeycardCustomTaskForce) return;
+ // Process custom upgrade
+ if (ev.Item is not KeycardItem keycard) return;
+ ev.Player.RemoveItem(keycard);
+ ev.Player.AddItem(ItemType.KeycardMTFCaptain, ItemAddReason.Scp914Upgrade);
+ }
}
+///
+/// Manages custom class handlers and spawn state.
+///
public class CustomClassManager
{
private readonly Dictionary _handlers = new();
@@ -83,9 +123,11 @@ public class CustomClassManager
private readonly Random _random = new();
private readonly Dictionary _spawnStates = new();
+ ///
+ /// Initializes a new instance of the class and registers all handlers.
+ ///
public CustomClassManager()
{
- // Register handlers
RegisterHandler(new JanitorHandler(this));
RegisterHandler(new ResearchSubjectHandler(this));
RegisterHandler(new HeadGuardHandler());
@@ -93,30 +135,49 @@ public class CustomClassManager
RegisterHandler(new GamblerHandler());
}
+ ///
+ /// Teleports a player to a position near the specified location.
+ ///
+ /// The player to teleport.
+ /// The base position.
public void TeleportPlayerToAround(Player player, Vector3 position)
{
- player.Position = position + new Vector3(0, 1, 0) + new Vector3((float)(_random.NextDouble() * 2), 0,
- (float)(_random.NextDouble() * 2));
+ player.Position = position + new Vector3(0, 1, 0) + new Vector3((float)(_random.NextDouble() * 2), 0, (float)(_random.NextDouble() * 2));
}
+ ///
+ /// Registers a handler for a specific custom class config type.
+ ///
+ /// The config type.
+ /// The handler instance.
private void RegisterHandler(ICustomClassHandler handler) where T : CustomClassConfig
{
lock (_lock)
{
_spawnStates[typeof(T)] = new SpawnState();
+ _handlers[typeof(T)] = handler;
}
-
- _handlers[typeof(T)] = handler;
}
+ ///
+ /// Resets all spawn states for a new round.
+ ///
public void ResetSpawnStates()
{
lock (_lock)
{
- foreach (var key in _spawnStates.Keys.ToList()) _spawnStates[key] = new SpawnState();
+ foreach (var key in _spawnStates.Keys.ToList())
+ _spawnStates[key] = new SpawnState();
}
}
+ ///
+ /// Attempts to handle a player spawn for a given custom class config.
+ ///
+ /// The player to handle.
+ /// The config instance.
+ /// The config type.
+ /// True if the spawn was handled; otherwise, false.
public bool TryHandleSpawn(Player player, CustomClassConfig config, Type configType)
{
if (player.Role != config.RequiredRole) return false;
@@ -124,106 +185,126 @@ public class CustomClassManager
lock (_lock)
{
- var state = _spawnStates[configType];
+ if (!_spawnStates.TryGetValue(configType, out var state))
+ return false;
if (state.Spawns >= config.MaxSpawns)
{
Logger.Debug($"Max spawns reached {configType} - {player.Nickname}");
return false;
}
-
if (_random.NextDouble() > config.ChancePerPlayer)
{
Logger.Debug($"Chance not met {configType} - {player.Nickname}");
return false;
}
-
state.Spawns++;
-
Logger.Debug($"Player spawning {configType} - {player.Nickname} - {state.Spawns} / {config.MaxSpawns}");
-
- if (!_handlers.TryGetValue(configType, out var handler)) return false;
+ if (!_handlers.TryGetValue(configType, out var handler))
+ return false;
Timing.CallDelayed(0.5f, () => { handler.HandleSpawn(player, config, _random); });
return true;
}
}
}
+///
+/// Interface for custom class spawn handlers.
+///
public interface ICustomClassHandler
{
+ ///
+ /// Handles the logic for spawning a player as a custom class.
+ ///
+ /// The player to spawn.
+ /// The configuration for the custom class.
+ /// A random number generator.
void HandleSpawn(Player player, CustomClassConfig config, Random random);
}
+///
+/// Handler for the Janitor custom class.
+///
public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler
{
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
- var scp914 = Map.Rooms.First(r => r.Name == RoomName.Lcz914);
-
+ var scp914 = Map.Rooms.FirstOrDefault(r => r.Name == RoomName.Lcz914);
+ if (scp914 == null)
+ {
+ Logger.Error("LCZ 914 room not found for Janitor spawn.");
+ return;
+ }
manager.TeleportPlayerToAround(player, scp914.Position);
-
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
-
player.SendBroadcast("You're a Janitor!", 3);
}
}
+///
+/// Handler for the Research Subject custom class.
+///
public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler
{
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
- var scientist = Player.ReadyList.First(p => p.Role == RoleTypeId.Scientist);
+ var scientist = Player.ReadyList.FirstOrDefault(p => p.Role == RoleTypeId.Scientist);
+ if (scientist == null)
+ {
+ Logger.Error("No Scientist found for Research Subject spawn.");
+ return;
+ }
manager.TeleportPlayerToAround(player, scientist.Position);
-
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
-
player.SendBroadcast("You're a Research Subject!", 3);
}
}
+///
+/// Handler for the Head Guard custom class.
+///
public class HeadGuardHandler : ICustomClassHandler
{
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
player.RemoveItem(ItemType.KeycardGuard);
-
- KeycardItem.CreateCustomKeycardTaskForce(player, "Head Guard Keycard", $"HG. {player.Nickname}",
- new KeycardLevels(1, 1, 2), Color.blue, Color.cyan, "1", 0);
-
+ KeycardItem.CreateCustomKeycardTaskForce(player, "Head Guard Keycard", $"HG. {player.Nickname}", new KeycardLevels(1, 1, 2), Color.blue, Color.cyan, "1", 0);
player.AddItem(ItemType.Adrenaline, ItemAddReason.StartingItem);
-
player.RemoveItem(ItemType.ArmorLight);
player.AddItem(ItemType.ArmorCombat, ItemAddReason.StartingItem);
-
player.RemoveItem(ItemType.GunFSP9);
-
var pickup = Pickup.Create(ItemType.GunCrossvec, Vector3.one);
- var firearm = (FirearmPickup)pickup;
- if (firearm != null)
+ if (pickup is FirearmPickup firearm)
{
- firearm.Base.Template.TryGetModule(out MagazineModule magazine);
- magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
+ if (firearm.Base.Template.TryGetModule(out MagazineModule magazine))
+ {
+ magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
+ }
+ else
+ {
+ Logger.Error("Failed to get magazine module for Head Guard firearm.");
+ }
}
else
{
- Logger.Error("Failed to get firearm from pickup");
+ Logger.Error("Failed to get firearm from pickup for Head Guard.");
}
-
if (pickup != null) player.AddItem(pickup);
-
player.SetAmmo(ItemType.Ammo9x19, 120);
-
player.SendBroadcast("You're a Head Guard!", 3);
}
}
+///
+/// Handler for the Medic custom class.
+///
public class MedicHandler : ICustomClassHandler
{
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
@@ -233,11 +314,13 @@ public class MedicHandler : ICustomClassHandler
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
-
player.SendBroadcast("You're a Medic!", 3);
}
}
+///
+/// Base handler for simple item-giving custom classes.
+///
public abstract class SimpleAddItemHandler : ICustomClassHandler
{
public virtual void HandleSpawn(Player player, CustomClassConfig config, Random random)
@@ -250,6 +333,9 @@ public abstract class SimpleAddItemHandler : ICustomClassHandler
}
}
+///
+/// Handler for the Gambler custom class.
+///
public class GamblerHandler : SimpleAddItemHandler
{
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
@@ -259,23 +345,42 @@ public class GamblerHandler : SimpleAddItemHandler
}
}
-internal record SpawnState
-{
- public int Spawns;
-}
-
+///
+/// Represents the base configuration for a custom class.
+///
public abstract class CustomClassConfig
{
+ ///
+ /// Minimum number of players required for this class to spawn.
+ ///
public virtual int MinPlayers { get; set; } = 4;
+ ///
+ /// Chance per player for this class to spawn (0.0 - 1.0).
+ ///
public virtual double ChancePerPlayer { get; set; } = 0.7;
+ ///
+ /// Maximum number of spawns for this class per round.
+ ///
public virtual int MaxSpawns { get; set; } = 1;
+ ///
+ /// Items to give to the player on spawn.
+ ///
public virtual ItemType[] Items { get; set; } = [];
+ ///
+ /// The required role for this class to be considered.
+ ///
public virtual RoleTypeId RequiredRole { get; set; } = RoleTypeId.ClassD;
}
-public class ResearchSubjectConfig : CustomClassConfig;
+///
+/// Configuration for the Research Subject class.
+///
+public sealed class ResearchSubjectConfig : CustomClassConfig { }
-public class JanitorConfig : CustomClassConfig
+///
+/// Configuration for the Janitor class.
+///
+public sealed class JanitorConfig : CustomClassConfig
{
public override int MinPlayers { get; set; } = 5;
public override double ChancePerPlayer { get; set; } = 0.3;
@@ -283,13 +388,19 @@ public class JanitorConfig : CustomClassConfig
public override ItemType[] Items { get; set; } = [ItemType.KeycardJanitor];
}
-public class HeadGuardConfig : CustomClassConfig
+///
+/// Configuration for the Head Guard class.
+///
+public sealed class HeadGuardConfig : CustomClassConfig
{
public override int MinPlayers { get; set; } = 9;
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.FacilityGuard;
}
-public class MedicConfig : CustomClassConfig
+///
+/// Configuration for the Medic class.
+///
+public sealed class MedicConfig : CustomClassConfig
{
public override int MinPlayers { get; set; } = 5;
public override double ChancePerPlayer { get; set; } = 0.3;
@@ -298,9 +409,20 @@ public class MedicConfig : CustomClassConfig
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scientist;
}
-public class GamblerConfig : CustomClassConfig
+///
+/// Configuration for the Gambler class.
+///
+public sealed class GamblerConfig : CustomClassConfig
{
public override double ChancePerPlayer { get; set; } = 0.3;
public override int MaxSpawns { get; set; } = 5;
public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin];
+}
+
+///
+/// Tracks the spawn state for a custom class.
+///
+internal sealed record SpawnState
+{
+ public int Spawns;
}
\ No newline at end of file