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