do a lot ig

This commit is contained in:
2025-07-03 22:05:38 +02:00
parent 0aee847089
commit 0615f8aeea
36 changed files with 1867 additions and 125 deletions
+437 -49
View File
@@ -1,8 +1,11 @@
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;
@@ -13,6 +16,7 @@ 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;
@@ -20,16 +24,14 @@ using Vector3 = UnityEngine.Vector3;
namespace CustomClasses;
/// <summary>
/// Main plugin class for CustomClasses. Handles plugin lifecycle and event subscriptions.
/// </summary>
public sealed class CustomClasses : Plugin
{
private readonly CustomClassManager _classManager;
public CustomClasses()
{
_classManager = new CustomClassManager();
}
public readonly CustomClassManager ClassManager = new();
public SerpentsHandManager SerpentsHandManager;
/// <inheritdoc/>
public override string Name => "CustomClasses";
@@ -41,7 +43,9 @@ public sealed class CustomClasses : Plugin
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>
@@ -67,20 +71,79 @@ public sealed class CustomClasses : Plugin
/// 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";
/// <inheritdoc/>
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;
PlayerEvents.Escaped += OnEscaped;
}
private void OnEscaped(PlayerEscapedEventArgs ev)
{
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/>
@@ -90,30 +153,142 @@ public sealed class CustomClasses : Plugin
ServerEvents.RoundEnded -= OnRoundEnded;
Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup;
Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem;
PlayerEvents.Escaped -= OnEscaped;
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();
ClassManager.ResetSpawnStates();
}
[SuppressMessage("ReSharper", "RedundantJumpStatement")]
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;
if (_classManager.TryHandleSpawn(ev.Player, ShadowStepperConfig, typeof(ShadowStepperConfig))) return;
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)
{
if (ev.Pickup.Type < ItemType.KeycardCustomTaskForce) return;
// Process custom upgrade
if (ev.Pickup is not KeycardPickup keycard) return;
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();
@@ -121,9 +296,9 @@ public sealed class CustomClasses : Plugin
private static void OnScp914ProcessingInventoryItem(Scp914ProcessingInventoryItemEventArgs ev)
{
if (ev.Item.Type < ItemType.KeycardCustomTaskForce) return;
// 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);
}
@@ -145,11 +320,24 @@ public class CustomClassManager
public CustomClassManager()
{
RegisterHandler<JanitorConfig>(new JanitorHandler(this));
RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler(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>
@@ -159,7 +347,7 @@ public class CustomClassManager
/// <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), 0, (float)(_random.NextDouble() * 2));
player.Position = position + new Vector3(0, 1, 0) + new Vector3((float)(_random.NextDouble() * 2 - 1), 0, (float)(_random.NextDouble() * 2 - 1));
}
/// <summary>
@@ -167,11 +355,12 @@ public class CustomClassManager
/// </summary>
/// <typeparam name="T">The config type.</typeparam>
/// <param name="handler">The handler instance.</param>
private void RegisterHandler<T>(ICustomClassHandler handler) where T : CustomClassConfig
/// <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)] = new SpawnState();
_spawnStates[typeof(T)] = spawnState ?? new SpawnState();
_handlers[typeof(T)] = handler;
}
}
@@ -184,7 +373,7 @@ public class CustomClassManager
lock (_lock)
{
foreach (var key in _spawnStates.Keys.ToList())
_spawnStates[key] = new SpawnState();
_spawnStates[key].Reset();
}
}
@@ -194,8 +383,9 @@ public class CustomClassManager
/// <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)
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;
@@ -216,12 +406,18 @@ public class CustomClassManager
}
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;
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>
@@ -236,8 +432,6 @@ public interface ICustomClassHandler
/// <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 HandleEscape(Player player, CustomClassConfig config);
}
public abstract class CustomClassHandler: ICustomClassHandler
@@ -269,14 +463,14 @@ public class JanitorHandler(CustomClassManager manager) : CustomClassHandler
player.AddItem(spawnItem, ItemAddReason.StartingItem);
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>!", CustomClasses.BroadcastDuration);
}
}
/// <summary>
/// Handler for the Research Subject custom class.
/// </summary>
public class ResearchSubjectHandler(CustomClassManager manager) : CustomClassHandler
public class ResearchSubjectHandler : CustomClassHandler
{
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
@@ -286,13 +480,13 @@ public class ResearchSubjectHandler(CustomClassManager manager) : CustomClassHan
Logger.Error("No Scientist found for Research Subject spawn.");
return;
}
manager.TeleportPlayerToAround(player, scientist.Position);
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>!", 3);
player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", CustomClasses.BroadcastDuration);
}
}
@@ -314,7 +508,7 @@ public class HeadGuardHandler : CustomClassHandler
{
if (firearm.Base.Template.TryGetModule(out MagazineModule magazine))
{
magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
magazine.ServerSetInstanceAmmo(firearm.Serial, magazine.AmmoMax);
}
else
{
@@ -325,9 +519,9 @@ public class HeadGuardHandler : CustomClassHandler
{
Logger.Error("Failed to get firearm from pickup for Head Guard.");
}
if (pickup != null) player.AddItem(pickup);
player.AddItem(pickup!);
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>!", CustomClasses.BroadcastDuration);
}
}
@@ -343,7 +537,7 @@ public class MedicHandler : CustomClassHandler
player.AddItem(spawnItem, ItemAddReason.StartingItem);
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>!", CustomClasses.BroadcastDuration);
}
}
@@ -356,7 +550,7 @@ public class ShadowStepperHandler : CustomClassHandler
{
ApplyEffects(player);
player.SendBroadcast("You're a <color=#000000>ShadowStepper</color>!", 3);
player.SendBroadcast("You're a <color=#000000>ShadowStepper</color>!", CustomClasses.BroadcastDuration);
}
public override void HandleEscape(Player player, CustomClassConfig config)
@@ -369,7 +563,7 @@ public class ShadowStepperHandler : CustomClassHandler
private static void ApplyEffects(Player player)
{
player.ReferenceHub.playerEffectsController.ChangeState<SilentWalk>(100,float.MaxValue);
player.ReferenceHub.playerEffectsController.ChangeState<Slowness>(20,float.MaxValue);
player.ReferenceHub.playerEffectsController.ChangeState<Slowness>(10,float.MaxValue);
}
}
@@ -396,7 +590,166 @@ 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>!", 3);
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();
}
}
}
@@ -477,15 +830,50 @@ public sealed class GamblerConfig : CustomClassConfig
/// <summary>
/// Configuration for the Shadow Stepper class.
/// </summary>
public sealed class ShadowStepperConfig : CustomClassConfig
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>
internal sealed record SpawnState
public record SpawnState
{
public int Spawns;
public virtual void Reset()
{
Spawns = 0;
}
}
+12
View File
@@ -26,12 +26,24 @@
<Reference Include="Assembly-CSharp-firstpass">
<HintPath>..\dependencies\Assembly-CSharp-firstpass.dll</HintPath>
</Reference>
<Reference Include="CommandSystem.Core">
<HintPath>..\dependencies\CommandSystem.Core.dll</HintPath>
</Reference>
<Reference Include="HintServiceMeow">
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\dependencies\Mirror.dll</HintPath>
</Reference>
<Reference Include="NorthwoodLib">
<HintPath>..\dependencies\NorthwoodLib.dll</HintPath>
</Reference>
<Reference Include="Pooling">
<HintPath>..\dependencies\Pooling.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Immutable">
<HintPath>..\dependencies\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
</Reference>
+276
View File
@@ -0,0 +1,276 @@
using CommandSystem;
using CustomPlayerEffects;
using HintServiceMeow.Core.Enum;
using HintServiceMeow.Core.Models.Hints;
using HintServiceMeow.Core.Utilities;
using Interactables.Interobjects.DoorUtils;
using InventorySystem.Items.Firearms.Modules;
using LabApi.Events.Handlers;
using LabApi.Features.Permissions;
using LabApi.Features.Wrappers;
using MEC;
using PlayerRoles;
using UnityEngine;
using Logger = LabApi.Features.Console.Logger;
using Random = System.Random;
namespace CustomClasses;
public class SerpentsHandManager
{
public static bool IsSerpentsHand(Player player) => player.CustomInfo.Contains("SerpentsHand");
private readonly CustomClasses _customClasses;
public SerpentsHandManager(CustomClasses customClasses)
{
_customClasses = customClasses;
PlayerEvents.Escaping += ev =>
{
if (!IsSerpentsHand(ev.Player)) return;
var state = (SerpentsHandState) _customClasses.ClassManager.GetSpawnState(typeof(SerpentsHandConfig));
var hadItem = false;
foreach (var playerItem in ev.Player.Items)
{
switch (playerItem.Type)
{
case ItemType.SCP018 or ItemType.SCP207 or ItemType.SCP244a or ItemType.SCP244b
or ItemType.SCP268 or ItemType.SCP330 or ItemType.SCP500 or ItemType.SCP1344 or ItemType.SCP1576
or ItemType.SCP1853 or ItemType.SCP2176 or ItemType.AntiSCP207:
state.Points += 1;
hadItem = true;
break;
case ItemType.GunSCP127:
state.Points += 2;
hadItem = true;
break;
}
}
if (!hadItem) return;
ev.Player.SendBroadcast("You brought back the SCP items...", 5);
ev.Player.SetRole(RoleTypeId.Spectator);
Logger.Info($"New SH Points: {state.Points}");
};
ServerEvents.RoundEnding += ev =>
{
var factions =
Player.ReadyList.Select(x =>
{
return x.Team switch
{
Team.ChaosInsurgency or Team.ClassD => Faction.FoundationEnemy,
Team.FoundationForces or Team.Scientists => Faction.FoundationStaff,
Team.Flamingos => Faction.Flamingos,
Team.SCPs => Faction.SCP,
Team.OtherAlive when IsSerpentsHand(x) => (Faction)35,
_ => Faction.Unclassified
};
}).Where(x=>x!=Faction.Unclassified).GroupBy(x=>x).Select(x=>x.Key).ToArray();
if (factions.Length > 1)
{
ev.IsAllowed = false;
return;
}
var state = (SerpentsHandState) _customClasses.ClassManager.GetSpawnState(typeof(SerpentsHandConfig));
if (state.Points >= 10)
{
ev.LeadingTeam = RoundSummary.LeadingTeam.Flamingos;
}
};
}
public static void PreSpawn(Player player)
{
player.SetRole(RoleTypeId.Tutorial, RoleChangeReason.RespawnMiniwave, RoleSpawnFlags.None);
const string customInfo = "<color=#32CD32>SerpentsHand</color>";
if (!Player.ValidateCustomInfo(customInfo, out var reason))
{
Logger.Error($"Invalid custom info for Serpents Hand: {reason}");
}
else
{
player.CustomInfo = customInfo;
player.InfoArea |= PlayerInfoArea.CustomInfo;
}
}
public IEnumerator<float> UpdateSerpentsHandHint()
{
while (true)
{
yield return Timing.WaitForSeconds(1);
if (_customClasses.ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is not SerpentsHandState state) continue;
foreach (var player in Player.ReadyList)
{
if (!_customClasses.Hints.ContainsKey(player))
{
_customClasses.Hints[player] = new Hint
{
Text = "", Alignment = HintAlignment.Center, YCoordinate = 900, Hide = true
};
var playerDisplay = PlayerDisplay.Get(player);
playerDisplay.AddHint(_customClasses.Hints[player]);
}
_customClasses.Hints[player].Text =
$"Serpents Hand Chance: {state.ExtraChance + SerpentsHandConfig.BaseChance:0.00}%";
_customClasses.Hints[player].Hide = state.HasSpawned || player.Role != RoleTypeId.Spectator;
}
}
// ReSharper disable once IteratorNeverReturns
}
}
public sealed record SerpentsHandState: SpawnState
{
public bool HasSpawned => _hasSpawned || PanicDisable;
public float ExtraChance;
public int Points;
public bool WillSpawn => _willSpawn && !PanicDisable;
private bool _hasSpawned;
private bool _willSpawn;
public bool PanicDisable;
public override void Reset()
{
base.Reset();
_hasSpawned = false;
ExtraChance = 0f;
Points = 0;
_willSpawn = false;
}
public void SetSpawned()
{
_hasSpawned = true;
}
public void SetWillSpawn()
{
_willSpawn = true;
}
public void SetWillNotSpawn()
{
_willSpawn = false;
}
}
public sealed class SerpentsHandConfig : CustomClassConfig
{
public override double ChancePerPlayer { get; set; } = 1.0;
public override int MaxSpawns { get; set; } = int.MaxValue;
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Tutorial;
public override ItemType[] Items { get; set; } = [ItemType.Painkillers, ItemType.Medkit, ItemType.ArmorCombat];
public const float BaseChance = 20f;
}
public class SerpentsHandHandler : SimpleAddItemHandler
{
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
base.HandleSpawn(player, config, random);
// player.Position = new Vector3(123.921f + (float)(random.NextDouble() * 2 - 1), 288.792f, 20.929f + (float)(random.NextDouble() * 2 - 1));
player.Position = new Vector3(0.22f + (float)(random.NextDouble() * 2 - 1), 300.96f, -0.31f + (float)(random.NextDouble() * 2 - 1));
ItemType[] guns = [ItemType.GunAK, ItemType.GunE11SR, ItemType.GunCrossvec];
var gun = guns[random.Next(0, guns.Length-1)];
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);
player.SetAmmo(magazine.AmmoType, 120);
}
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!);
KeycardItem.CreateCustomKeycardTaskForce(
player,
"Serpent's Hand Keycard",
$"SH. {player.Nickname}",
new KeycardLevels(3, 3, 2),
Color.black,
new Color(0.271f, 0.271f, 0.271f),
"SH",
3
);
player.EnableEffect<MovementBoost>(20, 30);
player.SendBroadcast("You're a <color=#2E8B57>Serpent's Hand</color> member!", CustomClasses.BroadcastDuration);
}
}
[CommandHandler(typeof(RemoteAdminCommandHandler))]
public class PanicDisableRemoteAdminCommand : ICommand
{
public string Command => "panicdisableserpentshand";
public string[] Aliases => [];
public string Description => "Panic disable Serpents Hand.";
public bool Execute(ArraySegment<string> arguments, ICommandSender sender, out string response)
{
var state = (SerpentsHandState)CustomClasses.Instance.ClassManager.GetSpawnState(typeof(SerpentsHandConfig));
state.PanicDisable = true;
response = "Serpents Hand has been disabled.";
return true;
}
}
[CommandHandler(typeof(ClientCommandHandler))]
public class PanicDisableCommand : ICommand
{
public string Command => "panicdisableserpentshand";
public string[] Aliases => [];
public string Description => "Panic disable Serpents Hand.";
public bool Execute(ArraySegment<string> arguments, ICommandSender sender, out string response)
{
if (!Player.TryGet(sender, out var player))
{
response = "You must be a player to use this command!";
return false;
}
if (!player.HasPermissions("panicdisable.serpentshand") && player.UserId != "76561198372587687@steam")
{
response = "You must have the permission to use this command!";
return false;
}
var state = (SerpentsHandState)CustomClasses.Instance.ClassManager.GetSpawnState(typeof(SerpentsHandConfig));
state.PanicDisable = true;
response = "Serpents Hand has been disabled.";
return true;
}
}
+66
View File
@@ -0,0 +1,66 @@
using CommandSystem;
using LabApi.Features.Wrappers;
namespace CustomClasses;
[CommandHandler(typeof(RemoteAdminCommandHandler))]
public class SetCClassCommand : ICommand
{
public string Command => "setcclass";
public string[] Aliases => ["scc"];
public string Description => "Forces a player to become a specific custom class";
public bool Execute(ArraySegment<string> arguments, ICommandSender sender, out string response)
{
var args = arguments.Array!;
if (arguments.Count < 2)
{
response = "Usage: setcclass <playerName> <className>";
return false;
}
// Find the last argument as the class name
var className = args[arguments.Offset + arguments.Count - 1].ToLower();
// Join all arguments except the last one to get the full player name
var playerName = string.Join(" ", args.Skip(arguments.Offset).Take(arguments.Count - 1));
var player = Player.ReadyList.FirstOrDefault(x => x.Nickname == playerName || x.UserId == playerName);
if (player == null)
{
response = $"Player {playerName} not found";
return false;
}
var customClasses = CustomClasses.Instance;
var manager = new CustomClassManager();
var success = className switch
{
"janitor" => manager.ForceSpawn(player, customClasses.JanitorConfig, typeof(JanitorConfig), null),
"subject" or "researchsubject" => manager.ForceSpawn(player, customClasses.ResearchSubjectConfig, typeof(ResearchSubjectConfig), null),
"headguard" => manager.ForceSpawn(player, customClasses.HeadGuardConfig, typeof(HeadGuardConfig), null),
"medic" => manager.ForceSpawn(player, customClasses.MedicConfig, typeof(MedicConfig), null),
"gambler" => manager.ForceSpawn(player, customClasses.GamblerConfig, typeof(GamblerConfig), null),
"shadowstepper" => manager.ForceSpawn(player, customClasses.ShadowStepperConfig, typeof(ShadowStepperConfig), null),
"demolitionist" => manager.ForceSpawn(player, customClasses.MtfDemolitionistConfig, typeof(MtfDemolitionistConfig), null),
"scout" => manager.ForceSpawn(player, customClasses.ScoutConfig, typeof(ScoutConfig), null),
"explosivemaster" => manager.ForceSpawn(player, customClasses.ExplosiveMasterConfig, typeof(ExplosiveMasterConfig), null),
"flashmaster" => manager.ForceSpawn(player, customClasses.FlashMasterConfig, typeof(FlashMasterConfig), null),
"serpentshand" => manager.ForceSpawn(player, customClasses.SerpentsHandConfig, typeof(SerpentsHandConfig),
() =>
{
SerpentsHandManager.PreSpawn(player);
}),
_ => false
};
if (!success)
{
response = $"Failed to set {playerName} to {className}. Make sure the player has the correct base role for the custom class.";
return false;
}
response = $"Successfully set {playerName} to {className}";
return true;
}
}