using System.Diagnostics.CodeAnalysis; using CustomPlayerEffects; using HintServiceMeow.Core.Models.Hints; using Interactables.Interobjects.DoorUtils; using InventorySystem; using InventorySystem.Items; using InventorySystem.Items.Firearms.Modules; using JetBrains.Annotations; 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 Scp914.Processors; 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 { public readonly CustomClassManager ClassManager = new(); public SerpentsHandManager SerpentsHandManager; /// 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 const ushort BroadcastDuration = 10; /// /// 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(); /// /// Configuration for the ShadowStepper class. /// public ShadowStepperConfig ShadowStepperConfig { get; private set; } = new(); public MtfDemolitionistConfig MtfDemolitionistConfig { get; private set; } = new(); public ScoutConfig ScoutConfig { get; private set; } = new(); public ExplosiveMasterConfig ExplosiveMasterConfig { get; private set; } = new(); public FlashMasterConfig FlashMasterConfig { get; private set; } = new(); public SerpentsHandConfig SerpentsHandConfig { get; private set; } = new(); internal readonly Dictionary Hints = new(); public static CustomClasses Instance { get; private set; } private const string Message = "PAf4jcb1UobNURH4USLKhBQtgR/GTRD1isf6h9DvUSGmFMbdh9b/isrtgBKmGpa4HMbAhAX4gRf0Cez4h9L6UR/qh9DsUSCyCAfyhcb4gRjujBGmisQ5USD8URK0"; public override void Enable() { const string customAlphabet = "abcdefABCDEFGHIJKLMNPQRSTUghijklmnopqrstuvwxyz0123456789+/=VWXYZ"; const string standardAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var standardized = ""; foreach (var c in Message) { var index = customAlphabet.IndexOf(c); standardized += index >= 0 ? standardAlphabet[index] : c; } // Then decode using standard base64 var decodedBytes = Convert.FromBase64String(standardized); var decodedMessage = System.Text.Encoding.UTF8.GetString(decodedBytes); Logger.Info(decodedMessage); PlayerEvents.Spawned += OnPlayerSpawned; ServerEvents.RoundEnded += OnRoundEnded; Scp914Events.ProcessingPickup += OnScp914ProcessingPickup; Scp914Events.ProcessingInventoryItem += OnScp914ProcessingInventoryItem; ServerEvents.WaveTeamSelected += OnWaveTeamSelected; ServerEvents.WaveRespawning += OnWaveRespawning; PlayerEvents.UsedItem += OnItemUsed; ServerEvents.GeneratorActivated += OnGeneratorEngaged; SerpentsHandManager = new SerpentsHandManager(this); Timing.RunCoroutine(SerpentsHandManager.UpdateSerpentsHandHint()); if (InventoryItemLoader.AvailableItems.TryGetValue(ItemType.KeycardCustomTaskForce, out var itemBase)) { if (!itemBase.TryGetComponent(out _)) { var processor = itemBase.gameObject.AddComponent(); var type = processor.GetType(); var fields = new[] { "_roughOutputs", "_coarseOutputs", "_oneToOneOutputs", "_fineOutputs", "_veryFineOutputs" }; var output = new[] { ItemType.KeycardMTFCaptain }; foreach (var fieldName in fields) { var field = type.GetField(fieldName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); field?.SetValue(processor, output); } } } Instance = this; } /// public override void Disable() { PlayerEvents.Spawned -= OnPlayerSpawned; ServerEvents.RoundEnded -= OnRoundEnded; Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup; Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem; ServerEvents.WaveTeamSelected -= OnWaveTeamSelected; ServerEvents.WaveRespawning -= OnWaveRespawning; PlayerEvents.UsedItem -= OnItemUsed; ServerEvents.GeneratorActivated -= OnGeneratorEngaged; Instance = null; } private void OnGeneratorEngaged(GeneratorActivatedEventArgs ev) { if (ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is not SerpentsHandState state) return; state.ExtraChance += 2.5f; } private void OnItemUsed(PlayerUsedItemEventArgs ev) { if (ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is not SerpentsHandState state) return; switch (ev.UsableItem.Type) { case ItemType.SCP268 or ItemType.SCP1576 or ItemType.SCP1344: state.ExtraChance += 1; break; case ItemType.SCP500 or ItemType.SCP207 or ItemType.SCP1853 or ItemType.AntiSCP207 or ItemType.SCP018 or ItemType.SCP2176: state.ExtraChance += 2; break; case ItemType.SCP244a or ItemType.SCP244b: state.ExtraChance += 0.5f; break; } } [SuppressMessage("ReSharper", "RedundantJumpStatement")] private void OnWaveTeamSelected(WaveTeamSelectedEventArgs ev) { var spectators = Player.ReadyList.Where(p => p.Role == RoleTypeId.Spectator).ToList(); if (spectators.Count <= 1) return; var random = new Random(); var spectator = spectators[random.Next(spectators.Count-1)]; if (ev.Wave == RespawnWaves.MiniChaosWave && ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is SerpentsHandState { HasSpawned: false } state) { if (random.Next(0, 100) > SerpentsHandConfig.BaseChance + state.ExtraChance) { state.SetSpawned(); state.SetWillSpawn(); ClassManager.TryHandleSpawn(spectator, ScoutConfig, typeof(ScoutConfig), () => { if (!ClassManager.ForceSpawn(spectator, SerpentsHandConfig, typeof(SerpentsHandConfig), PreSpawn)) Logger.Error("Serpents Hand didn't spawn"); return; void PreSpawn() { SerpentsHandManager.PreSpawn(spectator); } }); return; } } if (ClassManager.TryHandleSpawn(spectator, ScoutConfig, typeof(ScoutConfig), () => { spectator.SetRole(ev.Wave.Faction == Faction.FoundationStaff ? RoleTypeId.NtfPrivate : RoleTypeId.ChaosConscript, RoleChangeReason.Respawn, RoleSpawnFlags.UseSpawnpoint); })) return; } private void OnWaveRespawning(WaveRespawningEventArgs ev) { if (ev.Wave != RespawnWaves.MiniChaosWave) return; if (ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is not SerpentsHandState state) return; if (!state.WillSpawn) return; Cassie.Message("pitch_0.23 .G4 yield_03 .G4 pitch_1 Space yield_0.65 time breach detected near site yield_0.45 entrance yield_1.5 Security yield_0.8 pitch_0.95 Personnel pitch_1 proceed jam_01_2 .G6 with yield_0.4 pitch_0.45 jam_02_3 .G3 yield_0.15 pitch_0.35 .G2 jam_01_5 yield_0.15 pitch_0.2 .G1 pitch_0.9 yield_0.8 protocol", true, false, customSubtitles: "Space-time breach detected near site entrance. Security Personnel proceed with {DATA-EXPUNGED} protocol."); state.SetWillNotSpawn(); ev.IsAllowed = false; foreach (var evSpawningPlayer in ev.SpawningPlayers) { if (!ClassManager.ForceSpawn(evSpawningPlayer, SerpentsHandConfig, typeof(SerpentsHandConfig), PreSpawn)) Logger.Error("Serpents Hand didn't spawn"); continue; void PreSpawn() { SerpentsHandManager.PreSpawn(evSpawningPlayer); } } } private void OnRoundEnded(RoundEndedEventArgs ev) { ClassManager.ResetSpawnStates(); } [SuppressMessage("ReSharper", "RedundantJumpStatement")] private void OnPlayerSpawned(PlayerSpawnedEventArgs ev) { ev.Player.CustomInfo = ""; if (ClassManager.TryHandleSpawn(ev.Player, JanitorConfig, typeof(JanitorConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, ResearchSubjectConfig, typeof(ResearchSubjectConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, HeadGuardConfig, typeof(HeadGuardConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, MedicConfig, typeof(MedicConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, GamblerConfig, typeof(GamblerConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, ShadowStepperConfig, typeof(ShadowStepperConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, MtfDemolitionistConfig, typeof(MtfDemolitionistConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, ExplosiveMasterConfig, typeof(ExplosiveMasterConfig), null)) return; if (ClassManager.TryHandleSpawn(ev.Player, FlashMasterConfig, typeof(FlashMasterConfig), null)) return; } private static void OnScp914ProcessingPickup(Scp914ProcessingPickupEventArgs ev) { // Process custom upgrade if (ev.Pickup is not KeycardPickup keycard) { Logger.Debug($"Keycard not found for SCP-914 pickup {ev.Pickup.Serial}"); return; } if (keycard.Type < ItemType.KeycardCustomTaskForce) { Logger.Debug($"Keycard not a custom card {ev.Pickup.Serial}"); return; } var pickup = Pickup.Create(ItemType.KeycardMTFCaptain, keycard.Position); keycard.Destroy(); pickup?.Spawn(); } private static void OnScp914ProcessingInventoryItem(Scp914ProcessingInventoryItemEventArgs ev) { // Process custom upgrade if (ev.Item is not KeycardItem keycard) return; if (!keycard.Base.Customizable) 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()); RegisterHandler(new HeadGuardHandler()); RegisterHandler(new MedicHandler()); RegisterHandler(new GamblerHandler()); RegisterHandler(new ShadowStepperHandler()); RegisterHandler(new DemolitionistHandler()); RegisterHandler(new ScoutHandler()); RegisterHandler(new ExplosiveMasterHandler()); RegisterHandler(new FlashMasterHandler()); RegisterHandler(new SerpentsHandHandler(), new SerpentsHandState()); } public SpawnState GetSpawnState(Type configType) { lock (_lock) { return _spawnStates[configType]; } } /// /// 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 - 1), 0, (float)(_random.NextDouble() * 2 - 1)); } /// /// Registers a handler for a specific custom class config type. /// /// The config type. /// The handler instance. /// Optional custom spawn state private void RegisterHandler(ICustomClassHandler handler, [CanBeNull] SpawnState spawnState = null) where T : CustomClassConfig { lock (_lock) { _spawnStates[typeof(T)] = spawnState ?? 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].Reset(); } } /// /// 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, Action preSpawn) { 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}"); return ForceSpawn(player, config, configType, preSpawn); } } public bool ForceSpawn(Player player, CustomClassConfig config, Type configType, Action preSpawn) { if (!_handlers.TryGetValue(configType, out var handler)) return false; preSpawn?.Invoke(); 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); } public abstract class CustomClassHandler: ICustomClassHandler { public abstract void HandleSpawn(Player player, CustomClassConfig config, Random random); public virtual void HandleEscape(Player player, CustomClassConfig config) { //Intentionally left blank } } /// /// Handler for the Janitor custom class. /// public class JanitorHandler(CustomClassManager manager) : CustomClassHandler { public override 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!", CustomClasses.BroadcastDuration); } } /// /// Handler for the Research Subject custom class. /// public class ResearchSubjectHandler : CustomClassHandler { public override 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; } player.Position = 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!", CustomClasses.BroadcastDuration); } } /// /// Handler for the Head Guard custom class. /// public class HeadGuardHandler : CustomClassHandler { public override 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.Serial, 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."); } player.AddItem(pickup!); player.SetAmmo(ItemType.Ammo9x19, 120); player.SendBroadcast("You're a Head Guard!", CustomClasses.BroadcastDuration); } } /// /// Handler for the Medic custom class. /// public class MedicHandler : CustomClassHandler { public override 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!", CustomClasses.BroadcastDuration); } } /// /// Handler for ShadowStepper custom class. /// public class ShadowStepperHandler : CustomClassHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { ApplyEffects(player); player.SendBroadcast("You're a ShadowStepper!", CustomClasses.BroadcastDuration); } public override void HandleEscape(Player player, CustomClassConfig config) { base.HandleEscape(player, config); ApplyEffects(player); } private static void ApplyEffects(Player player) { player.ReferenceHub.playerEffectsController.ChangeState(100,float.MaxValue); player.ReferenceHub.playerEffectsController.ChangeState(10,float.MaxValue); } } /// /// Base handler for simple item-giving custom classes. /// public abstract class SimpleAddItemHandler : CustomClassHandler { public override 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!", CustomClasses.BroadcastDuration); } } /// /// Handler for the Demolitionist custom class. /// public class DemolitionistHandler : SimpleAddItemHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { base.HandleSpawn(player, config, random); player.SendBroadcast("You're a NTF Demolitionist!", CustomClasses.BroadcastDuration); } } public class ScoutHandler : SimpleAddItemHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { base.HandleSpawn(player, config, random); const ItemType gun = ItemType.GunCrossvec; var gunPickup = Pickup.Create(gun, Vector3.one); if (gunPickup is FirearmPickup firearm) { if (firearm.Base.Template.TryGetModule(out MagazineModule magazine)) { magazine.ServerSetInstanceAmmo(firearm.Serial, magazine.AmmoMax); } else { Logger.Error("Failed to get magazine module for Serpents Hand firearm."); } } else { Logger.Error("Failed to get firearm from pickup for Serpents Hand."); } player.AddItem(gunPickup!); player.ReferenceHub.playerEffectsController.ChangeState(40,32); Timing.RunCoroutine(DecreaseSpeedBoost()); player.SendBroadcast("You're a Scout for your Faction! The rest of your Faction will spawn shortly.", CustomClasses.BroadcastDuration); return; IEnumerator DecreaseSpeedBoost() { const float baseIntensity = 40f; const float baseDuration = 30f; const float minimumIntensity = 10f; const float deltaIntensity = baseIntensity - minimumIntensity; const float intensityStep = deltaIntensity / baseDuration; var duration = baseDuration; while (duration-- > 0) { yield return Timing.WaitForSeconds(1f); var intensity = (byte) (baseIntensity - intensityStep * (baseDuration - duration)); player.ReferenceHub.playerEffectsController.ChangeState(intensity, 5); } player.ReferenceHub.playerEffectsController.ChangeState(10,9999); } } } public class ExplosiveMasterHandler : SimpleAddItemHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { base.HandleSpawn(player, config, random); player.SendBroadcast("You're an Explosive Master!", CustomClasses.BroadcastDuration); player.SendBroadcast("IF YOU THROW THE GRENADE YOU WILL EXPLODE.", CustomClasses.BroadcastDuration); PlayerEvents.ThrowingProjectile += HandleGrenade; PlayerEvents.Spawning += HandlePlayerSpawn; PlayerEvents.UsingItem += HandleUsing; PlayerEvents.CancellingUsingItem += HandleCancel; return; void Unregister() { PlayerEvents.ThrowingProjectile -= HandleGrenade; PlayerEvents.Spawning -= HandlePlayerSpawn; PlayerEvents.UsingItem -= HandleUsing; PlayerEvents.CancellingUsingItem -= HandleCancel; } void HandleCancel(PlayerCancellingUsingItemEventArgs ev) { if (ev.Player != player) return; if (ev.UsableItem.Type is not ItemType.GrenadeHE) return; player.DisableEffect(); } void HandleUsing(PlayerUsingItemEventArgs ev) { if (ev.Player != player) return; if (ev.UsableItem.Type is ItemType.GrenadeHE) { player.EnableEffect(10, float.MaxValue); } } void HandleGrenade(PlayerThrowingProjectileEventArgs ev) { if (ev.Player != player) return; if (ev.ThrowableItem.Type is not ItemType.GrenadeHE) return; TimedGrenadeProjectile.SpawnActive(ev.Player.Position, ItemType.GrenadeHE, ev.Player); TimedGrenadeProjectile.SpawnActive(ev.Player.Position, ItemType.GrenadeHE, ev.Player); PlayerEvents.ThrowingProjectile -= HandleGrenade; } void HandlePlayerSpawn(PlayerSpawningEventArgs ev) { if(ev.Player != player) return; Unregister(); } } } public class FlashMasterHandler : SimpleAddItemHandler { public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { base.HandleSpawn(player, config, random); player.SendBroadcast("You're a Flash Master!", CustomClasses.BroadcastDuration); PlayerEvents.ThrowingProjectile += HandleGrenade; PlayerEvents.Spawning += HandlePlayerSpawn; return; void Unregister() { PlayerEvents.ThrowingProjectile -= HandleGrenade; } void HandleGrenade(PlayerThrowingProjectileEventArgs ev) { if (ev.Player != player) return; if (ev.ThrowableItem.Type is not ItemType.GrenadeFlash) return; for (var i = 0; i < 3; i++) TimedGrenadeProjectile.SpawnActive(ev.Player.Position, ItemType.GrenadeFlash, ev.Player); PlayerEvents.ThrowingProjectile -= HandleGrenade; } void HandlePlayerSpawn(PlayerSpawningEventArgs ev) { if(ev.Player != player) return; Unregister(); } } } /// /// 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]; } /// /// Configuration for the Shadow Stepper class. /// public sealed class ShadowStepperConfig : CustomClassConfig; public sealed class MtfDemolitionistConfig : CustomClassConfig { public override double ChancePerPlayer { get; set; } = 0.2; public override int MaxSpawns { get; set; } = int.MaxValue; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.NtfPrivate; public override ItemType[] Items { get; set; } = [ItemType.GrenadeHE, ItemType.GrenadeHE, ItemType.GrenadeHE]; } public sealed class ScoutConfig : CustomClassConfig { public override double ChancePerPlayer { get; set; } = 0.25; public override int MaxSpawns { get; set; } = int.MaxValue; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Spectator; public override ItemType[] Items { get; set; } = [ItemType.GrenadeFlash, ItemType.KeycardMTFOperative, ItemType.ArmorLight, ItemType.Medkit ,ItemType.Ammo9x19, ItemType.Ammo9x19, ItemType.Ammo9x19, ItemType.Ammo9x19, ItemType.Ammo9x19, ItemType.Ammo9x19]; } public sealed class ExplosiveMasterConfig : CustomClassConfig { public override double ChancePerPlayer { get; set; } = 0.2; public override int MaxSpawns { get; set; } = int.MaxValue; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.ChaosMarauder; public override ItemType[] Items { get; set; } = [ItemType.GrenadeHE]; } public sealed class FlashMasterConfig : CustomClassConfig { public override double ChancePerPlayer { get; set; } = 0.0; public override int MaxSpawns { get; set; } = int.MaxValue; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.ChaosMarauder; public override ItemType[] Items { get; set; } = [ItemType.GrenadeFlash]; } /// /// Tracks the spawn state for a custom class. /// public record SpawnState { public int Spawns; public virtual void Reset() { Spawns = 0; } }