879 lines
32 KiB
C#
879 lines
32 KiB
C#
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;
|
|
|
|
|
|
/// <summary>
|
|
/// Main plugin class for CustomClasses. Handles plugin lifecycle and event subscriptions.
|
|
/// </summary>
|
|
public sealed class CustomClasses : Plugin
|
|
{
|
|
public readonly CustomClassManager ClassManager = new();
|
|
public SerpentsHandManager SerpentsHandManager;
|
|
|
|
/// <inheritdoc/>
|
|
public override string Name => "CustomClasses";
|
|
/// <inheritdoc/>
|
|
public override string Author => "Code002Lover";
|
|
/// <inheritdoc/>
|
|
public override Version Version { get; } = new(1, 0, 0);
|
|
/// <inheritdoc/>
|
|
public override string Description => "Adds custom classes to the game";
|
|
/// <inheritdoc/>
|
|
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
|
|
|
public const ushort BroadcastDuration = 10;
|
|
|
|
/// <summary>
|
|
/// Configuration for the Janitor class.
|
|
/// </summary>
|
|
public JanitorConfig JanitorConfig { get; private set; } = new();
|
|
/// <summary>
|
|
/// Configuration for the Research Subject class.
|
|
/// </summary>
|
|
public ResearchSubjectConfig ResearchSubjectConfig { get; private set; } = new();
|
|
/// <summary>
|
|
/// Configuration for the Head Guard class.
|
|
/// </summary>
|
|
public HeadGuardConfig HeadGuardConfig { get; private set; } = new();
|
|
/// <summary>
|
|
/// Configuration for the Medic class.
|
|
/// </summary>
|
|
public MedicConfig MedicConfig { get; private set; } = new();
|
|
/// <summary>
|
|
/// Configuration for the Gambler class.
|
|
/// </summary>
|
|
public GamblerConfig GamblerConfig { get; private set; } = new();
|
|
|
|
/// <summary>
|
|
/// Configuration for the ShadowStepper class.
|
|
/// </summary>
|
|
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<Player, Hint> 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<Scp914ItemProcessor>(out _))
|
|
{
|
|
var processor = itemBase.gameObject.AddComponent<StandardItemProcessor>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages custom class handlers and spawn state.
|
|
/// </summary>
|
|
public class CustomClassManager
|
|
{
|
|
private readonly Dictionary<Type, ICustomClassHandler> _handlers = new();
|
|
private readonly object _lock = new();
|
|
private readonly Random _random = new();
|
|
private readonly Dictionary<Type, SpawnState> _spawnStates = new();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CustomClassManager"/> class and registers all handlers.
|
|
/// </summary>
|
|
public CustomClassManager()
|
|
{
|
|
RegisterHandler<JanitorConfig>(new JanitorHandler(this));
|
|
RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler());
|
|
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler());
|
|
RegisterHandler<MedicConfig>(new MedicHandler());
|
|
RegisterHandler<GamblerConfig>(new GamblerHandler());
|
|
RegisterHandler<ShadowStepperConfig>(new ShadowStepperHandler());
|
|
RegisterHandler<MtfDemolitionistConfig>(new DemolitionistHandler());
|
|
RegisterHandler<ScoutConfig>(new ScoutHandler());
|
|
RegisterHandler<ExplosiveMasterConfig>(new ExplosiveMasterHandler());
|
|
RegisterHandler<FlashMasterConfig>(new FlashMasterHandler());
|
|
RegisterHandler<SerpentsHandConfig>(new SerpentsHandHandler(), new SerpentsHandState());
|
|
}
|
|
|
|
public SpawnState GetSpawnState(Type configType)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _spawnStates[configType];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Teleports a player to a position near the specified location.
|
|
/// </summary>
|
|
/// <param name="player">The player to teleport.</param>
|
|
/// <param name="position">The base position.</param>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a handler for a specific custom class config type.
|
|
/// </summary>
|
|
/// <typeparam name="T">The config type.</typeparam>
|
|
/// <param name="handler">The handler instance.</param>
|
|
/// <param name="spawnState">Optional custom spawn state</param>
|
|
private void RegisterHandler<T>(ICustomClassHandler handler, [CanBeNull] SpawnState spawnState = null) where T : CustomClassConfig
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_spawnStates[typeof(T)] = spawnState ?? new SpawnState();
|
|
_handlers[typeof(T)] = handler;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all spawn states for a new round.
|
|
/// </summary>
|
|
public void ResetSpawnStates()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
foreach (var key in _spawnStates.Keys.ToList())
|
|
_spawnStates[key].Reset();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to handle a player spawn for a given custom class config.
|
|
/// </summary>
|
|
/// <param name="player">The player to handle.</param>
|
|
/// <param name="config">The config instance.</param>
|
|
/// <param name="configType">The config type.</param>
|
|
/// <param name="preSpawn"></param>
|
|
/// <returns>True if the spawn was handled; otherwise, false.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for custom class spawn handlers.
|
|
/// </summary>
|
|
public interface ICustomClassHandler
|
|
{
|
|
/// <summary>
|
|
/// Handles the logic for spawning a player as a custom class.
|
|
/// </summary>
|
|
/// <param name="player">The player to spawn.</param>
|
|
/// <param name="config">The configuration for the custom class.</param>
|
|
/// <param name="random">A random number generator.</param>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Janitor custom class.
|
|
/// </summary>
|
|
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 <color=#A0A0A0>Janitor</color>!", CustomClasses.BroadcastDuration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Research Subject custom class.
|
|
/// </summary>
|
|
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 <color=#944710>Research Subject</color>!", CustomClasses.BroadcastDuration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Head Guard custom class.
|
|
/// </summary>
|
|
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 <color=#00B7EB>Head Guard</color>!", CustomClasses.BroadcastDuration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Medic custom class.
|
|
/// </summary>
|
|
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 <color=#727472>Medic</color>!", CustomClasses.BroadcastDuration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for ShadowStepper custom class.
|
|
/// </summary>
|
|
public class ShadowStepperHandler : CustomClassHandler
|
|
{
|
|
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
|
|
{
|
|
ApplyEffects(player);
|
|
|
|
player.SendBroadcast("You're a <color=#000000>ShadowStepper</color>!", 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<SilentWalk>(100,float.MaxValue);
|
|
player.ReferenceHub.playerEffectsController.ChangeState<Slowness>(10,float.MaxValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base handler for simple item-giving custom classes.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Gambler custom class.
|
|
/// </summary>
|
|
public class GamblerHandler : SimpleAddItemHandler
|
|
{
|
|
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
|
|
{
|
|
base.HandleSpawn(player, config, random);
|
|
player.SendBroadcast("You're a <color=#FF9966>Gambler</color>!", CustomClasses.BroadcastDuration);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handler for the Demolitionist custom class.
|
|
/// </summary>
|
|
public class DemolitionistHandler : SimpleAddItemHandler
|
|
{
|
|
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
|
|
{
|
|
base.HandleSpawn(player, config, random);
|
|
player.SendBroadcast("You're a <color=#FF9966>NTF Demolitionist</color>!", 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<MovementBoost>(40,32);
|
|
|
|
Timing.RunCoroutine(DecreaseSpeedBoost());
|
|
|
|
player.SendBroadcast("You're a <color=#FF9966>Scout</color> for your Faction! The rest of your Faction will spawn shortly.", CustomClasses.BroadcastDuration);
|
|
return;
|
|
|
|
IEnumerator<float> 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<MovementBoost>(intensity, 5);
|
|
}
|
|
player.ReferenceHub.playerEffectsController.ChangeState<MovementBoost>(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 <color=#FF0000>Explosive Master</color>!", CustomClasses.BroadcastDuration);
|
|
player.SendBroadcast("<color=red>IF YOU THROW THE GRENADE YOU WILL EXPLODE.</color>", 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<Slowness>();
|
|
}
|
|
|
|
void HandleUsing(PlayerUsingItemEventArgs ev)
|
|
{
|
|
if (ev.Player != player) return;
|
|
|
|
if (ev.UsableItem.Type is ItemType.GrenadeHE)
|
|
{
|
|
player.EnableEffect<Slowness>(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 <color=#FFFFFF>Flash Master</color>!", 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the base configuration for a custom class.
|
|
/// </summary>
|
|
public abstract class CustomClassConfig
|
|
{
|
|
/// <summary>
|
|
/// Minimum number of players required for this class to spawn.
|
|
/// </summary>
|
|
public virtual int MinPlayers { get; set; } = 4;
|
|
/// <summary>
|
|
/// Chance per player for this class to spawn (0.0 - 1.0).
|
|
/// </summary>
|
|
public virtual double ChancePerPlayer { get; set; } = 0.7;
|
|
/// <summary>
|
|
/// Maximum number of spawns for this class per round.
|
|
/// </summary>
|
|
public virtual int MaxSpawns { get; set; } = 1;
|
|
/// <summary>
|
|
/// Items to give to the player on spawn.
|
|
/// </summary>
|
|
public virtual ItemType[] Items { get; set; } = [];
|
|
/// <summary>
|
|
/// The required role for this class to be considered.
|
|
/// </summary>
|
|
public virtual RoleTypeId RequiredRole { get; set; } = RoleTypeId.ClassD;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration for the Research Subject class.
|
|
/// </summary>
|
|
public sealed class ResearchSubjectConfig : CustomClassConfig;
|
|
|
|
/// <summary>
|
|
/// Configuration for the Janitor class.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration for the Head Guard class.
|
|
/// </summary>
|
|
public sealed class HeadGuardConfig : CustomClassConfig
|
|
{
|
|
public override int MinPlayers { get; set; } = 9;
|
|
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.FacilityGuard;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration for the Medic class.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration for the Gambler class.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration for the Shadow Stepper class.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks the spawn state for a custom class.
|
|
/// </summary>
|
|
public record SpawnState
|
|
{
|
|
public int Spawns;
|
|
|
|
public virtual void Reset()
|
|
{
|
|
Spawns = 0;
|
|
}
|
|
} |