This commit is contained in:
code002lover 2025-06-06 13:37:21 +02:00
parent 73a4da1edd
commit 67e3d6ceaa
2 changed files with 204 additions and 69 deletions

13
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.0.2",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

View File

@ -2,6 +2,7 @@ using Interactables.Interobjects.DoorUtils;
using InventorySystem.Items; using InventorySystem.Items;
using InventorySystem.Items.Firearms.Modules; using InventorySystem.Items.Firearms.Modules;
using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Arguments.Scp914Events;
using LabApi.Events.Arguments.ServerEvents; using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers; using LabApi.Events.Handlers;
using LabApi.Features; using LabApi.Features;
@ -17,48 +18,65 @@ using Vector3 = UnityEngine.Vector3;
namespace CustomClasses; namespace CustomClasses;
public class CustomClasses : Plugin /// <summary>
/// Main plugin class for CustomClasses. Handles plugin lifecycle and event subscriptions.
/// </summary>
public sealed class CustomClasses : Plugin
{ {
private readonly CustomClassManager _classManager = new(); private readonly CustomClassManager _classManager;
public CustomClasses()
{
_classManager = new CustomClassManager();
}
/// <inheritdoc/>
public override string Name => "CustomClasses"; public override string Name => "CustomClasses";
/// <inheritdoc/>
public override string Author => "Code002Lover"; public override string Author => "Code002Lover";
/// <inheritdoc/>
public override Version Version { get; } = new(1, 0, 0); public override Version Version { get; } = new(1, 0, 0);
/// <inheritdoc/>
public override string Description => "Adds custom classes to the game"; public override string Description => "Adds custom classes to the game";
/// <inheritdoc/>
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
public JanitorConfig JanitorConfig { get; set; } = new(); /// <summary>
public ResearchSubjectConfig ResearchSubjectConfig { get; set; } = new(); /// Configuration for the Janitor class.
public HeadGuardConfig HeadGuardConfig { get; set; } = new(); /// </summary>
public MedicConfig MedicConfig { get; set; } = new(); public JanitorConfig JanitorConfig { get; private set; } = new();
public GamblerConfig GamblerConfig { get; 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();
/// <inheritdoc/>
public override void Enable() public override void Enable()
{ {
PlayerEvents.Spawned += OnPlayerSpawned; PlayerEvents.Spawned += OnPlayerSpawned;
ServerEvents.RoundEnded += OnRoundEnded; ServerEvents.RoundEnded += OnRoundEnded;
Scp914Events.ProcessingPickup += ev => Scp914Events.ProcessingPickup += OnScp914ProcessingPickup;
{ Scp914Events.ProcessingInventoryItem += OnScp914ProcessingInventoryItem;
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);
};
} }
/// <inheritdoc/>
public override void Disable() public override void Disable()
{ {
PlayerEvents.Spawned -= OnPlayerSpawned; PlayerEvents.Spawned -= OnPlayerSpawned;
ServerEvents.RoundEnded -= OnRoundEnded; ServerEvents.RoundEnded -= OnRoundEnded;
Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup;
Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem;
} }
private void OnRoundEnded(RoundEndedEventArgs ev) 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, MedicConfig, typeof(MedicConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, GamblerConfig, typeof(GamblerConfig))) 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);
}
} }
/// <summary>
/// Manages custom class handlers and spawn state.
/// </summary>
public class CustomClassManager public class CustomClassManager
{ {
private readonly Dictionary<Type, ICustomClassHandler> _handlers = new(); private readonly Dictionary<Type, ICustomClassHandler> _handlers = new();
@ -83,9 +123,11 @@ public class CustomClassManager
private readonly Random _random = new(); private readonly Random _random = new();
private readonly Dictionary<Type, SpawnState> _spawnStates = 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() public CustomClassManager()
{ {
// Register handlers
RegisterHandler<JanitorConfig>(new JanitorHandler(this)); RegisterHandler<JanitorConfig>(new JanitorHandler(this));
RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler(this)); RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler(this));
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler()); RegisterHandler<HeadGuardConfig>(new HeadGuardHandler());
@ -93,30 +135,49 @@ public class CustomClassManager
RegisterHandler<GamblerConfig>(new GamblerHandler()); RegisterHandler<GamblerConfig>(new GamblerHandler());
} }
/// <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) public void TeleportPlayerToAround(Player player, Vector3 position)
{ {
player.Position = position + new Vector3(0, 1, 0) + new Vector3((float)(_random.NextDouble() * 2), 0, player.Position = position + new Vector3(0, 1, 0) + new Vector3((float)(_random.NextDouble() * 2), 0, (float)(_random.NextDouble() * 2));
(float)(_random.NextDouble() * 2));
} }
/// <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>
private void RegisterHandler<T>(ICustomClassHandler handler) where T : CustomClassConfig private void RegisterHandler<T>(ICustomClassHandler handler) where T : CustomClassConfig
{ {
lock (_lock) lock (_lock)
{ {
_spawnStates[typeof(T)] = new SpawnState(); _spawnStates[typeof(T)] = new SpawnState();
_handlers[typeof(T)] = handler;
} }
_handlers[typeof(T)] = handler;
} }
/// <summary>
/// Resets all spawn states for a new round.
/// </summary>
public void ResetSpawnStates() public void ResetSpawnStates()
{ {
lock (_lock) lock (_lock)
{ {
foreach (var key in _spawnStates.Keys.ToList()) _spawnStates[key] = new SpawnState(); foreach (var key in _spawnStates.Keys.ToList())
_spawnStates[key] = new SpawnState();
} }
} }
/// <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>
/// <returns>True if the spawn was handled; otherwise, false.</returns>
public bool TryHandleSpawn(Player player, CustomClassConfig config, Type configType) public bool TryHandleSpawn(Player player, CustomClassConfig config, Type configType)
{ {
if (player.Role != config.RequiredRole) return false; if (player.Role != config.RequiredRole) return false;
@ -124,106 +185,126 @@ public class CustomClassManager
lock (_lock) lock (_lock)
{ {
var state = _spawnStates[configType]; if (!_spawnStates.TryGetValue(configType, out var state))
return false;
if (state.Spawns >= config.MaxSpawns) if (state.Spawns >= config.MaxSpawns)
{ {
Logger.Debug($"Max spawns reached {configType} - {player.Nickname}"); Logger.Debug($"Max spawns reached {configType} - {player.Nickname}");
return false; return false;
} }
if (_random.NextDouble() > config.ChancePerPlayer) if (_random.NextDouble() > config.ChancePerPlayer)
{ {
Logger.Debug($"Chance not met {configType} - {player.Nickname}"); Logger.Debug($"Chance not met {configType} - {player.Nickname}");
return false; return false;
} }
state.Spawns++; state.Spawns++;
Logger.Debug($"Player spawning {configType} - {player.Nickname} - {state.Spawns} / {config.MaxSpawns}"); Logger.Debug($"Player spawning {configType} - {player.Nickname} - {state.Spawns} / {config.MaxSpawns}");
if (!_handlers.TryGetValue(configType, out var handler))
if (!_handlers.TryGetValue(configType, out var handler)) return false; return false;
Timing.CallDelayed(0.5f, () => { handler.HandleSpawn(player, config, _random); }); Timing.CallDelayed(0.5f, () => { handler.HandleSpawn(player, config, _random); });
return true; return true;
} }
} }
} }
/// <summary>
/// Interface for custom class spawn handlers.
/// </summary>
public interface ICustomClassHandler 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); void HandleSpawn(Player player, CustomClassConfig config, Random random);
} }
/// <summary>
/// Handler for the Janitor custom class.
/// </summary>
public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler
{ {
public void HandleSpawn(Player player, CustomClassConfig config, Random random) 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); manager.TeleportPlayerToAround(player, scp914.Position);
foreach (var spawnItem in config.Items) foreach (var spawnItem in config.Items)
{ {
player.AddItem(spawnItem, ItemAddReason.StartingItem); player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}"); Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
} }
player.SendBroadcast("You're a <color=#A0A0A0>Janitor</color>!", 3); player.SendBroadcast("You're a <color=#A0A0A0>Janitor</color>!", 3);
} }
} }
/// <summary>
/// Handler for the Research Subject custom class.
/// </summary>
public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler
{ {
public void HandleSpawn(Player player, CustomClassConfig config, Random random) 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); manager.TeleportPlayerToAround(player, scientist.Position);
foreach (var spawnItem in config.Items) foreach (var spawnItem in config.Items)
{ {
player.AddItem(spawnItem, ItemAddReason.StartingItem); player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}"); Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
} }
player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", 3); player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", 3);
} }
} }
/// <summary>
/// Handler for the Head Guard custom class.
/// </summary>
public class HeadGuardHandler : ICustomClassHandler public class HeadGuardHandler : ICustomClassHandler
{ {
public void HandleSpawn(Player player, CustomClassConfig config, Random random) public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{ {
player.RemoveItem(ItemType.KeycardGuard); 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.AddItem(ItemType.Adrenaline, ItemAddReason.StartingItem);
player.RemoveItem(ItemType.ArmorLight); player.RemoveItem(ItemType.ArmorLight);
player.AddItem(ItemType.ArmorCombat, ItemAddReason.StartingItem); player.AddItem(ItemType.ArmorCombat, ItemAddReason.StartingItem);
player.RemoveItem(ItemType.GunFSP9); player.RemoveItem(ItemType.GunFSP9);
var pickup = Pickup.Create(ItemType.GunCrossvec, Vector3.one); var pickup = Pickup.Create(ItemType.GunCrossvec, Vector3.one);
var firearm = (FirearmPickup)pickup; if (pickup is FirearmPickup firearm)
if (firearm != null)
{ {
firearm.Base.Template.TryGetModule(out MagazineModule magazine); if (firearm.Base.Template.TryGetModule(out MagazineModule magazine))
magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax); {
magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
}
else
{
Logger.Error("Failed to get magazine module for Head Guard firearm.");
}
} }
else 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); if (pickup != null) player.AddItem(pickup);
player.SetAmmo(ItemType.Ammo9x19, 120); player.SetAmmo(ItemType.Ammo9x19, 120);
player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3); player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3);
} }
} }
/// <summary>
/// Handler for the Medic custom class.
/// </summary>
public class MedicHandler : ICustomClassHandler public class MedicHandler : ICustomClassHandler
{ {
public void HandleSpawn(Player player, CustomClassConfig config, Random random) public void HandleSpawn(Player player, CustomClassConfig config, Random random)
@ -233,11 +314,13 @@ public class MedicHandler : ICustomClassHandler
player.AddItem(spawnItem, ItemAddReason.StartingItem); player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}"); Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
} }
player.SendBroadcast("You're a <color=#727472>Medic</color>!", 3); player.SendBroadcast("You're a <color=#727472>Medic</color>!", 3);
} }
} }
/// <summary>
/// Base handler for simple item-giving custom classes.
/// </summary>
public abstract class SimpleAddItemHandler : ICustomClassHandler public abstract class SimpleAddItemHandler : ICustomClassHandler
{ {
public virtual void HandleSpawn(Player player, CustomClassConfig config, Random random) public virtual void HandleSpawn(Player player, CustomClassConfig config, Random random)
@ -250,6 +333,9 @@ public abstract class SimpleAddItemHandler : ICustomClassHandler
} }
} }
/// <summary>
/// Handler for the Gambler custom class.
/// </summary>
public class GamblerHandler : SimpleAddItemHandler public class GamblerHandler : SimpleAddItemHandler
{ {
public override void HandleSpawn(Player player, CustomClassConfig config, Random random) public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
@ -259,23 +345,42 @@ public class GamblerHandler : SimpleAddItemHandler
} }
} }
internal record SpawnState /// <summary>
{ /// Represents the base configuration for a custom class.
public int Spawns; /// </summary>
}
public abstract class CustomClassConfig public abstract class CustomClassConfig
{ {
/// <summary>
/// Minimum number of players required for this class to spawn.
/// </summary>
public virtual int MinPlayers { get; set; } = 4; 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; 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; public virtual int MaxSpawns { get; set; } = 1;
/// <summary>
/// Items to give to the player on spawn.
/// </summary>
public virtual ItemType[] Items { get; set; } = []; 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; public virtual RoleTypeId RequiredRole { get; set; } = RoleTypeId.ClassD;
} }
public class ResearchSubjectConfig : CustomClassConfig; /// <summary>
/// Configuration for the Research Subject class.
/// </summary>
public sealed class ResearchSubjectConfig : CustomClassConfig { }
public class JanitorConfig : CustomClassConfig /// <summary>
/// Configuration for the Janitor class.
/// </summary>
public sealed class JanitorConfig : CustomClassConfig
{ {
public override int MinPlayers { get; set; } = 5; public override int MinPlayers { get; set; } = 5;
public override double ChancePerPlayer { get; set; } = 0.3; 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 override ItemType[] Items { get; set; } = [ItemType.KeycardJanitor];
} }
public class HeadGuardConfig : CustomClassConfig /// <summary>
/// Configuration for the Head Guard class.
/// </summary>
public sealed class HeadGuardConfig : CustomClassConfig
{ {
public override int MinPlayers { get; set; } = 9; public override int MinPlayers { get; set; } = 9;
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.FacilityGuard; public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.FacilityGuard;
} }
public class MedicConfig : CustomClassConfig /// <summary>
/// Configuration for the Medic class.
/// </summary>
public sealed class MedicConfig : CustomClassConfig
{ {
public override int MinPlayers { get; set; } = 5; public override int MinPlayers { get; set; } = 5;
public override double ChancePerPlayer { get; set; } = 0.3; 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 override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scientist;
} }
public class GamblerConfig : CustomClassConfig /// <summary>
/// Configuration for the Gambler class.
/// </summary>
public sealed class GamblerConfig : CustomClassConfig
{ {
public override double ChancePerPlayer { get; set; } = 0.3; public override double ChancePerPlayer { get; set; } = 0.3;
public override int MaxSpawns { get; set; } = 5; public override int MaxSpawns { get; set; } = 5;
public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin]; public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin];
}
/// <summary>
/// Tracks the spawn state for a custom class.
/// </summary>
internal sealed record SpawnState
{
public int Spawns;
} }