Compare commits
3 Commits
c9bee028d9
...
326b99c464
Author | SHA1 | Date | |
---|---|---|---|
326b99c464 | |||
67e3d6ceaa | |||
73a4da1edd |
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.0.2",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using Interactables.Interobjects.DoorUtils;
|
||||
using InventorySystem.Items;
|
||||
using InventorySystem.Items.Firearms.Modules;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Events.Arguments.Scp914Events;
|
||||
using LabApi.Events.Arguments.ServerEvents;
|
||||
using LabApi.Events.Handlers;
|
||||
using LabApi.Features;
|
||||
@ -16,29 +18,65 @@ using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
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";
|
||||
/// <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 JanitorConfig JanitorConfig { get; set; } = new();
|
||||
public ResearchSubjectConfig ResearchSubjectConfig { get; set; } = new();
|
||||
public HeadGuardConfig HeadGuardConfig { get; set; } = new();
|
||||
/// <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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Enable()
|
||||
{
|
||||
PlayerEvents.Spawned += OnPlayerSpawned;
|
||||
ServerEvents.RoundEnded += OnRoundEnded;
|
||||
Scp914Events.ProcessingPickup += OnScp914ProcessingPickup;
|
||||
Scp914Events.ProcessingInventoryItem += OnScp914ProcessingInventoryItem;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Disable()
|
||||
{
|
||||
PlayerEvents.Spawned -= OnPlayerSpawned;
|
||||
ServerEvents.RoundEnded -= OnRoundEnded;
|
||||
Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup;
|
||||
Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem;
|
||||
}
|
||||
|
||||
private void OnRoundEnded(RoundEndedEventArgs ev)
|
||||
@ -51,9 +89,33 @@ public class CustomClasses : Plugin
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
private readonly Dictionary<Type, ICustomClassHandler> _handlers = new();
|
||||
@ -61,38 +123,61 @@ public class CustomClassManager
|
||||
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()
|
||||
{
|
||||
// Register handlers
|
||||
RegisterHandler<JanitorConfig>(new JanitorHandler(this));
|
||||
RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler(this));
|
||||
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler(this));
|
||||
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler());
|
||||
RegisterHandler<MedicConfig>(new MedicHandler());
|
||||
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)
|
||||
{
|
||||
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), 0, (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
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (player.Role != config.RequiredRole) return false;
|
||||
@ -100,127 +185,202 @@ public class CustomClassManager
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var state = _spawnStates[configType];
|
||||
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}");
|
||||
|
||||
if (_handlers.TryGetValue(configType, out var handler)) return handler.HandleSpawn(player, config, _random);
|
||||
if (!_handlers.TryGetValue(configType, out var handler))
|
||||
return false;
|
||||
Timing.CallDelayed(0.5f, () => { handler.HandleSpawn(player, config, _random); });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for custom class spawn handlers.
|
||||
/// </summary>
|
||||
public interface ICustomClassHandler
|
||||
{
|
||||
bool HandleSpawn(Player player, CustomClassConfig config, Random random);
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for the Janitor custom class.
|
||||
/// </summary>
|
||||
public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler
|
||||
{
|
||||
public bool 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);
|
||||
Timing.CallDelayed(0.5f, () =>
|
||||
var scp914 = Map.Rooms.FirstOrDefault(r => r.Name == RoomName.Lcz914);
|
||||
if (scp914 == null)
|
||||
{
|
||||
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>!", 3);
|
||||
});
|
||||
|
||||
return true;
|
||||
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>!", 3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for the Research Subject custom class.
|
||||
/// </summary>
|
||||
public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler
|
||||
{
|
||||
public bool 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);
|
||||
Timing.CallDelayed(0.5f, () =>
|
||||
var scientist = Player.ReadyList.FirstOrDefault(p => p.Role == RoleTypeId.Scientist);
|
||||
if (scientist == null)
|
||||
{
|
||||
manager.TeleportPlayerToAround(player, scientist.Position);
|
||||
Logger.Error("No Scientist found for Research Subject spawn.");
|
||||
return;
|
||||
}
|
||||
manager.TeleportPlayerToAround(player, 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);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var spawnItem in config.Items)
|
||||
/// <summary>
|
||||
/// Handler for the Head Guard custom class.
|
||||
/// </summary>
|
||||
public class HeadGuardHandler : ICustomClassHandler
|
||||
{
|
||||
public 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))
|
||||
{
|
||||
player.AddItem(spawnItem, ItemAddReason.StartingItem);
|
||||
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
|
||||
magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
|
||||
}
|
||||
|
||||
player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", 3);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class HeadGuardHandler(CustomClassManager manager) : ICustomClassHandler
|
||||
{
|
||||
public bool HandleSpawn(Player player, CustomClassConfig config, Random random)
|
||||
{
|
||||
Timing.CallDelayed(0.5f, () =>
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to get magazine module for Head Guard firearm.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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 != null) player.AddItem(pickup);
|
||||
|
||||
player.SetAmmo(ItemType.Ammo9x19, 120);
|
||||
|
||||
player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3);
|
||||
});
|
||||
|
||||
return true;
|
||||
Logger.Error("Failed to get firearm from pickup for Head Guard.");
|
||||
}
|
||||
if (pickup != null) player.AddItem(pickup);
|
||||
player.SetAmmo(ItemType.Ammo9x19, 120);
|
||||
player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3);
|
||||
}
|
||||
}
|
||||
|
||||
internal record SpawnState
|
||||
/// <summary>
|
||||
/// Handler for the Medic custom class.
|
||||
/// </summary>
|
||||
public class MedicHandler : ICustomClassHandler
|
||||
{
|
||||
public int Spawns;
|
||||
public 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>!", 3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base handler for simple item-giving custom classes.
|
||||
/// </summary>
|
||||
public abstract class SimpleAddItemHandler : ICustomClassHandler
|
||||
{
|
||||
public virtual 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>!", 3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
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 double ChancePerPlayer { get; set; } = 0.3;
|
||||
@ -228,8 +388,41 @@ public class JanitorConfig : CustomClassConfig
|
||||
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 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>
|
||||
/// Tracks the spawn state for a custom class.
|
||||
/// </summary>
|
||||
internal sealed record SpawnState
|
||||
{
|
||||
public int Spawns;
|
||||
}
|
@ -23,6 +23,7 @@ public class GamblingCoinChancesConfig
|
||||
public int AdvancedPositiveEffectChance { get; set; } = 150;
|
||||
public int AdvancedNegativeEffectChance { get; set; } = 250;
|
||||
public int RemoveCoinChance { get; set; } = 300;
|
||||
public int SpawnZombieChance { get; set; } = 100;
|
||||
}
|
||||
|
||||
public class GamblingCoinMessages
|
||||
@ -44,6 +45,7 @@ public class GamblingCoinMessages
|
||||
public string NegativeEffectMessage { get; set; } = "You feel worse";
|
||||
public string AdvancedNegativeEffectMessage { get; set; } = "You feel like you could die any second";
|
||||
public string SwitchInventoryMessage { get; set; } = "Whoops... looks like something happened to your items!";
|
||||
public string SpawnZombieMessage { get; set; } = "You spawned as a Zombie!";
|
||||
}
|
||||
|
||||
public class GamblingCoinGameplayConfig
|
||||
|
@ -187,7 +187,23 @@ public class GamblingCoinEventHandler
|
||||
x.Player.ClearInventory();
|
||||
foreach (var randomPlayerItem in randomPlayerItems) x.Player.AddItem(randomPlayerItem.Type);
|
||||
}, configChances.SwitchInventoryChance)
|
||||
.AddAction(x => { x.Player.CurrentItem?.DropItem().Destroy(); }, configChances.RemoveCoinChance);
|
||||
.AddAction(x => { x.Player.CurrentItem?.DropItem().Destroy(); }, configChances.RemoveCoinChance)
|
||||
.AddAction(x =>
|
||||
{
|
||||
var spectators = Player.List.Where(player => player.Role == RoleTypeId.Spectator).ToArray();
|
||||
var spectator = spectators[Random.Range(0, spectators.Length)];
|
||||
|
||||
spectator.SendBroadcast(configMessages.SpawnZombieMessage, configGameplay.BroadcastDuration);
|
||||
spectator.SetRole(RoleTypeId.Scp0492);
|
||||
|
||||
var spawnRoom = Map.Rooms.First(room => room.Name == RoomName.HczWarhead);
|
||||
if (Warhead.IsDetonated)
|
||||
{
|
||||
spawnRoom = Map.Rooms.First(room => room.Name == RoomName.Outside);
|
||||
}
|
||||
|
||||
spectator.Position = spawnRoom.Position + new Vector3(0, 1, 0);
|
||||
}, configChances.SpawnZombieChance);
|
||||
|
||||
return;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using CustomPlayerEffects;
|
||||
using HintServiceMeow.Core.Models.Hints;
|
||||
using InventorySystem.Items.Usables.Scp330;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Events.Arguments.Scp0492Events;
|
||||
@ -12,18 +13,22 @@ namespace GrowingZombies;
|
||||
|
||||
public class GrowingZombies : Plugin
|
||||
{
|
||||
private readonly Dictionary<Player, int> _zombieCorpseCount = new();
|
||||
public readonly Dictionary<Player, int> ZombieCorpseCount = new();
|
||||
public static GrowingZombies Instance { get; set; }
|
||||
|
||||
public override string Name => "GrowingZombies";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "Makes zombies grow stronger as they eat more";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
Scp0492Events.ConsumedCorpse += OnZombieEat;
|
||||
ServerEvents.RoundEnded += OnRoundEnd;
|
||||
PlayerEvents.Left += OnPlayerLeave;
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
@ -31,17 +36,18 @@ public class GrowingZombies : Plugin
|
||||
Scp0492Events.ConsumedCorpse -= OnZombieEat;
|
||||
ServerEvents.RoundEnded -= OnRoundEnd;
|
||||
PlayerEvents.Left -= OnPlayerLeave;
|
||||
_zombieCorpseCount.Clear();
|
||||
ZombieCorpseCount.Clear();
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
private void OnRoundEnd(RoundEndedEventArgs ev)
|
||||
{
|
||||
_zombieCorpseCount.Clear();
|
||||
ZombieCorpseCount.Clear();
|
||||
}
|
||||
|
||||
private void OnPlayerLeave(PlayerLeftEventArgs ev)
|
||||
{
|
||||
_zombieCorpseCount.Remove(ev.Player);
|
||||
ZombieCorpseCount.Remove(ev.Player);
|
||||
}
|
||||
|
||||
private void OnZombieEat(Scp0492ConsumedCorpseEventArgs ev)
|
||||
@ -50,21 +56,21 @@ public class GrowingZombies : Plugin
|
||||
return;
|
||||
|
||||
// Increment corpse count for this zombie
|
||||
if (!_zombieCorpseCount.ContainsKey(ev.Player))
|
||||
_zombieCorpseCount[ev.Player] = 0;
|
||||
_zombieCorpseCount[ev.Player]++;
|
||||
if (!ZombieCorpseCount.ContainsKey(ev.Player))
|
||||
ZombieCorpseCount[ev.Player] = 0;
|
||||
ZombieCorpseCount[ev.Player]++;
|
||||
|
||||
var corpsesEaten = _zombieCorpseCount[ev.Player];
|
||||
var corpsesEaten = ZombieCorpseCount[ev.Player];
|
||||
|
||||
ev.Player.MaxHealth += 50;
|
||||
ev.Player.MaxHealth = Math.Min(1000, ev.Player.MaxHealth + 50);
|
||||
ev.Player.MaxHumeShield += 10;
|
||||
|
||||
var movementBoostIntensity = (byte)Math.Min(1 + corpsesEaten * 0.1f, 3f);
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<MovementBoost>(movementBoostIntensity, 30);
|
||||
var movementBoostIntensity = (byte)Math.Min(1 + corpsesEaten * 0.5f, 5f);
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<MovementBoost>(movementBoostIntensity, 120);
|
||||
|
||||
// Add damage resistance after eating multiple corpses
|
||||
var damageResistance = (byte)Math.Min(0.5 - corpsesEaten * 0.5f, 2f);
|
||||
if (corpsesEaten >= 3)
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<DamageReduction>(damageResistance, 20);
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<DamageReduction>((byte)(corpsesEaten*2), float.MaxValue);
|
||||
|
||||
// Add regeneration effect after eating multiple corpses
|
||||
if (corpsesEaten < 5) return;
|
||||
|
@ -23,6 +23,9 @@
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="HintServiceMeow">
|
||||
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mirror">
|
||||
<HintPath>..\dependencies\Mirror.dll</HintPath>
|
||||
</Reference>
|
||||
|
161
LobbyGame/LobbyGame.cs
Normal file
161
LobbyGame/LobbyGame.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using CommandSystem.Commands.RemoteAdmin;
|
||||
using GameCore;
|
||||
using LabApi.Events.Arguments.ServerEvents;
|
||||
using LabApi.Events.Handlers;
|
||||
using LabApi.Features;
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
using MEC;
|
||||
using Interactables.Interobjects.DoorUtils;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Features.Wrappers;
|
||||
using MapGeneration;
|
||||
using PlayerRoles;
|
||||
using UnityEngine;
|
||||
using Logger = LabApi.Features.Console.Logger;
|
||||
using Version = System.Version;
|
||||
|
||||
namespace LobbyGame
|
||||
{
|
||||
public class LobbyGame: Plugin
|
||||
{
|
||||
public override string Name => "LobbyGame";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "Adds a lobby minigame";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
public int RoundTimer { get; set; } = 20;
|
||||
public static LobbyGame Singleton { get; private set; }
|
||||
|
||||
|
||||
private bool _isStarted;
|
||||
private Room _randomRoom;
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
ServerEvents.WaitingForPlayers += WaitingForPlayers;
|
||||
PlayerEvents.Joined += PlayerJoined;
|
||||
Singleton = this;
|
||||
}
|
||||
|
||||
private static void PlayerJoined(PlayerJoinedEventArgs ev)
|
||||
{
|
||||
Timing.RunCoroutine(ContinuouslyTrySpawning());
|
||||
return;
|
||||
|
||||
IEnumerator<float> ContinuouslyTrySpawning()
|
||||
{
|
||||
while (!RoundStart.RoundStarted)
|
||||
{
|
||||
GameObject.Find("StartRound").transform.localScale = Vector3.zero;
|
||||
yield return Timing.WaitForSeconds(0.5f);
|
||||
if(Singleton._randomRoom == null) continue;
|
||||
if(!Singleton._isStarted) continue;
|
||||
ev.Player.SetRole(RoleTypeId.ChaosRifleman, RoleChangeReason.None, RoleSpawnFlags.None);
|
||||
ev.Player.Position = Singleton._randomRoom.Position + new Vector3(0, 1, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WaitingForPlayers()
|
||||
{
|
||||
Timing.CallDelayed(15, () =>
|
||||
{
|
||||
if (Singleton._isStarted) return;
|
||||
|
||||
var randomRoom = Map.GetRandomRoom(FacilityZone.Entrance);
|
||||
while (randomRoom is not { Zone: FacilityZone.Entrance })
|
||||
{
|
||||
randomRoom = Map.GetRandomRoom(FacilityZone.Entrance);
|
||||
}
|
||||
Logger.Debug($"Random entrance room: {randomRoom.Name}");
|
||||
|
||||
RoundStart.LobbyLock = true;
|
||||
|
||||
Singleton._randomRoom = randomRoom;
|
||||
Singleton._isStarted = true;
|
||||
|
||||
foreach (var player in Player.ReadyList)
|
||||
{
|
||||
player.SetRole(RoleTypeId.ChaosRifleman, RoleChangeReason.None, RoleSpawnFlags.None);
|
||||
|
||||
try
|
||||
{
|
||||
player.Position = randomRoom.Position + new Vector3(0, 1, 0);
|
||||
}
|
||||
catch (Exception _)
|
||||
{
|
||||
player.SetRole(RoleTypeId.Spectator);
|
||||
}
|
||||
}
|
||||
|
||||
Timing.RunCoroutine(ContinuouslyUpdateRoundTimer());
|
||||
|
||||
GameObject.Find("StartRound").transform.localScale = Vector3.zero;
|
||||
|
||||
foreach (var door in Map.Doors)
|
||||
{
|
||||
door.IsOpened = false;
|
||||
door.Lock(DoorLockReason.Lockdown2176, true);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
IEnumerator<float> ContinuouslyUpdateRoundTimer()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return Timing.WaitForSeconds(1);
|
||||
|
||||
foreach (var player in Player.ReadyList)
|
||||
{
|
||||
player.SendBroadcast(
|
||||
$"<size=30><color=grey>Round starts in</color> <color=red>{Singleton.RoundTimer}</color> <color=grey>seconds</color></size>",
|
||||
10, Broadcast.BroadcastFlags.Normal, true);
|
||||
}
|
||||
|
||||
Logger.Debug($"Round starts in {Singleton.RoundTimer} seconds");
|
||||
|
||||
if (Player.ReadyList.Count() <= 1)
|
||||
{
|
||||
Singleton.RoundTimer = 20;
|
||||
continue;
|
||||
}
|
||||
Singleton.RoundTimer--;
|
||||
if (Singleton.RoundTimer >= -1) continue;
|
||||
Singleton.RoundTimer = 20;
|
||||
foreach (var player in Player.ReadyList)
|
||||
{
|
||||
player.ClearInventory();
|
||||
player.SetRole(RoleTypeId.Spectator);
|
||||
}
|
||||
|
||||
foreach (var door in Map.Doors)
|
||||
{
|
||||
door.Lock(DoorLockReason.Lockdown2176, false);
|
||||
}
|
||||
|
||||
foreach (var player in Player.ReadyList)
|
||||
{
|
||||
player.SendBroadcast(
|
||||
"",
|
||||
1, Broadcast.BroadcastFlags.Normal, true);
|
||||
}
|
||||
|
||||
CharacterClassManager.ForceRoundStart();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
ServerEvents.WaitingForPlayers -= WaitingForPlayers;
|
||||
PlayerEvents.Joined -= PlayerJoined;
|
||||
Singleton = null;
|
||||
}
|
||||
}
|
||||
}
|
57
LobbyGame/LobbyGame.csproj
Normal file
57
LobbyGame/LobbyGame.csproj
Normal file
@ -0,0 +1,57 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>..\dependencies\0Harmony.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<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="UnityEngine.CoreModule">
|
||||
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
69
ModInfo/ModInfo.cs
Normal file
69
ModInfo/ModInfo.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using HintServiceMeow.Core.Enum;
|
||||
using HintServiceMeow.Core.Models.Hints;
|
||||
using HintServiceMeow.Core.Utilities;
|
||||
using LabApi.Features;
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
using LabApi.Features.Wrappers;
|
||||
using MEC;
|
||||
|
||||
namespace ModInfo
|
||||
{
|
||||
public class ModInfo : Plugin
|
||||
{
|
||||
public override string Name => "ModInfo";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "Shows some extra info for moderators";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
private readonly Dictionary<Player, Hint> _spectatorHints = new();
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
Timing.RunCoroutine(GodmodeHintLoop());
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private IEnumerator<float> GodmodeHintLoop()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
yield return Timing.WaitForSeconds(1);
|
||||
UpdateHints();
|
||||
}
|
||||
// ReSharper disable once IteratorNeverReturns
|
||||
}
|
||||
|
||||
private void UpdateHints()
|
||||
{
|
||||
foreach (var player in Player.ReadyList) UpdateHint(player);
|
||||
}
|
||||
|
||||
private void UpdateHint(Player player)
|
||||
{
|
||||
var hint = _spectatorHints.TryGetValue(player, out var hintValue) ? hintValue : AddPlayerHint(player);
|
||||
hint.Hide = !player.IsGodModeEnabled;
|
||||
}
|
||||
|
||||
private Hint AddPlayerHint(Player player)
|
||||
{
|
||||
var hint = new Hint
|
||||
{
|
||||
Text = "<size=40><color=#50C878>GODMODE</color></size>",
|
||||
Alignment = HintAlignment.Left,
|
||||
YCoordinate = 800,
|
||||
Hide = true
|
||||
};
|
||||
|
||||
var playerDisplay = PlayerDisplay.Get(player);
|
||||
playerDisplay.AddHint(hint);
|
||||
|
||||
_spectatorHints[player] = hint;
|
||||
return hint;
|
||||
}
|
||||
}
|
||||
}
|
62
ModInfo/ModInfo.csproj
Normal file
62
ModInfo/ModInfo.csproj
Normal file
@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>..\dependencies\0Harmony.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<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="UnityEngine.CoreModule">
|
||||
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include=".template.config\template.json"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -1,4 +1,5 @@
|
||||
using HintServiceMeow.Core.Enum;
|
||||
using System.Drawing;
|
||||
using HintServiceMeow.Core.Enum;
|
||||
using HintServiceMeow.Core.Models.Hints;
|
||||
using HintServiceMeow.Core.Utilities;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
@ -6,11 +7,13 @@ using LabApi.Events.Handlers;
|
||||
using LabApi.Features;
|
||||
using LabApi.Features.Console;
|
||||
using LabApi.Features.Wrappers;
|
||||
using MEC;
|
||||
using PlayerRoles;
|
||||
using PlayerRoles.PlayableScps.Scp079;
|
||||
using PlayerRoles.PlayableScps.Scp096;
|
||||
using PlayerRoles.PlayableScps.Scp3114;
|
||||
using Timer = System.Timers.Timer;
|
||||
using MEC;
|
||||
using PlayerRoles.PlayableScps.Scp049.Zombies;
|
||||
|
||||
namespace SCPTeamHint;
|
||||
|
||||
@ -27,11 +30,27 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
Logger.Debug("Apple juice");
|
||||
PlayerEvents.Joined += OnJoin;
|
||||
PlayerEvents.Left += OnLeft;
|
||||
|
||||
Timing.CallContinuously(1, UpdateHints);
|
||||
Timing.RunCoroutine(ContinuouslyUpdateHints());
|
||||
}
|
||||
|
||||
private IEnumerator<float> ContinuouslyUpdateHints()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return Timing.WaitForSeconds(1);
|
||||
try
|
||||
{
|
||||
UpdateHints();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e);
|
||||
}
|
||||
}
|
||||
// ReSharper disable once IteratorNeverReturns
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
@ -46,11 +65,11 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
|
||||
lock (_hintsLock)
|
||||
{
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy && x.IsSCP))
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy && (x.IsSCP || x.Role is RoleTypeId.Scp0492)))
|
||||
{
|
||||
var text =
|
||||
$" <size=25><color=red>{player.RoleBase.RoleName}</color> | <color=#6761cd>{player.HumeShield}</color> | <color=#da0101>{player.Health}</color> | <color=grey>{player.Zone}</color></size> ";
|
||||
|
||||
|
||||
switch (player.RoleBase)
|
||||
{
|
||||
case Scp096Role scp:
|
||||
@ -60,17 +79,23 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
|
||||
if (!tracker) break;
|
||||
|
||||
text += $"Targets: {tracker.Targets.Count}";
|
||||
var targetColor = tracker.Targets.Count > 0 ? "red" : "grey";
|
||||
text += $"<color=grey>Targets:</color> <color={targetColor}>{tracker.Targets.Count}</color>";
|
||||
break;
|
||||
case Scp3114Role scp3114:
|
||||
{
|
||||
text += "\n";
|
||||
|
||||
var stolenRole = scp3114.CurIdentity.StolenRole;
|
||||
|
||||
text += $" {stolenRole}";
|
||||
if (scp3114.Disguised)
|
||||
{
|
||||
text += $" {stolenRole}";
|
||||
}
|
||||
else
|
||||
{
|
||||
text += " None";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Scp079Role scp079:
|
||||
text =
|
||||
$" <size=25><color=red>{player.RoleBase.RoleName}</color> | <color=grey>{scp079.CurrentCamera.Room.Zone}</color></size> ";
|
||||
@ -82,7 +107,16 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
if (!auxManager || !tierManager) break;
|
||||
|
||||
text +=
|
||||
$" <color=grey>AUX: {auxManager.CurrentAuxFloored} / {auxManager.MaxAux} | Level {tierManager.AccessTierLevel}</color>";
|
||||
$" <color=#FFEF00>AUX: {auxManager.CurrentAuxFloored}</color> / {auxManager.MaxAux} | <color=#FFD700>Level {tierManager.AccessTierLevel}</color>";
|
||||
break;
|
||||
case ZombieRole:
|
||||
var count = GrowingZombies.GrowingZombies.Instance.ZombieCorpseCount[player];
|
||||
|
||||
const string corpseColor = "E68A8A";
|
||||
|
||||
text += "\n";
|
||||
|
||||
text += $" <color=#{corpseColor}>Corpses eaten: {count}</color>";
|
||||
break;
|
||||
}
|
||||
|
||||
@ -108,8 +142,8 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
}
|
||||
|
||||
Logger.Debug(
|
||||
$"Player {player.Nickname} is on team {player.RoleBase.Team} | hide: {player.RoleBase.Team != Team.SCPs}");
|
||||
hint.Hide = player.RoleBase.Team != Team.SCPs;
|
||||
$"Player {player.Nickname} is on team {player.RoleBase.Team} with Role {player.Role} | hide: {player.RoleBase.Team != Team.SCPs}");
|
||||
hint.Hide = player.RoleBase.Team != Team.SCPs && player.Role != RoleTypeId.Scp0492 && player.Role != RoleTypeId.Overwatch;
|
||||
if (!hint.Hide) hint.Text = hintText;
|
||||
}
|
||||
|
||||
|
@ -45,4 +45,9 @@
|
||||
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GrowingZombies\GrowingZombies.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -34,6 +34,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerHints", "ServerHints\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowingZombies", "GrowingZombies\GrowingZombies.csproj", "{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemplateProject", "TemplateProject\TemplateProject.csproj", "{E5A28D1C-638F-4849-9784-240D50A6DA29}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyGame", "LobbyGame\LobbyGame.csproj", "{E02243D5-0229-47BB-88A7-252EC753C8CC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatsTracker", "StatsTracker\StatsTracker.csproj", "{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModInfo", "ModInfo\ModInfo.csproj", "{8C55C629-FFB9-41AC-8F5C-1BF715110766}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -108,5 +116,21 @@ Global
|
||||
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8C55C629-FFB9-41AC-8F5C-1BF715110766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C55C629-FFB9-41AC-8F5C-1BF715110766}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C55C629-FFB9-41AC-8F5C-1BF715110766}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8C55C629-FFB9-41AC-8F5C-1BF715110766}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
5
StatsTracker/FodyWeavers.xml
Normal file
5
StatsTracker/FodyWeavers.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura>
|
||||
</Costura>
|
||||
</Weavers>
|
176
StatsTracker/FodyWeavers.xsd
Normal file
176
StatsTracker/FodyWeavers.xsd
Normal file
@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
162
StatsTracker/StatsTracker.cs
Normal file
162
StatsTracker/StatsTracker.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using LabApi.Features;
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data.SQLite;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Events.Handlers;
|
||||
using LabApi.Loader;
|
||||
|
||||
namespace StatsTracker
|
||||
{
|
||||
public class StatsTracker : Plugin
|
||||
{
|
||||
public override string Name => "StatsTracker";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "Tracks stats for players.";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
private string _dbPath;
|
||||
private readonly ConcurrentDictionary<string, PlayerStats> _currentSessionStats = new();
|
||||
|
||||
private class PlayerStats
|
||||
{
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
public Dictionary<ItemType, int> ItemUsage { get; set; } = new();
|
||||
}
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
_dbPath = Path.Combine(this.GetConfigDirectory().FullName, "stats.db");
|
||||
InitializeDatabase();
|
||||
|
||||
PlayerEvents.Death += OnPlayerDied;
|
||||
PlayerEvents.UsedItem += OnItemUsed;
|
||||
PlayerEvents.Left += OnPlayerLeft;
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
PlayerEvents.Death -= OnPlayerDied;
|
||||
PlayerEvents.UsedItem -= OnItemUsed;
|
||||
PlayerEvents.Left -= OnPlayerLeft;
|
||||
|
||||
// Save any remaining stats
|
||||
foreach (var player in _currentSessionStats)
|
||||
{
|
||||
SavePlayerStats(player.Key, player.Value);
|
||||
}
|
||||
_currentSessionStats.Clear();
|
||||
}
|
||||
|
||||
private void InitializeDatabase()
|
||||
{
|
||||
using var connection = new SQLiteConnection($"Data Source={_dbPath}");
|
||||
connection.Open();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
|
||||
CREATE TABLE IF NOT EXISTS PlayerStats (
|
||||
UserId TEXT PRIMARY KEY,
|
||||
Kills INTEGER DEFAULT 0,
|
||||
Deaths INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ItemUsage (
|
||||
UserId TEXT,
|
||||
ItemType INTEGER,
|
||||
UsageCount INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (UserId, ItemType)
|
||||
);
|
||||
""";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private void OnPlayerDied(PlayerDeathEventArgs ev)
|
||||
{
|
||||
if (ev.Attacker != null)
|
||||
{
|
||||
var killerStats = _currentSessionStats.GetOrAdd(ev.Attacker.UserId, _ => new PlayerStats());
|
||||
killerStats.Kills++;
|
||||
}
|
||||
|
||||
var victimStats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats());
|
||||
victimStats.Deaths++;
|
||||
}
|
||||
|
||||
private void OnItemUsed(PlayerUsedItemEventArgs ev)
|
||||
{
|
||||
var stats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats());
|
||||
|
||||
if (!stats.ItemUsage.ContainsKey(ev.UsableItem.Type))
|
||||
stats.ItemUsage[ev.UsableItem.Type] = 0;
|
||||
|
||||
stats.ItemUsage[ev.UsableItem.Type]++;
|
||||
}
|
||||
|
||||
private void OnPlayerLeft(PlayerLeftEventArgs ev)
|
||||
{
|
||||
if (_currentSessionStats.TryRemove(ev.Player.UserId, out var stats))
|
||||
{
|
||||
SavePlayerStats(ev.Player.UserId, stats);
|
||||
}
|
||||
}
|
||||
|
||||
private void SavePlayerStats(string userId, PlayerStats stats)
|
||||
{
|
||||
using var connection = new SQLiteConnection($"Data Source={_dbPath}");
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
// Update player stats
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = """
|
||||
|
||||
INSERT INTO PlayerStats (UserId, Kills, Deaths)
|
||||
VALUES (@userId, @kills, @deaths)
|
||||
ON CONFLICT(UserId) DO UPDATE SET
|
||||
Kills = Kills + @kills,
|
||||
Deaths = Deaths + @deaths;
|
||||
""";
|
||||
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@kills", stats.Kills);
|
||||
command.Parameters.AddWithValue("@deaths", stats.Deaths);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Update item usage
|
||||
foreach (var itemInfoPair in stats.ItemUsage)
|
||||
{
|
||||
var itemType = itemInfoPair.Key;
|
||||
var count = itemInfoPair.Value;
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
|
||||
INSERT INTO ItemUsage (UserId, ItemType, UsageCount)
|
||||
VALUES (@userId, @itemType, @count)
|
||||
ON CONFLICT(UserId, ItemType) DO UPDATE SET
|
||||
UsageCount = UsageCount + @count;
|
||||
""";
|
||||
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@itemType", (int)itemType);
|
||||
command.Parameters.AddWithValue("@count", count);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
StatsTracker/StatsTracker.csproj
Normal file
62
StatsTracker/StatsTracker.csproj
Normal file
@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Costura.Fody" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>..\dependencies\0Harmony.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<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="UnityEngine.CoreModule">
|
||||
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
13
TemplateProject/.template.config/template.json
Normal file
13
TemplateProject/.template.config/template.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"author": "Code002Lover",
|
||||
"name": "SCP:SL LabAPI template",
|
||||
"description": "LabAPI basic template for own use",
|
||||
"identity": "Code002Lover.LabAPI.1.0",
|
||||
"shortName": "labapi",
|
||||
"tags": {
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
},
|
||||
"sourceName": "TemplateProject"
|
||||
}
|
||||
|
24
TemplateProject/TemplateProject.cs
Normal file
24
TemplateProject/TemplateProject.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using LabApi.Features;
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
|
||||
namespace TemplateProject
|
||||
{
|
||||
public class TemplateProject: Plugin
|
||||
{
|
||||
public override string Name => "TemplateProject";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "Is a template for creating plugins. It does nothing.";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
62
TemplateProject/TemplateProject.csproj
Normal file
62
TemplateProject/TemplateProject.csproj
Normal file
@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<Optimize>true</Optimize>
|
||||
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>..\dependencies\0Harmony.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<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="UnityEngine.CoreModule">
|
||||
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include=".template.config\template.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
75
VisibleSpectators/PlayerDisplayUtil.cs
Normal file
75
VisibleSpectators/PlayerDisplayUtil.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using PlayerRoles;
|
||||
using LabApi.Features;
|
||||
using LabApi.Features.Wrappers;
|
||||
|
||||
namespace VisibleSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Utility for formatting player display names and color mapping.
|
||||
/// </summary>
|
||||
public static class PlayerDisplayUtil
|
||||
{
|
||||
private static readonly Dictionary<string, string> ColorMap = new()
|
||||
{
|
||||
{ "DEFAULT", "FFFFFF" },
|
||||
{ "PUMPKIN", "EE7600" },
|
||||
{ "ARMY_GREEN", "4B5320" },
|
||||
{ "MINT", "98FB98" },
|
||||
{ "NICKEL", "727472" },
|
||||
{ "CARMINE", "960018" },
|
||||
{ "EMERALD", "50C878" },
|
||||
{ "GREEN", "228B22" },
|
||||
{ "LIME", "BFFF00" },
|
||||
{ "POLICE_BLUE", "002DB3" },
|
||||
{ "ORANGE", "FF9966" },
|
||||
{ "SILVER_BLUE", "666699" },
|
||||
{ "BLUE_GREEN", "4DFFB8" },
|
||||
{ "MAGENTA", "FF0090" },
|
||||
{ "YELLOW", "FAFF86" },
|
||||
{ "TOMATO", "FF6448" },
|
||||
{ "DEEP_PINK", "FF1493" },
|
||||
{ "AQUA", "00FFFF" },
|
||||
{ "CYAN", "00B7EB" },
|
||||
{ "CRIMSON", "DC143C" },
|
||||
{ "LIGHT_GREEN", "32CD32" },
|
||||
{ "SILVER", "A0A0A0" },
|
||||
{ "BROWN", "944710" },
|
||||
{ "RED", "C50000" },
|
||||
{ "PINK", "FF96DE" },
|
||||
{ "LIGHT_RED", "FD8272" },
|
||||
{ "PURPLE", "8137CE" },
|
||||
{ "BLUE", "005EBC" },
|
||||
{ "TEAL", "008080" },
|
||||
{ "GOLD", "EFC01A" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns a formatted display string for a player, with color.
|
||||
/// </summary>
|
||||
public static string PlayerToDisplay(Player player)
|
||||
{
|
||||
if (player is not { IsReady: true }) return string.Empty;
|
||||
const string defaultColor = "FFFFFF";
|
||||
try
|
||||
{
|
||||
var groupColor = player.GroupColor;
|
||||
if (string.IsNullOrEmpty(groupColor))
|
||||
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
return ColorMap.TryGetValue(groupColor.ToUpper(), out var color)
|
||||
? $"<color=#{color}FF>{player.DisplayName}</color>"
|
||||
: $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the player is not Overwatch.
|
||||
/// </summary>
|
||||
public static bool IsNotOverwatch(Player player)
|
||||
{
|
||||
return player != null && player.Role != RoleTypeId.Overwatch;
|
||||
}
|
||||
}
|
38
VisibleSpectators/Plugin.cs
Normal file
38
VisibleSpectators/Plugin.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
using LabApi.Features;
|
||||
using LabApi.Events.Handlers;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Features.Console;
|
||||
using MEC;
|
||||
|
||||
namespace VisibleSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Main entry point for the VisibleSpectators plugin.
|
||||
/// </summary>
|
||||
public class Plugin : Plugin<SpectatorConfig>
|
||||
{
|
||||
private SpectatorManager _spectatorManager;
|
||||
public override string Name => "VisibleSpectators";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "See your spectators";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
Logger.Debug("starting...");
|
||||
_spectatorManager = new SpectatorManager(Config);
|
||||
PlayerEvents.ChangedSpectator += _spectatorManager.OnSpectate;
|
||||
PlayerEvents.Joined += _spectatorManager.OnJoin;
|
||||
Timing.RunCoroutine(_spectatorManager.KeepUpdatingSpectators());
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
Logger.Debug("unloading...");
|
||||
PlayerEvents.Joined -= _spectatorManager.OnJoin;
|
||||
PlayerEvents.ChangedSpectator -= _spectatorManager.OnSpectate;
|
||||
_spectatorManager = null;
|
||||
}
|
||||
}
|
16
VisibleSpectators/SpectatorConfig.cs
Normal file
16
VisibleSpectators/SpectatorConfig.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace VisibleSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the VisibleSpectators plugin.
|
||||
/// </summary>
|
||||
public class SpectatorConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Header message shown above the spectator list.
|
||||
/// </summary>
|
||||
public string HeaderMessage { get; set; } = "Spectators:";
|
||||
/// <summary>
|
||||
/// Message shown when there are no spectators.
|
||||
/// </summary>
|
||||
public string NoSpectatorsMessage { get; set; } = "No spectators";
|
||||
}
|
98
VisibleSpectators/SpectatorManager.cs
Normal file
98
VisibleSpectators/SpectatorManager.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using HintServiceMeow.Core.Enum;
|
||||
using HintServiceMeow.Core.Models.Hints;
|
||||
using HintServiceMeow.Core.Utilities;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Features;
|
||||
using LabApi.Features.Console;
|
||||
using LabApi.Features.Wrappers;
|
||||
using MEC;
|
||||
using PlayerRoles;
|
||||
|
||||
namespace VisibleSpectators;
|
||||
|
||||
/// <summary>
|
||||
/// Handles spectator hint management and updates for players.
|
||||
/// </summary>
|
||||
public class SpectatorManager
|
||||
{
|
||||
private readonly SpectatorConfig _config;
|
||||
private readonly Dictionary<Player, Hint> _spectatorHints = new();
|
||||
public int YCoordinate { get; set; } = 100;
|
||||
|
||||
public SpectatorManager(SpectatorConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public IEnumerator<float> KeepUpdatingSpectators()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
UpdateSpectators();
|
||||
yield return Timing.WaitForSeconds(1);
|
||||
}
|
||||
// ReSharper disable once IteratorNeverReturns
|
||||
}
|
||||
|
||||
public void OnSpectate(PlayerChangedSpectatorEventArgs ev)
|
||||
{
|
||||
UpdateSpectators(ev.OldTarget);
|
||||
UpdateSpectators(ev.NewTarget);
|
||||
UpdateSpectators(ev.Player);
|
||||
}
|
||||
|
||||
public void OnJoin(PlayerJoinedEventArgs ev)
|
||||
{
|
||||
AddPlayerHint(ev.Player);
|
||||
}
|
||||
|
||||
private void UpdateSpectators()
|
||||
{
|
||||
foreach (var player in GetPlayers())
|
||||
UpdateSpectators(player);
|
||||
}
|
||||
|
||||
private void AddPlayerHint(Player player)
|
||||
{
|
||||
var hint = new Hint
|
||||
{
|
||||
Text = $"{_config.HeaderMessage}\n{_config.NoSpectatorsMessage}",
|
||||
Alignment = HintAlignment.Right,
|
||||
YCoordinate = YCoordinate,
|
||||
Hide = true
|
||||
};
|
||||
var playerDisplay = PlayerDisplay.Get(player);
|
||||
playerDisplay.AddHint(hint);
|
||||
_spectatorHints[player] = hint;
|
||||
}
|
||||
|
||||
private void UpdateSpectators(Player player)
|
||||
{
|
||||
if (player == null) return;
|
||||
if (!_spectatorHints.ContainsKey(player)) AddPlayerHint(player);
|
||||
var spectators = _config.NoSpectatorsMessage;
|
||||
try
|
||||
{
|
||||
spectators = string.Join("\n", player.CurrentSpectators.Where(PlayerDisplayUtil.IsNotOverwatch).Select(PlayerDisplayUtil.PlayerToDisplay));
|
||||
if (player.Role == RoleTypeId.Spectator)
|
||||
spectators = player.CurrentlySpectating == null
|
||||
? _config.NoSpectatorsMessage
|
||||
: string.Join("\n",
|
||||
player.CurrentlySpectating?.CurrentSpectators.Where(PlayerDisplayUtil.IsNotOverwatch)
|
||||
.Select(PlayerDisplayUtil.PlayerToDisplay) ?? Array.Empty<string>());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e);
|
||||
}
|
||||
if (spectators.Length < 2) spectators = _config.NoSpectatorsMessage;
|
||||
_spectatorHints[player].Text = $"{_config.HeaderMessage}\n{spectators}";
|
||||
_spectatorHints[player].Hide = player.Role is RoleTypeId.Destroyed or RoleTypeId.None;
|
||||
_spectatorHints[player].YCoordinate = YCoordinate + player.CurrentSpectators.Count * 10;
|
||||
}
|
||||
|
||||
private static Player[] GetPlayers()
|
||||
{
|
||||
return Player.ReadyList.Where(PlayerDisplayUtil.IsNotOverwatch).Where(x => x != null).ToArray();
|
||||
}
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
using HintServiceMeow.Core.Enum;
|
||||
using HintServiceMeow.Core.Models.Hints;
|
||||
using HintServiceMeow.Core.Utilities;
|
||||
using LabApi.Events.Arguments.PlayerEvents;
|
||||
using LabApi.Events.Handlers;
|
||||
using LabApi.Features;
|
||||
using LabApi.Features.Console;
|
||||
using LabApi.Features.Wrappers;
|
||||
using LabApi.Loader.Features.Plugins;
|
||||
using MEC;
|
||||
using PlayerRoles;
|
||||
|
||||
namespace VisibleSpectators;
|
||||
|
||||
public class Plugin : Plugin<SpectatorConfig>
|
||||
{
|
||||
private static Plugin _singleton;
|
||||
|
||||
private static readonly Dictionary<string, string> GetColorMap = new()
|
||||
{
|
||||
{ "DEFAULT", "FFFFFF" },
|
||||
{ "PUMPKIN", "EE7600" },
|
||||
{ "ARMY_GREEN", "4B5320" },
|
||||
{ "MINT", "98FB98" },
|
||||
{ "NICKEL", "727472" },
|
||||
{ "CARMINE", "960018" },
|
||||
{ "EMERALD", "50C878" },
|
||||
{ "GREEN", "228B22" },
|
||||
{ "LIME", "BFFF00" },
|
||||
{ "POLICE_BLUE", "002DB3" },
|
||||
{ "ORANGE", "FF9966" },
|
||||
{ "SILVER_BLUE", "666699" },
|
||||
{ "BLUE_GREEN", "4DFFB8" },
|
||||
{ "MAGENTA", "FF0090" },
|
||||
{ "YELLOW", "FAFF86" },
|
||||
{ "TOMATO", "FF6448" },
|
||||
{ "DEEP_PINK", "FF1493" },
|
||||
{ "AQUA", "00FFFF" },
|
||||
{ "CYAN", "00B7EB" },
|
||||
{ "CRIMSON", "DC143C" },
|
||||
{ "LIGHT_GREEN", "32CD32" },
|
||||
{ "SILVER", "A0A0A0" },
|
||||
{ "BROWN", "944710" },
|
||||
{ "RED", "C50000" },
|
||||
{ "PINK", "FF96DE" },
|
||||
{ "LIGHT_RED", "FD8272" },
|
||||
{ "PURPLE", "8137CE" },
|
||||
{ "BLUE", "005EBC" },
|
||||
{ "TEAL", "008080" },
|
||||
{ "GOLD", "EFC01A" }
|
||||
};
|
||||
|
||||
private readonly Dictionary<Player, Hint> _spectatorHints = new();
|
||||
public override string Name => "VisibleSpectators";
|
||||
public override string Author => "Code002Lover";
|
||||
public override Version Version { get; } = new(1, 0, 0);
|
||||
public override string Description => "See your spectators";
|
||||
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
|
||||
|
||||
public int YCoordinate { get; set; } = 100;
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
Logger.Debug("starting...");
|
||||
_singleton = this;
|
||||
|
||||
PlayerEvents.ChangedSpectator += OnSpectate;
|
||||
PlayerEvents.Joined += OnJoin;
|
||||
|
||||
Timing.CallContinuously(1, UpdateSpectators);
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
Logger.Debug("unloading...");
|
||||
|
||||
PlayerEvents.Joined -= OnJoin;
|
||||
PlayerEvents.ChangedSpectator -= OnSpectate;
|
||||
|
||||
_singleton = null;
|
||||
}
|
||||
|
||||
private void UpdateSpectators()
|
||||
{
|
||||
foreach (var player in GetPlayers()) UpdateSpectators(player);
|
||||
}
|
||||
|
||||
private void AddPlayerHint(Player player)
|
||||
{
|
||||
var hint = new Hint
|
||||
{
|
||||
Text = $"{Config!.HeaderMessage}\n{Config!.NoSpectatorsMessage}",
|
||||
Alignment = HintAlignment.Right,
|
||||
YCoordinate = YCoordinate,
|
||||
Hide = true
|
||||
};
|
||||
|
||||
var playerDisplay = PlayerDisplay.Get(player);
|
||||
playerDisplay.AddHint(hint);
|
||||
|
||||
_spectatorHints[player] = hint;
|
||||
}
|
||||
|
||||
private static string PlayerToDisplay(Player player)
|
||||
{
|
||||
if (player == null) return "";
|
||||
if (!player.IsReady) return "";
|
||||
|
||||
// Default color if GroupColor is null or not found in the map
|
||||
const string defaultColor = "FFFFFF";
|
||||
|
||||
try
|
||||
{
|
||||
var groupColor = player.GroupColor;
|
||||
if (string.IsNullOrEmpty(groupColor))
|
||||
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
|
||||
return GetColorMap.TryGetValue(groupColor.ToUpper(), out var color)
|
||||
? $"<color=#{color}FF>{player.DisplayName}</color>"
|
||||
: $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNotOverwatch(Player player)
|
||||
{
|
||||
return player != null && player.Role != RoleTypeId.Overwatch;
|
||||
}
|
||||
|
||||
private void UpdateSpectators(Player player)
|
||||
{
|
||||
// Safety check - if player doesn't have a hint, create one
|
||||
if (!_spectatorHints.ContainsKey(player)) AddPlayerHint(player);
|
||||
|
||||
var spectators = Config!.NoSpectatorsMessage;
|
||||
|
||||
try
|
||||
{
|
||||
spectators = string.Join("\n", player.CurrentSpectators.Where(IsNotOverwatch).Select(PlayerToDisplay));
|
||||
if (player.Role == RoleTypeId.Spectator)
|
||||
spectators = player.CurrentlySpectating == null
|
||||
? Config!.NoSpectatorsMessage
|
||||
: string.Join("\n",
|
||||
player.CurrentlySpectating?.CurrentSpectators.Where(IsNotOverwatch)
|
||||
.Select(PlayerToDisplay) ?? Array.Empty<string>());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e);
|
||||
}
|
||||
|
||||
if (spectators.Length < 2) spectators = Config!.NoSpectatorsMessage;
|
||||
|
||||
|
||||
_spectatorHints[player].Text = $"{Config!.HeaderMessage}\n{spectators}";
|
||||
|
||||
_spectatorHints[player].Hide = player.Role is RoleTypeId.Destroyed or RoleTypeId.None;
|
||||
|
||||
_spectatorHints[player].YCoordinate = YCoordinate + player.CurrentSpectators.Count * 10;
|
||||
}
|
||||
|
||||
private static Player[] GetPlayers()
|
||||
{
|
||||
return Player.ReadyList.Where(IsNotOverwatch).ToArray();
|
||||
}
|
||||
|
||||
private static void OnSpectate(PlayerChangedSpectatorEventArgs ev)
|
||||
{
|
||||
_singleton.UpdateSpectators(ev.OldTarget);
|
||||
_singleton.UpdateSpectators(ev.NewTarget);
|
||||
_singleton.UpdateSpectators(ev.Player);
|
||||
}
|
||||
|
||||
private void OnJoin(PlayerJoinedEventArgs ev)
|
||||
{
|
||||
AddPlayerHint(ev.Player);
|
||||
}
|
||||
}
|
||||
|
||||
public class SpectatorConfig
|
||||
{
|
||||
public string HeaderMessage { get; set; } = "Spectators:";
|
||||
public string NoSpectatorsMessage { get; set; } = "No spectators";
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user