diff --git a/AfkSwap/AfkSwap.cs b/AfkSwap/AfkSwap.cs
index 6653c82..062ea30 100644
--- a/AfkSwap/AfkSwap.cs
+++ b/AfkSwap/AfkSwap.cs
@@ -98,7 +98,7 @@ public class AfkSwap : Plugin
continue;
}
- if (!_playerPositions[playerTime.Key].Equals(playerTime.Key.Position))
+ if ((_playerPositions[playerTime.Key] - playerTime.Key.Position).sqrMagnitude > 2)
{
_playerSpawnTimes.Remove(playerTime.Key);
_playerPositions.Remove(playerTime.Key);
diff --git a/CustomClasses/CustomClasses.cs b/CustomClasses/CustomClasses.cs
index 82958a1..f05d67c 100644
--- a/CustomClasses/CustomClasses.cs
+++ b/CustomClasses/CustomClasses.cs
@@ -11,11 +11,13 @@ using LabApi.Events.Arguments.Scp914Events;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
+using LabApi.Features.Enums;
using LabApi.Features.Wrappers;
using LabApi.Loader.Features.Plugins;
using MapGeneration;
using MEC;
using PlayerRoles;
+using PlayerRoles.PlayableScps.Scp106;
using Scp914.Processors;
using UnityEngine;
using Logger = LabApi.Features.Console.Logger;
@@ -32,6 +34,7 @@ public sealed class CustomClasses : Plugin
{
public readonly CustomClassManager ClassManager = new();
public SerpentsHandManager SerpentsHandManager;
+ public NegromancerManager NegromancerManager;
///
public override string Name => "CustomClasses";
@@ -77,6 +80,8 @@ public sealed class CustomClasses : Plugin
public ExplosiveMasterConfig ExplosiveMasterConfig { get; private set; } = new();
public FlashMasterConfig FlashMasterConfig { get; private set; } = new();
public SerpentsHandConfig SerpentsHandConfig { get; private set; } = new();
+ public NegromancerConfig NegromancerConfig { get; private set; } = new();
+ public NegromancerShadowConfig NegromancerShadowConfig { get; private set; } = new();
internal readonly Dictionary Hints = new();
@@ -142,6 +147,7 @@ public sealed class CustomClasses : Plugin
}
}
+ NegromancerManager = new NegromancerManager(this);
Instance = this;
}
@@ -330,6 +336,8 @@ public class CustomClassManager
RegisterHandler(new ExplosiveMasterHandler());
RegisterHandler(new FlashMasterHandler());
RegisterHandler(new SerpentsHandHandler(), new SerpentsHandState());
+ RegisterHandler(new NegromancerHandler());
+ RegisterHandler(new NegromancerShadowHandler());
}
public SpawnState GetSpawnState(Type configType)
@@ -420,6 +428,29 @@ public class CustomClassManager
}
}
+public class NegromancerShadowHandler : CustomClassHandler
+{
+ public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
+ {
+ player.MaxHealth = 1000;
+ player.MaxHumeShield = 0;
+ player.HumeShield = 0;
+
+ player.EnableEffect(10, float.PositiveInfinity);
+
+ const string customInfo = "Shadow";
+ if (!Player.ValidateCustomInfo(customInfo, out var reason))
+ {
+ Logger.Error($"Invalid custom info for Shadow: {reason}");
+ }
+ else
+ {
+ player.CustomInfo = customInfo;
+ player.InfoArea |= PlayerInfoArea.CustomInfo;
+ }
+ }
+}
+
///
/// Interface for custom class spawn handlers.
///
@@ -444,6 +475,15 @@ public abstract class CustomClassHandler: ICustomClassHandler
}
}
+public enum JanitorSpawn
+{
+ Lcz173,
+ Lcz914,
+ LczGr18,
+ Lcz330
+}
+
+
///
/// Handler for the Janitor custom class.
///
@@ -451,13 +491,51 @@ 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)
+ var spawnLocation = (JanitorSpawn)random.Next(0, 4);
+
+ switch (spawnLocation)
{
- Logger.Error("LCZ 914 room not found for Janitor spawn.");
- return;
+ case JanitorSpawn.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);
+ break;
+
+ case JanitorSpawn.Lcz173:
+ var lcz173Door = Map.Doors.FirstOrDefault(x => x.DoorName == DoorName.Lcz173Connector);
+ if (lcz173Door == null)
+ {
+ Logger.Error("LCZ 173 connector door not found for Janitor spawn.");
+ return;
+ }
+ manager.TeleportPlayerToAround(player, lcz173Door.Position);
+ break;
+ case JanitorSpawn.LczGr18:
+ var lczGr18Door = Map.Doors.FirstOrDefault(x => x.DoorName == DoorName.LczGr18Inner);
+ if (lczGr18Door == null)
+ {
+ Logger.Error("LCZ Gr18 door not found for Janitor spawn.");
+ return;
+ }
+ manager.TeleportPlayerToAround(player, lczGr18Door.Position);
+ break;
+ case JanitorSpawn.Lcz330:
+ var lcz330Door = Map.Doors.FirstOrDefault(x => x.DoorName == DoorName.Lcz330);
+ if (lcz330Door == null)
+ {
+ Logger.Error("LCZ 330 door not found for Janitor spawn.");
+ return;
+ }
+ manager.TeleportPlayerToAround(player, lcz330Door.Position);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
- manager.TeleportPlayerToAround(player, scp914.Position);
+
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
@@ -865,6 +943,22 @@ public sealed class FlashMasterConfig : CustomClassConfig
public override ItemType[] Items { get; set; } = [ItemType.GrenadeFlash];
}
+public sealed class NegromancerConfig : CustomClassConfig
+{
+ public override double ChancePerPlayer { get; set; } = 0.0;
+ public override int MaxSpawns { get; set; } = 1;
+ public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scp049;
+ public override ItemType[] Items { get; set; } = [];
+}
+
+public sealed class NegromancerShadowConfig : CustomClassConfig
+{
+ public override double ChancePerPlayer { get; set; } = 0.0;
+ public override int MaxSpawns { get; set; } = int.MaxValue;
+ public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scp106;
+ public override ItemType[] Items { get; set; } = [];
+}
+
///
/// Tracks the spawn state for a custom class.
///
diff --git a/CustomClasses/NegromancerHandler.cs b/CustomClasses/NegromancerHandler.cs
new file mode 100644
index 0000000..f10d23c
--- /dev/null
+++ b/CustomClasses/NegromancerHandler.cs
@@ -0,0 +1,164 @@
+using System.Net;
+using CustomPlayerEffects;
+using LabApi.Events.Arguments.Scp049Events;
+using LabApi.Events.Handlers;
+using LabApi.Features.Console;
+using LabApi.Features.Wrappers;
+using MEC;
+using PlayerRoles;
+using PlayerRoles.PlayableScps.Scp049;
+using PlayerRoles.PlayableScps.Scp106;
+
+namespace CustomClasses;
+
+public class NegromancerHandler : CustomClassHandler
+{
+ public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
+ {
+ player.SendBroadcast("You are the Negromancer! Revived players become your Shadow.", CustomClasses.BroadcastDuration);
+ const string customInfo = "Shadowmancer";
+ if (!Player.ValidateCustomInfo(customInfo, out var reason))
+ {
+ Logger.Error($"Invalid custom info for Negromancer: {reason}");
+ }
+ else
+ {
+ player.CustomInfo = customInfo;
+ player.InfoArea |= PlayerInfoArea.CustomInfo;
+ }
+ }
+}
+
+public class NegromancerManager
+{
+ private readonly CustomClasses _plugin;
+
+ public static bool IsNegromancer(Player player) => player.CustomInfo.Contains("Shadowmancer");
+ public static bool IsShadow(Player player) => player.CustomInfo.Contains("Shadow") && !IsNegromancer(player);
+
+
+ public NegromancerManager(CustomClasses plugin)
+ {
+ _plugin = plugin;
+ Scp049Events.ResurrectedBody += OnScp049ResurrectedBody;
+
+ Timing.RunCoroutine(HealNearbyShadows());
+
+ Scp106Events.TeleportingPlayer += ev =>
+ {
+ if (!IsShadow(ev.Player)) return;
+ ev.IsAllowed = false;
+ ev.Target.EnableEffect(1, float.PositiveInfinity);
+ ev.Target.Damage(40f, ev.Player);
+ ev.Player.SendHitMarker();
+ };
+
+ Scp106Events.UsingHunterAtlas += ev =>
+ {
+ if (!IsShadow(ev.Player)) return;
+ ev.IsAllowed = false;
+
+ var position = ev.DestinationPosition;
+ var room = Room.GetRoomAtPosition(position);
+
+ if (room?.LightController == null)
+ {
+ return;
+ }
+
+ var stat = ev.Player.GetStatModule();
+
+ stat.CurValue = 0;
+
+ room.LightController.FlickerLights(3);
+ };
+
+ Scp106Events.ChangingSubmersionStatus += ev =>
+ {
+ if (!IsShadow(ev.Player)) return;
+ ev.IsAllowed = false;
+
+ var stat = ev.Player.GetStatModule();
+
+ ev.Player.DisableEffect();
+
+ var scaled = stat.CurValue * 40f;
+ var scaledByte = (byte)scaled;
+
+ if(scaledByte < 15) scaledByte = 15;
+
+ Logger.Debug($"Scaled {stat.CurValue} to {scaledByte}");
+
+
+ ev.Player.EnableEffect(scaledByte, 20);
+ stat.CurValue = 0;
+ Timing.CallDelayed(10, () =>
+ {
+ ev.Player.DisableEffect();
+ ev.Player.EnableEffect(10, float.PositiveInfinity);
+ });
+ };
+
+ Scp106Events.ChangingVigor += ev =>
+ {
+ if (!IsShadow(ev.Player)) return;
+ var delta = ev.Value - ev.OldValue;
+ delta *= 0.5f;
+ ev.Value = ev.OldValue + delta;
+ };
+
+ }
+
+ private static IEnumerator HealNearbyShadows()
+ {
+ while (true)
+ {
+ yield return Timing.WaitForSeconds(1);
+ try
+ {
+ Player.ReadyList.Where(IsNegromancer).Where(x =>
+ {
+ var scp = x.RoleBase as Scp049Role;
+ if (scp == null)
+ {
+ Logger.Error("Negromancer has no Scp049Role");
+ return false;
+ }
+
+ scp.SubroutineModule.TryGetSubroutine(out Scp049CallAbility ability);
+
+ if (ability) return ability.IsMarkerShown;
+
+ Logger.Error("Negromancer has no Scp049CallAbility");
+ return false;
+
+ }).ToList().ForEach(player =>
+ {
+ Player.ReadyList.Where(IsShadow).Where(x =>
+ {
+ var distance = (x.Position - player.Position).SqrMagnitudeIgnoreY();
+ return distance < 64;
+ }
+ ).ToList().ForEach(x => x.Heal(10));
+ });
+ }
+ catch (Exception e)
+ {
+ // ignored
+ }
+ }
+ // ReSharper disable once IteratorNeverReturns
+ }
+
+ private void OnScp049ResurrectedBody(Scp049ResurrectedBodyEventArgs ev)
+ {
+ var classManager = _plugin.ClassManager;
+ // Check if the reviver is a Negromancer
+ if (classManager == null ||
+ !IsNegromancer(ev.Player)) return;
+
+ ev.Target.SetRole(RoleTypeId.Scp106, RoleChangeReason.Respawn, RoleSpawnFlags.None);
+ classManager.ForceSpawn(ev.Target, _plugin.NegromancerShadowConfig, typeof(NegromancerShadowConfig), null);
+
+ }
+}
\ No newline at end of file
diff --git a/CustomClasses/SerpentsHandManager.cs b/CustomClasses/SerpentsHandManager.cs
index db8d841..be3151a 100644
--- a/CustomClasses/SerpentsHandManager.cs
+++ b/CustomClasses/SerpentsHandManager.cs
@@ -47,6 +47,12 @@ public class SerpentsHandManager
}
}
+ if (Warhead.IsDetonationInProgress || Warhead.IsDetonated)
+ {
+ hadItem = true;
+ state.Points += 1;
+ }
+
if (!hadItem) return;
ev.Player.SendBroadcast("You brought back the SCP items...", 5);
@@ -107,6 +113,8 @@ public class SerpentsHandManager
while (true)
{
yield return Timing.WaitForSeconds(1);
+
+ RoundSummary.singleton.ExtraTargets = Player.ReadyList.Count(IsSerpentsHand);
if (_customClasses.ClassManager.GetSpawnState(typeof(SerpentsHandConfig)) is not SerpentsHandState state) continue;
@@ -134,10 +142,10 @@ public class SerpentsHandManager
public sealed record SerpentsHandState: SpawnState
{
- public bool HasSpawned => _hasSpawned || PanicDisable;
+ public bool HasSpawned => _hasSpawned || PanicDisable || Warhead.IsDetonated;
public float ExtraChance;
public int Points;
- public bool WillSpawn => _willSpawn && !PanicDisable;
+ public bool WillSpawn => _willSpawn && !PanicDisable && !Warhead.IsDetonated;
private bool _hasSpawned;
private bool _willSpawn;
diff --git a/CustomClasses/SetCClassCommand.cs b/CustomClasses/SetCClassCommand.cs
index d1e39de..f071e37 100644
--- a/CustomClasses/SetCClassCommand.cs
+++ b/CustomClasses/SetCClassCommand.cs
@@ -1,9 +1,11 @@
using CommandSystem;
+using LabApi.Features.Permissions;
using LabApi.Features.Wrappers;
namespace CustomClasses;
[CommandHandler(typeof(RemoteAdminCommandHandler))]
+[CommandHandler(typeof(ClientCommandHandler))]
public class SetCClassCommand : ICommand
{
public string Command => "setcclass";
@@ -11,6 +13,19 @@ public class SetCClassCommand : ICommand
public string Description => "Forces a player to become a specific custom class";
public bool Execute(ArraySegment arguments, ICommandSender sender, out string response)
{
+ var executor = Player.Get(sender);
+ if (executor == null)
+ {
+ response = "You must be a player to use this command!";
+ return false;
+ }
+
+ if (!executor.HasPermissions("customclasses.setcustomclass"))
+ {
+ response = "You do not have permission to use this command!";
+ return false;
+ }
+
var args = arguments.Array!;
if (arguments.Count < 2)
{
@@ -51,6 +66,7 @@ public class SetCClassCommand : ICommand
{
SerpentsHandManager.PreSpawn(player);
}),
+ "negromancer" => manager.ForceSpawn(player, customClasses.NegromancerConfig, typeof(NegromancerConfig), null),
_ => false
};
diff --git a/ScpSwap/ScpSwap.cs b/ScpSwap/ScpSwap.cs
index 5839ecc..516c7c6 100644
--- a/ScpSwap/ScpSwap.cs
+++ b/ScpSwap/ScpSwap.cs
@@ -46,6 +46,11 @@ public class ScpSwap : Plugin
return;
}
+ if (ev.Role.RoleTypeId == RoleTypeId.Scp0492)
+ {
+ return;
+ }
+
ev.Player.SendBroadcast("Willst du dein SCP wechseln? Drücke Ö und gebe .scpswap ein.", 10);
};
}
diff --git a/ScpSwap/SwapCommand.cs b/ScpSwap/SwapCommand.cs
index 9b0b47a..a075b46 100644
--- a/ScpSwap/SwapCommand.cs
+++ b/ScpSwap/SwapCommand.cs
@@ -33,6 +33,12 @@ public class SwapCommand : ICommand
return false;
}
+ if (player.Role == RoleTypeId.Scp0492)
+ {
+ response = "You can't swap SCPs while you're a zombie!";
+ return false;
+ }
+
List validScp =
[
"049",
diff --git a/VIPTreatment/BullshitDetectedCommand.cs b/VIPTreatment/BullshitDetectedCommand.cs
new file mode 100644
index 0000000..162596a
--- /dev/null
+++ b/VIPTreatment/BullshitDetectedCommand.cs
@@ -0,0 +1,42 @@
+using CommandSystem;
+using LabApi.Features.Permissions;
+using LabApi.Features.Wrappers;
+
+namespace VIPTreatment;
+
+[CommandHandler(typeof(RemoteAdminCommandHandler))]
+[CommandHandler(typeof(ClientCommandHandler))]
+public class BullshitDetectedCommand : ICommand
+{
+ public string Command => "bullshitdetected";
+
+ public string[] Aliases => [];
+
+ public string Description => "Broadcasts a message to all players";
+
+ public bool Execute(ArraySegment 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("viptreatment.wiki"))
+ {
+ response = "You must have the permission to use this command!";
+ return false;
+ }
+
+ foreach (var target in Player.ReadyList)
+ {
+ target.SendBroadcast("⚠️ BULLSHIT DETECTED ⚠️", 5);
+ target.SendBroadcast("Tipp: Das Wiki hat immer die aktuellsten Informationen!", 5);
+ }
+
+ Cassie.Message("false information yield_0.8 detected yield_01 see V yield_0.2 KEY", true, false, false);
+
+ response = "Broadcast message sent to all ready players.";
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/VisibleSpectators/PlayerDisplayUtil.cs b/VisibleSpectators/PlayerDisplayUtil.cs
index 1621a95..e068de4 100644
--- a/VisibleSpectators/PlayerDisplayUtil.cs
+++ b/VisibleSpectators/PlayerDisplayUtil.cs
@@ -1,5 +1,4 @@
using PlayerRoles;
-using LabApi.Features;
using LabApi.Features.Wrappers;
namespace VisibleSpectators;
@@ -42,6 +41,12 @@ public static class PlayerDisplayUtil
{ "TEAL", "008080" },
{ "GOLD", "EFC01A" }
};
+
+ private static readonly Dictionary PlayerSpecificDisplays = new()
+ {
+ { "76561198372750067@steam", ("DEAFCC", "1+1=10") },
+ { "76561198372587687@steam", ("9933FF", "HoherGeist") }
+ };
///
/// Returns a formatted display string for a player, with color.
@@ -49,6 +54,12 @@ public static class PlayerDisplayUtil
public static string PlayerToDisplay(Player player)
{
if (player is not { IsReady: true }) return string.Empty;
+
+ if (PlayerSpecificDisplays.TryGetValue(player.UserId, out var specificDisplay))
+ {
+ return $"{specificDisplay.Name}";
+ }
+
const string defaultColor = "FFFFFF";
try
{
diff --git a/build.sh b/build.sh
index 80140b2..b92f334 100755
--- a/build.sh
+++ b/build.sh
@@ -12,7 +12,7 @@ excluded_projects=("TemplateProject" "RangeBan" "LobbyGame" "RangeBan.Tests" "St
# Find project directories (containing .csproj files)
echo "Copying DLLs to output folder..."
-for proj in $(find . -name "*.csproj"); do
+find . -path "./fuchsbau" -prune -o -path "./testbau" -prune -o -name "*.csproj" -print0 | while IFS= read -r -d '' proj; do
# Extract project name from .csproj file
proj_name=$(basename "$proj" .csproj)