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; using LabApi.Features.Wrappers; using LabApi.Loader.Features.Plugins; using MapGeneration; using MEC; using PlayerRoles; using UnityEngine; using Logger = LabApi.Features.Console.Logger; using Random = System.Random; using Vector3 = UnityEngine.Vector3; namespace CustomClasses; /// /// Main plugin class for CustomClasses. Handles plugin lifecycle and event subscriptions. /// public sealed class CustomClasses : Plugin { 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); /// /// 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 += 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) { _classManager.ResetSpawnStates(); } private void OnPlayerSpawned(PlayerSpawnedEventArgs ev) { if (_classManager.TryHandleSpawn(ev.Player, JanitorConfig, typeof(JanitorConfig))) return; if (_classManager.TryHandleSpawn(ev.Player, ResearchSubjectConfig, typeof(ResearchSubjectConfig))) return; if (_classManager.TryHandleSpawn(ev.Player, HeadGuardConfig, typeof(HeadGuardConfig))) return; 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(); private readonly object _lock = new(); private readonly Random _random = new(); private readonly Dictionary _spawnStates = new(); /// /// Initializes a new instance of the class and registers all handlers. /// public CustomClassManager() { RegisterHandler(new JanitorHandler(this)); RegisterHandler(new ResearchSubjectHandler(this)); RegisterHandler(new HeadGuardHandler()); RegisterHandler(new MedicHandler()); 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)); } /// /// 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; } } /// /// Resets all spawn states for a new round. /// public void ResetSpawnStates() { lock (_lock) { 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; if (Player.ReadyList.Count() <= config.MinPlayers) return false; lock (_lock) { 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; 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.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.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); 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); if (pickup is FirearmPickup firearm) { 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 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) { 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 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) { foreach (var spawnItem in config.Items) { player.AddItem(spawnItem, ItemAddReason.StartingItem); Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}"); } } } /// /// Handler for the Gambler custom class. /// public class GamblerHandler : SimpleAddItemHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { base.HandleSpawn(player, config, random); player.SendBroadcast("You're a Gambler!", 3); } } /// /// 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; } /// /// Configuration for the Research Subject class. /// public sealed class ResearchSubjectConfig : 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; public override int MaxSpawns { get; set; } = 2; public override ItemType[] Items { get; set; } = [ItemType.KeycardJanitor]; } /// /// 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; } /// /// 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; public override int MaxSpawns { get; set; } = 1; public override ItemType[] Items { get; set; } = [ItemType.Medkit, ItemType.Adrenaline]; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scientist; } /// /// 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; }