feat: add BasePower abstraction

This commit is contained in:
admin
2026-07-26 23:47:38 +02:00
commit 6dbbfc0dde
83 changed files with 7587 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
using Cs2SuperPowers.Utils;
namespace Cs2SuperPowers.Powers;
public class AirStrike(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 21;
public override string Name => "Air Strike";
public override string Description => "Decoys thrown by you launch an airstrike (5 second cooldown).";
public override char ChatColor => ChatColors.Red;
public override string HtmlColor => "#FF0000";
private readonly Dictionary<CCSPlayerController, DateTime> _lastUse = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventDecoyStarted>(OnDecoyStarted);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_lastUse.Clear();
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().IsAlive().Check() || !IsAssignedTo(player!))
{
continue;
}
NextFrame(() =>
{
player.GiveNamedItem("weapon_decoy");
});
}
return HookResult.Continue;
}
private HookResult OnDecoyStarted(EventDecoyStarted @event, GameEventInfo _)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
// Check cooldown (skip if nodelay is enabled)
var now = DateTime.UtcNow;
DateTime lastUseTime = default;
bool isOnCooldown = !GlobalConfig.NoDelayEnabled && _lastUse.TryGetValue(player!, out lastUseTime) && (now - lastUseTime).TotalSeconds < 5;
// Always give a new decoy, regardless of cooldown!
player!.GiveNamedItem("weapon_decoy");
if (isOnCooldown)
{
double secondsRemaining = Math.Ceiling(5 - (now - lastUseTime).TotalSeconds);
player.PrintToCenterAlert($"Air Strike is on cooldown! {ChatColors.DarkRed}{secondsRemaining}{ChatColors.Default}s remaining");
return HookResult.Continue;
}
_lastUse[player!] = now;
var pos = new Vector(@event.X, @event.Y, @event.Z);
// Airstrike: spawn grenades above the target, spread over an area, falling down
var random = new Random();
int grenadeCount = GlobalConfig.GetAirStrikeCount(20);
float airstrikeHeight = 800.0f; // Height above the detonation point
float spreadRadius = 300.0f; // How far from the center grenades can land
for (int i = 0; i < grenadeCount; i++)
{
// Random offset in a circle around the target
double angle = random.NextDouble() * 2 * Math.PI;
double radius = Math.Sqrt(random.NextDouble()) * spreadRadius; // sqrt for uniform distribution
float offsetX = (float)(Math.Cos(angle) * radius);
float offsetY = (float)(Math.Sin(angle) * radius);
// Spawn position: above the ground, with random spread
var spawnPos = new Vector(
pos.X + offsetX,
pos.Y + offsetY,
pos.Z + airstrikeHeight + (float)(random.NextDouble() * 100.0 - 50.0) // small vertical variation
);
// Angle: straight down, maybe with a little randomization
float pitch = -90f + (float)(random.NextDouble() * 10.0 - 5.0); // -90 is straight down
float yaw = (float)(random.NextDouble() * 360.0);
var ang = new QAngle(pitch, yaw, 0);
// Velocity: mostly downward, with a little horizontal for realism
float downwardSpeed = (float)(random.NextDouble() * 200.0 + 800.0); // 800-1000 units/sec down
float horizontalSpeed = (float)(random.NextDouble() * 100.0); // up to 100 units/sec sideways
// Calculate horizontal direction
float horizAngle = (float)(random.NextDouble() * 2 * Math.PI);
var horizontal = new Vector(
(float)Math.Cos(horizAngle) * horizontalSpeed,
(float)Math.Sin(horizAngle) * horizontalSpeed,
0
);
var velocity = new Vector(
horizontal.X,
horizontal.Y,
-downwardSpeed
);
var createdGrenade = GrenadeSpawner.CreateGrenade(spawnPos, ang, velocity, player.PlayerPawn.Value);
}
return HookResult.Continue;
}
}
+65
View File
@@ -0,0 +1,65 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class ArmBreaker(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 6;
public override string Name => "Arms Breaker";
public override string Description => $"You have 25% chance to disarm victim on hit.";
public override char ChatColor => ChatColors.LightBlue;
private readonly Random _random = new();
public override string HtmlColor => "#D6E6FF";
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (
!Ensure.Player(victim).IsValid().IsAlive().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 1 player in the opposing team
var attackerTeam = attacker!.Team;
var enemyCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator && p.Team != attackerTeam);
if (enemyCount < 1)
{
return HookResult.Continue;
}
if (_random.NextDouble() <= 0.25)
{
var weaponServices = victim!.PlayerPawn?.Value?.WeaponServices;
if (weaponServices?.MyWeapons == null) return HookResult.Continue;
foreach (var weapon in weaponServices.MyWeapons)
{
var weaponName = weapon.Value?.DesignerName;
if (weaponName != null && !weaponName.Contains("weapon_knife") && !weaponName.Contains("weapon_c4"))
{
victim.DropActiveWeapon();
}
}
}
return HookResult.Continue;
}
}
+8
View File
@@ -0,0 +1,8 @@
using CounterStrikeSharp.API.Core;
namespace Cs2SuperPowers.Powers.Assign;
public interface IPowerAssignLogic
{
void Initialize(BasePlugin pluginRef, IReadOnlyCollection<ISuperPower> powers);
}
+149
View File
@@ -0,0 +1,149 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers.Assign;
public class RandomAssignLogic : IPowerAssignLogic
{
// Configuration for special rounds
private const double SpecialRoundChance = 0.25;
// Powers that can be assigned during special rounds (everyone gets the same power)
private readonly string[] SpecialRoundPowers =
{
"Go Fast", "Grenade Launcher", "Air Strike", "Not So Decoy", "I Am Chicken!"
};
private readonly List<Delegate> _registeredHandlers = new();
public void Initialize(BasePlugin pluginRef, IReadOnlyCollection<ISuperPower> powers)
{
BasePlugin.GameEventHandler<EventPlayerDisconnect> disconnectHandler = (@event, _) =>
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check()) return HookResult.Continue;
PlayerPowers.Instance.RemovePower(player!);
return HookResult.Continue;
};
_registeredHandlers.Add(disconnectHandler);
pluginRef.RegisterEventHandler(disconnectHandler);
BasePlugin.GameEventHandler<EventRoundStart> roundStartHandler = (_, _) =>
{
var gameRules = Utilities
.FindAllEntitiesByDesignerName<CBaseEntity>("cs_gamerules")
.Single()
.As<CCSGameRulesProxy>()
.GameRules!;
if (gameRules.WarmupPeriod)
{
return HookResult.Continue;
}
var activePlayerCount = Utilities.GetPlayers().Count(player =>
player.IsValid && player.Team != CsTeam.None && player.Team != CsTeam.Spectator);
var randomAssignablePowers = powers
.Where(p => p.CanBeRandomlyAssigned && p.MinimumPlayersRequired <= activePlayerCount)
.ToArray();
if (randomAssignablePowers.Length == 0)
{
return HookResult.Continue;
}
var random = new Random();
var isSpecialRound = random.NextDouble() < SpecialRoundChance;
if (isSpecialRound)
{
// Special round: everyone gets the same power
var specialRoundCandidates = randomAssignablePowers
.Where(p => SpecialRoundPowers.Contains(p.Name))
.ToArray();
var specialPower = specialRoundCandidates.Length == 0
? null
: specialRoundCandidates[random.Next(specialRoundCandidates.Length)];
if (specialPower != null)
{
// Assign the same power to all players (without individual messages)
foreach (var player in Utilities.GetPlayers())
{
if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) continue;
PlayerPowers.Instance.AddPower(player!, specialPower, showMessages: false);
}
// Announce special round to all players
Server.PrintToChatAll($"{ChatColors.Gold}SPECIAL ROUND!{ChatColors.Default}");
Server.PrintToChatAll($"{ChatColors.Yellow}Everyone has the same power: {ChatColors.Default}{specialPower.ChatColor}{specialPower.Name}{ChatColors.Default}");
}
else
{
// Fallback to normal assignment if special power not found
foreach (var player in Utilities.GetPlayers())
{
if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) continue;
var randomPower = randomAssignablePowers.OrderBy(_ => random.Next()).First();
PlayerPowers.Instance.AddPower(player!, randomPower, showMessages: true);
}
}
}
else
{
// Normal round: random powers for each player
foreach (var player in Utilities.GetPlayers())
{
if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) continue;
var randomPower = randomAssignablePowers.OrderBy(_ => random.Next()).First();
PlayerPowers.Instance.AddPower(player!, randomPower, showMessages: true);
}
// Show individual powers to each team
foreach (var player in Utilities.GetPlayers())
{
var team = player.Team;
player.PrintToChat($"Your {ChatColors.Blue}Team {ChatColors.Default}powers are:");
PlayerPowers.Instance
.GetAssignedPowers()
.Where(x => x.Player.Team == team)
.Select(x => {
player.PrintToChat($"{ChatColors.DarkRed} ► {x.Player.PlayerName} {ChatColors.Default}: {x.Power.ChatColor}{x.Power.Name}");
return x;
})
.ToList();
}
}
return HookResult.Continue;
};
_registeredHandlers.Add(roundStartHandler);
pluginRef.RegisterEventHandler(roundStartHandler, HookMode.Pre);
BasePlugin.GameEventHandler<EventPlayerDeath> deathHandler = (@event, _) =>
{
var victim = @event.Userid;
var attacker = @event.Attacker;
if (victim == null || attacker == null || victim == attacker) return HookResult.Continue;
if (!PlayerPowers.Instance.TryGetValue(attacker, out var attackerPower))
{
return HookResult.Continue;
}
victim.PrintToChat($"You have been killed by {ChatColors.DarkRed}{attacker.PlayerName}{ChatColors.Lime}");
victim.PrintToChat($"His/her super power was: {attackerPower!.ChatColor}{attackerPower.Name}");
return HookResult.Continue;
};
_registeredHandlers.Add(deathHandler);
pluginRef.RegisterEventHandler(deathHandler);
}
}
+110
View File
@@ -0,0 +1,110 @@
using System.Text;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Events;
using Cs2SuperPowers.Players;
namespace Cs2SuperPowers.Powers;
public abstract class BasePower(IPlayerHud playerHud) : ISuperPower
{
private readonly HashSet<ulong> _playerIds = new();
protected BasePlugin? Plugin { get; private set; }
public abstract int Id { get; }
public abstract string Name { get; }
public abstract string Description { get; }
public abstract char ChatColor { get; }
public abstract string HtmlColor { get; }
public virtual bool CanBeRandomlyAssigned => true;
public virtual int MinimumPlayersRequired => 0;
public void Initialize(BasePlugin pluginRef)
{
Plugin = pluginRef;
OnInitialize();
}
protected abstract void OnInitialize();
public virtual void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
_playerIds.Add(player.SteamID);
if (showMessages)
{
player.PrintToChat($"You have been granted the power of {ChatColor}{Name}");
player.PrintToChat(Description);
var centerMessageBuilder = new StringBuilder();
centerMessageBuilder.Append("<font class='fontSize-l' class='fontWeight-Bold' color='#FFFFFF'>Your superpower:</font> <br>");
centerMessageBuilder.Append($"<font class='fontSize-l' class='fontWeight-Bold' color='{HtmlColor}'>{Name}</font> <br>");
centerMessageBuilder.Append($"<font class='fontSize-s' class='fontWeight-Normal' color='#FFFFFF'>{Description}</font> <br>");
playerHud.ShowMessage(player, centerMessageBuilder.ToString(), TimeSpan.FromSeconds(10));
}
}
public virtual void UnassignFromPlayer(CCSPlayerController player)
{
_playerIds.Remove(player.SteamID);
}
private readonly List<Delegate> _registeredDelegates = new();
protected void RegisterEventHandler<T>(BasePlugin.GameEventHandler<T> handler, HookMode hookMode = HookMode.Post)
where T : GameEvent
{
_registeredDelegates.Add(handler);
Plugin!.RegisterEventHandler(handler, hookMode);
}
protected void RegisterListener<T>(T handler) where T : Delegate
{
_registeredDelegates.Add(handler);
Plugin!.RegisterListener(handler);
}
protected void AddTimer(float seconds, Action callback, bool repeat = false)
{
if (repeat)
{
_registeredDelegates.Add(callback);
Plugin!.AddTimer(seconds, callback);
}
else
{
Action? wrappedCallback = null;
wrappedCallback = () =>
{
callback();
if (wrappedCallback != null)
{
_registeredDelegates.Remove(wrappedCallback);
}
};
_registeredDelegates.Add(wrappedCallback);
Plugin!.AddTimer(seconds, wrappedCallback);
}
}
protected void NextFrame(Action callback)
{
Action? wrappedCallback = null;
wrappedCallback = () =>
{
callback();
if (wrappedCallback != null)
{
_registeredDelegates.Remove(wrappedCallback);
}
};
_registeredDelegates.Add(wrappedCallback);
Server.NextFrame(wrappedCallback);
}
protected bool IsAssignedTo(CCSPlayerController player)
{
return _playerIds.Contains(player.SteamID);
}
}
+156
View File
@@ -0,0 +1,156 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
using Microsoft.Extensions.Logging;
namespace Cs2SuperPowers.Powers;
public class BlindingLight(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 22;
public override string Name => "Blinding Light";
public override string Description => $"Inflict a blinding flash of light that disorients enemies. Press E (Use) to activate the power.";
public override char ChatColor => ChatColors.BlueGrey;
public override string HtmlColor => "#cdcece";
private readonly List<BlindingLightInfo> _registrations = new();
private const int SecondsBetweenActivations = 8;
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
if (!_registrations.Any(r => r.Player == player))
{
_registrations.Add(new BlindingLightInfo(player, DateTime.MinValue));
}
Plugin!.Logger.LogInformation("BlindingLight assigned to {PlayerName} ({SteamID})", player.PlayerName, player.SteamID);
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
var registration = _registrations.FirstOrDefault(r => r.Player == player);
if (registration != null)
{
_registrations.Remove(registration);
}
Plugin!.Logger.LogInformation("BlindingLight unassigned from {PlayerName} ({SteamID})", player.PlayerName, player.SteamID);
}
protected override void OnInitialize()
{
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
Plugin!.Logger.LogInformation("BlindingLight initialized!");
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
var registration = _registrations
.FirstOrDefault(r => r.Player == player);
if (registration != null)
{
registration.WasUsePressedLastTick = false;
}
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
_registrations.Clear();
foreach (var player in PlayerPowers.Instance.GetPlayersWithPower(this))
{
_registrations.Add(new BlindingLightInfo(player, DateTime.MinValue));
}
Plugin!.Logger.LogInformation("BlindingLight: Round start, registered {Count} players", _registrations.Count);
return HookResult.Continue;
}
private void OnTick()
{
foreach (var registration in _registrations.ToList())
{
if (!Ensure.Player(registration.Player).IsValid().Check())
{
_registrations.Remove(registration);
continue;
}
var usePressed = registration.Player.Buttons.HasFlag(PlayerButtons.Use);
var canUsePower = (DateTime.UtcNow - registration.LastUse).TotalSeconds > SecondsBetweenActivations;
if (usePressed && !registration.WasUsePressedLastTick)
{
Plugin!.Logger.LogInformation("BlindingLight: {PlayerName} pressed use! CanUse: {CanUse}", registration.Player.PlayerName, canUsePower);
if (canUsePower)
{
ActivatePower(registration);
}
else
{
var secondsBeforeNextUse =
SecondsBetweenActivations - (int)(DateTime.UtcNow - registration.LastUse).TotalSeconds;
registration.Player
.PrintToCenterAlert($"You need to wait {ChatColors.DarkRed}{secondsBeforeNextUse} {ChatColors.Default}seconds before using the power.");
}
}
registration.WasUsePressedLastTick = usePressed;
}
}
private void ActivatePower(BlindingLightInfo registration)
{
// Check for at least 1 player in the opposing team
var playerTeam = registration.Player.Team;
var enemyCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator && p.Team != playerTeam);
Plugin!.Logger.LogInformation("BlindingLight: Activating for {PlayerName}, enemy count: {EnemyCount}", registration.Player.PlayerName, enemyCount);
if (enemyCount < 1)
{
return;
}
var flashProjectile = Utilities.CreateEntityByName<CFlashbangProjectile>("flashbang_projectile");
if (flashProjectile == null || !flashProjectile.IsValid)
{
Plugin!.Logger.LogError("BlindingLight: Failed to create flashbang projectile!");
return;
}
var player = registration.Player;
var pos = player.PlayerPawn.Value!.AbsOrigin;
pos!.Z += 10;
flashProjectile.TicksAtZeroVelocity = 100;
flashProjectile.TeamNum = player.TeamNum;
flashProjectile.Teleport(pos, null, new Vector(0, 0, -10));
flashProjectile.DispatchSpawn();
flashProjectile.AcceptInput("InitializeSpawnFromWorld", player.PlayerPawn.Value, player.PlayerPawn.Value);
flashProjectile.DetonateTime = 0;
Plugin!.Logger.LogInformation("BlindingLight: Flashbang created and detonated!");
registration.UpdateUse();
}
}
public record BlindingLightInfo(CCSPlayerController Player, DateTime LastUse)
{
public DateTime LastUse { get; private set; } = LastUse;
public bool WasUsePressedLastTick { get; set; } = false;
public void UpdateUse()
{
LastUse = DateTime.UtcNow;
}
}
+57
View File
@@ -0,0 +1,57 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class BulletStorm(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 13;
public override string Name => "Bullet Storm";
public override string Description => $"Gain effectively unlimited reserve ammo so you can keep reloading without ever running dry.";
public override char ChatColor => ChatColors.Olive;
public override string HtmlColor => "#0000FF";
protected override void OnInitialize()
{
RegisterEventHandler<EventWeaponFire>(OnWeaponFire);
RegisterEventHandler<EventWeaponReload>(OnWeaponReload);
}
private HookResult OnWeaponFire(EventWeaponFire @event, GameEventInfo info)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
RefillReserveAmmo(player!);
return HookResult.Continue;
}
private HookResult OnWeaponReload(EventWeaponReload @event, GameEventInfo info)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
RefillReserveAmmo(player!);
return HookResult.Continue;
}
private static void RefillReserveAmmo(CCSPlayerController player)
{
var activeWeaponHandle = player.PlayerPawn.Value?.WeaponServices?.ActiveWeapon;
if (activeWeaponHandle?.Value != null)
{
activeWeaponHandle.Value.ReserveAmmo[0] = 999;
}
}
}
+127
View File
@@ -0,0 +1,127 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class DoubleJump(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 28;
public override string Name => "Double Jump";
public override string Description => "Jump again in mid-air! A classic double jump ability.";
public override char ChatColor => ChatColors.Blue;
public override string HtmlColor => "#0000FF";
private readonly Dictionary<CCSPlayerController, bool> _canDoubleJump = new();
private readonly Dictionary<CCSPlayerController, bool> _wasOnGround = new();
private readonly Dictionary<CCSPlayerController, bool> _requireJumpRelease = new();
protected override void OnInitialize()
{
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_canDoubleJump.Clear();
_wasOnGround.Clear();
_requireJumpRelease.Clear();
return HookResult.Continue;
}
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
_canDoubleJump[player] = true;
_wasOnGround[player] = true;
_requireJumpRelease[player] = false;
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
_canDoubleJump.Remove(player);
_wasOnGround.Remove(player);
_requireJumpRelease.Remove(player);
}
private void OnTick()
{
foreach (var player in Utilities.GetPlayers())
{
if (!player.IsValid || !player.PawnIsAlive)
continue;
if (!IsAssignedTo(player))
continue;
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
continue;
var isOnGround = pawn.GroundEntity.IsValid;
var jumpPressed = player.Buttons.HasFlag(PlayerButtons.Jump);
var wasOnGroundLastTick = _wasOnGround.GetValueOrDefault(player, true);
if (isOnGround)
{
_canDoubleJump[player] = true;
_requireJumpRelease[player] = false;
}
else if (wasOnGroundLastTick)
{
_canDoubleJump[player] = true;
_requireJumpRelease[player] = jumpPressed;
}
if (!isOnGround && !jumpPressed)
{
_requireJumpRelease[player] = false;
}
if (!isOnGround &&
jumpPressed &&
!_requireJumpRelease.GetValueOrDefault(player, false) &&
_canDoubleJump.GetValueOrDefault(player, false))
{
var velocity = pawn.AbsVelocity;
velocity.Z = Math.Max(velocity.Z, 300.0f);
pawn.Teleport(pawn.AbsOrigin, null, velocity);
_canDoubleJump[player] = false;
_requireJumpRelease[player] = true;
}
_wasOnGround[player] = isOnGround;
}
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var victim = @event.Userid;
if (!Ensure.Player(victim).IsValid().Check() || !IsAssignedTo(victim!))
{
return HookResult.Continue;
}
bool isWorldDamage = !Ensure.Player(@event.Attacker).IsValid().Check();
bool hasNoWeapon = string.IsNullOrEmpty(@event.Weapon);
if (!isWorldDamage || !hasNoWeapon)
{
return HookResult.Continue;
}
var pawn = victim!.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
{
return HookResult.Continue;
}
pawn.Health = Math.Min(pawn.MaxHealth, pawn.Health + @event.DmgHealth);
return HookResult.Stop;
}
}
+110
View File
@@ -0,0 +1,110 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class DroppedNades(IPlayerHud playerHud) : BasePower(playerHud)
{
private readonly Dictionary<CCSPlayerController, DateTime> _lastUse = new();
public override int Id => 34;
public override string Name => "Dropped Nades";
public override string Description => "MAG-7 shots drop 5 grenades on the ground. This power is force-only.";
public override char ChatColor => ChatColors.DarkRed;
public override string HtmlColor => "#AA0000";
public override bool CanBeRandomlyAssigned => false;
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventWeaponFire>(OnWeaponFire);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
_lastUse.Clear();
return HookResult.Continue;
}
private HookResult OnWeaponFire(EventWeaponFire @event, GameEventInfo info)
{
var controller = @event.Userid;
if (!Ensure.Player(controller).IsValid().Check() || !IsAssignedTo(controller!))
{
return HookResult.Continue;
}
var pawn = controller!.PlayerPawn.Value;
var weapon = pawn?.WeaponServices?.ActiveWeapon.Value;
if (pawn == null || !pawn.IsValid || weapon == null || weapon.DesignerName != "weapon_mag7")
{
return HookResult.Continue;
}
var now = DateTime.UtcNow;
if (!GlobalConfig.NoDelayEnabled &&
_lastUse.TryGetValue(controller, out var lastUseTime) &&
(now - lastUseTime).TotalSeconds < 0.35)
{
return HookResult.Continue;
}
_lastUse[controller] = now;
var origin = pawn.AbsOrigin;
if (origin == null)
{
return HookResult.Continue;
}
int grenadeCount = GlobalConfig.GetDroppedNadesCount(5);
var random = new Random();
var viewAngle = pawn.V_angle;
float baseYawRad = viewAngle.Y * MathF.PI / 180f;
var forward = new Vector(MathF.Cos(baseYawRad), MathF.Sin(baseYawRad), 0.0f);
var right = new Vector(-forward.Y, forward.X, 0.0f);
int droppedGrenades = 0;
int attempts = 0;
int maxAttempts = grenadeCount + 8;
while (droppedGrenades < grenadeCount && attempts < maxAttempts)
{
int i = attempts;
attempts++;
float lateralOffset = (float)(random.NextDouble() * 18.0 - 9.0);
float forwardOffset = (float)(random.NextDouble() * 12.0 + 16.0);
var spawnPos = new Vector(
origin.X + forward.X * forwardOffset + right.X * lateralOffset,
origin.Y + forward.Y * forwardOffset + right.Y * lateralOffset,
origin.Z + 10.0f
);
float yaw = viewAngle.Y + (float)(random.NextDouble() * 16.0 - 8.0);
var angle = new QAngle(8.0f, yaw, 0.0f);
var velocity = new Vector(
forward.X * (float)(random.NextDouble() * 40.0 + 45.0) + right.X * (float)(random.NextDouble() * 16.0 - 8.0),
forward.Y * (float)(random.NextDouble() * 40.0 + 45.0) + right.Y * (float)(random.NextDouble() * 16.0 - 8.0),
(float)(random.NextDouble() * 12.0 + 8.0)
);
var grenadeWeapon = controller.GiveNamedItem<CHEGrenade>("weapon_hegrenade");
if (grenadeWeapon == null || !grenadeWeapon.IsValid)
{
continue;
}
if (grenadeWeapon.IsHeldByPlayer)
{
continue;
}
grenadeWeapon.Teleport(spawnPos, angle, velocity);
droppedGrenades++;
}
return HookResult.Continue;
}
}
+70
View File
@@ -0,0 +1,70 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Utils;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class ExplosiveBullets(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 30;
public override string Name => "Explosive Bullets";
public override string Description => "Every bullet hit causes a small explosion.";
public override char ChatColor => ChatColors.Orange;
public override string HtmlColor => "#FF4500";
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (!Ensure.Player(attacker).IsValid().Check() ||
!Ensure.Player(victim).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
if (string.IsNullOrWhiteSpace(@event.Weapon) || @event.Weapon.Contains("grenade", StringComparison.OrdinalIgnoreCase))
{
return HookResult.Continue;
}
var attackerPawn = attacker!.PlayerPawn.Value;
var victimPawn = victim!.PlayerPawn.Value;
if (victimPawn == null || attackerPawn == null)
return HookResult.Continue;
var pos = victimPawn.AbsOrigin;
if (pos == null)
return HookResult.Continue;
pos.Z += 20;
// Create explosion with a small grenade at victim's position
var ang = new QAngle(0, 0, 0);
var velocity = new Vector(0, 0, 0);
var grenade = GrenadeSpawner.CreateGrenade(pos, ang, velocity, attackerPawn);
// Detonate it immediately
Server.NextFrame(() =>
{
if (grenade != null && grenade.IsValid)
{
grenade.AcceptInput("InitializeSpawnFromWorld", attackerPawn, attackerPawn);
grenade.DetonateTime = 0;
}
});
return HookResult.Continue;
}
}
+93
View File
@@ -0,0 +1,93 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class FinalMission(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 8;
public override string Name => "Final Mission";
public override string Description => $"Upon death, unleash a powerful explosion that eliminates all players within a specified radius, turning your sacrifice into a final act of devastation.";
public override char ChatColor => ChatColors.Purple;
public override string HtmlColor => "#F5CB42";
public override int MinimumPlayersRequired => 4;
private const float ExplosionRadius = 500.0f;
private const int ExplosionDamage = 999;
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
var playerPawn = player!.PlayerPawn.Value;
var origin = playerPawn?.AbsOrigin;
if (playerPawn == null || origin == null)
{
return HookResult.Continue;
}
var explosionCenter = new Vector(origin.X, origin.Y, origin.Z);
Server.NextFrame(() =>
{
foreach (var target in Utilities.GetPlayers())
{
if (!Ensure.Player(target).IsValid().IsAlive().Check())
{
continue;
}
var targetPawn = target.PlayerPawn.Value;
var targetOrigin = targetPawn?.AbsOrigin;
if (targetPawn == null || targetOrigin == null)
{
continue;
}
var deltaX = targetOrigin.X - explosionCenter.X;
var deltaY = targetOrigin.Y - explosionCenter.Y;
var deltaZ = targetOrigin.Z - explosionCenter.Z;
var distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
if (distanceSquared > ExplosionRadius * ExplosionRadius)
{
continue;
}
targetPawn.Health = Math.Max(0, targetPawn.Health - ExplosionDamage);
Utilities.SetStateChanged(targetPawn, "CBaseEntity", "m_iHealth");
}
});
Server.PrintToChatAll($"{ChatColors.DarkRed}► {ChatColors.Green} {player.PlayerName}: {ChatColors.Lime}explodes!!!");
var fileNames = new[] { "radiobotfallback01", "radiobotfallback02", "radiobotfallback04" };
AddTimer(0.1f, () =>
{
var randomFile = fileNames[new Random().Next(fileNames.Length)];
player.ExecuteClientCommand($"play vo/agents/balkan/{randomFile}.vsnd");
});
return HookResult.Continue;
}
}
+65
View File
@@ -0,0 +1,65 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class FlashKing(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 17;
public override string Name => "Flash King";
public override string Description => $"With this superpower, your flashbangs blind enemies for three times as long.";
public override char ChatColor => ChatColors.LightBlue;
public override string HtmlColor => "#D6E6FF";
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerBlind>(OnPlayerBlinded);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().IsAlive().Check() || !IsAssignedTo(player!))
{
continue;
}
NextFrame(() =>
{
player.GiveNamedItem("weapon_flashbang");
player.GiveNamedItem("weapon_flashbang");
});
}
return HookResult.Continue;
}
private HookResult OnPlayerBlinded(EventPlayerBlind @event, GameEventInfo info)
{
var victim = @event.Userid;
var attacker = @event.Attacker;
if (!Ensure.Player(victim).IsValid().Check() || !IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
var playerPawn = victim!.PlayerPawn.Value;
if (attacker != victim && attacker!.TeamNum != playerPawn!.TeamNum)
{
playerPawn!.FlashDuration = playerPawn!.FlashDuration * 3;
}
return HookResult.Continue;
}
}
+75
View File
@@ -0,0 +1,75 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class FlashProof(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 3;
public override string Name => "Flash Proof";
public override string Description => $"You cannot be blind by flashes. You will gain new flash every time your flash explodes.";
public override char ChatColor => ChatColors.LightBlue;
public override string HtmlColor => "#D6E6FF";
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerBlind>(OnPlayerBlinded);
RegisterEventHandler<EventFlashbangDetonate>(OnFlashbangDetonate);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().IsAlive().Check() || !IsAssignedTo(player!))
{
continue;
}
NextFrame(() =>
{
player.GiveNamedItem("weapon_flashbang");
player.GiveNamedItem("weapon_flashbang");
});
}
return HookResult.Continue;
}
private HookResult OnPlayerBlinded(EventPlayerBlind @event, GameEventInfo info)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
var playerPawn = player!.PlayerPawn.Value;
playerPawn!.FlashDuration = 0.0f;
return HookResult.Continue;
}
private HookResult OnFlashbangDetonate(EventFlashbangDetonate @event, GameEventInfo info)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().IsAlive().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
player!.GiveNamedItem("weapon_flashbang");
return HookResult.Continue;
}
}
+192
View File
@@ -0,0 +1,192 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Ghost(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 7;
public override string Name => "The Ghost";
public override string Description => $"You are invisible to other players - but will show up for few seconds after you will take/give damage.";
public override char ChatColor => ChatColors.White;
private readonly Random _random = new Random();
public override string HtmlColor => "#FFFFFF";
public override int MinimumPlayersRequired => 4;
private readonly Dictionary<ulong, int> _visibilityTokens = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
RegisterEventHandler<EventRoundEnd>(OnRoundEnd);
}
private HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info)
{
foreach (var player in PlayerPowers.Instance.GetPlayersWithPower(this))
{
_visibilityTokens[player.SteamID] = _visibilityTokens.GetValueOrDefault(player.SteamID) + 1;
NextFrame(() =>
{
SetPlayerVisibility(player, true);
});
}
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
var validPlayersWithPower = PlayerPowers.Instance
.GetPlayersWithPower(this)
.Where(player => Ensure.Player(player).IsValid().Check());
foreach (var player in validPlayersWithPower)
{
NextFrame(() =>
{
SetPlayerVisibility(player, false);
});
}
return HookResult.Continue;
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
NextFrame(() =>
{
SetPlayerVisibility(player, true);
});
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (!Ensure.Player(victim).IsValid().IsAlive().Check() ||
!Ensure.Player(attacker).IsValid().Check())
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
if (IsAssignedTo(victim!))
{
RevealGhostTemporarily(victim!);
}
else if (IsAssignedTo(attacker!))
{
RevealGhostTemporarily(attacker!);
}
return HookResult.Continue;
}
private void RevealGhostTemporarily(CCSPlayerController player)
{
if (!Ensure.Player(player).IsValid().IsAlive().Check())
{
return;
}
var token = _visibilityTokens.GetValueOrDefault(player.SteamID) + 1;
_visibilityTokens[player.SteamID] = token;
SetPlayerVisibility(player, true);
AddTimer(_random.Next(1, 5), () =>
{
if (!IsAssignedTo(player) || _visibilityTokens.GetValueOrDefault(player.SteamID) != token)
{
return;
}
SetPlayerVisibility(player, false);
});
}
private void SetPlayerVisibility(CCSPlayerController player, bool visible)
{
if (!IsAssignedTo(player) && !visible)
{
return;
}
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null)
{
var color = visible
? Color.FromArgb(255, 255, 255, 255)
: Color.FromArgb(0, 255, 255, 255);
var shadowStrength = visible ? 1.0f : 0.0f;
playerPawn.Render = color;
playerPawn.ShadowStrength = shadowStrength;
Utilities.SetStateChanged(playerPawn, "CBaseModelEntity", "m_clrRender");
var activeWeapon = playerPawn.WeaponServices?.ActiveWeapon.Value;
if (activeWeapon != null && activeWeapon.IsValid)
{
activeWeapon.Render = color;
activeWeapon.ShadowStrength = shadowStrength;
Utilities.SetStateChanged(activeWeapon, "CBaseModelEntity", "m_clrRender");
}
var skeleton = playerPawn.CBodyComponent?.SceneNode?.GetSkeletonInstance();
if (skeleton != null)
{
skeleton.MaterialGroup.Value = visible ? (uint)0 : 1;
}
var myWeapons = playerPawn.WeaponServices?.MyWeapons;
if (myWeapons != null)
{
foreach (var gun in myWeapons)
{
var weapon = gun.Value;
if (weapon != null)
{
weapon.Render = color;
weapon.ShadowStrength = shadowStrength;
Utilities.SetStateChanged(weapon, "CBaseModelEntity", "m_clrRender");
}
}
}
var myWearables = playerPawn.MyWearables;
if (myWearables != null)
{
foreach (var wearableHandle in myWearables)
{
var wearable = wearableHandle.Value;
if (wearable == null || !wearable.IsValid)
{
continue;
}
wearable.Render = color;
wearable.ShadowStrength = shadowStrength;
Utilities.SetStateChanged(wearable, "CBaseModelEntity", "m_clrRender");
}
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class GoFast(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 19;
public override string Name => "Go Fast";
public override string Description => $"Your movement speed is increased by 50%";
public override char ChatColor => ChatColors.Green;
public override string HtmlColor => "#009905";
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
foreach (var player in PlayerPowers.Instance.GetPlayersWithPower(this))
{
if (!Ensure.Player(player).IsValid().Check())
{
continue;
}
NextFrame(() =>
{
player.SetNewSpeed(player.GetSpeed() * 1.5f);
player.GravityScale += 5;
});
}
return HookResult.Continue;
}
}
+121
View File
@@ -0,0 +1,121 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
using Cs2SuperPowers.Utils;
namespace Cs2SuperPowers.Powers;
public class GrenadeLauncher(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 11;
public override string Name => "Grenade Launcher";
public override string Description => "Every shot from your pistol launches a grenade (0.5s cooldown).";
public override char ChatColor => ChatColors.Red;
public override string HtmlColor => "#FF0000";
private readonly Dictionary<CCSPlayerController, DateTime> _lastUse = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventWeaponFire>(OnWeaponFire);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_lastUse.Clear();
return HookResult.Continue;
}
private HookResult OnWeaponFire(EventWeaponFire @event, GameEventInfo info)
{
var controller = @event.Userid;
if (!Ensure.Player(controller).IsValid().Check() || !IsAssignedTo(controller!))
return HookResult.Continue;
// Only pistols
var weapon = controller!.PlayerPawn.Value?.WeaponServices?.ActiveWeapon.Value;
if (weapon == null)
return HookResult.Continue;
// List of pistol weapon names (classnames)
var pistolClassNames = new[]
{
"weapon_glock", "weapon_usp_silencer", "weapon_hkp2000", "weapon_p250",
"weapon_fiveseven", "weapon_tec9", "weapon_cz75a", "weapon_deagle",
"weapon_revolver", "weapon_elite"
};
if (!pistolClassNames.Contains(weapon.DesignerName))
return HookResult.Continue;
// Check cooldown (skip if nodelay is enabled)
var now = DateTime.UtcNow;
if (!GlobalConfig.NoDelayEnabled && _lastUse.TryGetValue(controller!, out var lastUseTime) && (now - lastUseTime).TotalSeconds < 0.5)
return HookResult.Continue;
_lastUse[controller!] = now;
var pawn = controller!.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
return HookResult.Continue;
var pos = pawn.AbsOrigin;
if (pos == null) return HookResult.Continue;
pos = new Vector(pos.X, pos.Y, pos.Z + 64f);
// Forward vector from the player's view angles
var ang = pawn.V_angle;
float yaw = ang.Y * MathF.PI / 180f;
float pitch = ang.X * MathF.PI / 180f;
var forward = new Vector(
MathF.Cos(yaw) * MathF.Cos(pitch),
MathF.Sin(yaw) * MathF.Cos(pitch),
-MathF.Sin(pitch)
);
// Spawn farther in front of the player so the grenade does not collide with the shooter.
const float spawnAhead = 72f;
pos = new Vector(
pos.X + forward.X * spawnAhead,
pos.Y + forward.Y * spawnAhead,
pos.Z + forward.Z * 24f
);
const float speed = 1100f;
int grenadeCount = GlobalConfig.GetGrenadeLauncherCount(1);
for (int i = 0; i < grenadeCount; i++)
{
// Add slight random spread for multiple grenades
var random = new Random();
var spreadAng = new QAngle(
ang.X + (float)(random.NextDouble() * 10.0 - 5.0), // ±5° pitch
ang.Y + (float)(random.NextDouble() * 10.0 - 5.0), // ±5° yaw
ang.Z
);
float spreadYaw = spreadAng.Y * MathF.PI / 180f;
float spreadPitch = spreadAng.X * MathF.PI / 180f;
var spreadForward = new Vector(
MathF.Cos(spreadYaw) * MathF.Cos(spreadPitch),
MathF.Sin(spreadYaw) * MathF.Cos(spreadPitch),
-MathF.Sin(spreadPitch)
);
var velocity = new Vector(
spreadForward.X * speed,
spreadForward.Y * speed,
spreadForward.Z * speed + 100f
);
GrenadeSpawner.CreateGrenade(pos, spreadAng, velocity, pawn);
}
return HookResult.Continue;
}
}
+69
View File
@@ -0,0 +1,69 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class IAmChicken(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 10;
public override string Name => "I Am Chicken!";
public override string Description => $"You become Chicken! You're harder to hit but have 30% of HP.";
public override char ChatColor => ChatColors.Orange;
public override string HtmlColor => "#FF8B42";
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
var random = new Random();
foreach (var player in PlayerPowers.Instance.GetPlayersWithPower(this))
{
if (!Ensure.Player(player).IsValid().Check())
{
continue;
}
var playerPawn = player.PlayerPawn?.Value;
if (playerPawn == null)
{
continue;
}
NextFrame(() =>
{
SetPlayerModel(player, "models/chicken/chicken.vmdl");
playerPawn!.CBodyComponent!.SceneNode!.GetSkeletonInstance().MaterialGroup.Value =
(uint)random.Next(1, 4);
playerPawn.MaxHealth = 30;
playerPawn.Health = 30;
Utilities.SetStateChanged(player, "CCSPlayerPawn", "m_iMaxHealth");
});
}
return HookResult.Continue;
}
private void SetPlayerModel(CCSPlayerController player, string model)
{
var pawn = player.PlayerPawn?.Value;
if (pawn == null) return;
NextFrame(() =>
{
pawn.SetModel(model);
var originalRender = pawn.Render;
pawn.Render = Color.FromArgb(255, originalRender.R, originalRender.G, originalRender.B);
});
}
}
+51
View File
@@ -0,0 +1,51 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class IronBody(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 18;
public override string Name => "Iron Body";
public override string Description => $"You can only be killed by headshots.";
public override char ChatColor => ChatColors.LightPurple;
public override string HtmlColor => "#8B4513";
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (@event.Hitgroup == (int)HitGroup_t.HITGROUP_HEAD)
{
return HookResult.Continue;
}
if (
!Ensure.Player(victim).IsValid().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(victim!))
{
return HookResult.Continue;
}
var playerPawn = victim!.PlayerPawn.Value;
var newHealth = playerPawn!.Health + @event.DmgHealth;
if (newHealth > 100)
newHealth = 100;
playerPawn.Health = newHealth;
return HookResult.Stop;
}
}
+52
View File
@@ -0,0 +1,52 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class IronHead(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 4;
public override string Name => "Iron Head";
public override string Description => $"You cannot be killed by head shots.";
public override char ChatColor => ChatColors.LightPurple;
public override string HtmlColor => "#8B4513";
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (@event.Hitgroup != (int)HitGroup_t.HITGROUP_HEAD)
{
return HookResult.Continue;
}
if (
!Ensure.Player(victim).IsValid().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(victim!))
{
return HookResult.Continue;
}
var playerPawn = victim!.PlayerPawn.Value;
var newHealth = playerPawn!.Health + @event.DmgHealth;
if (newHealth > 100)
newHealth = 100;
playerPawn.Health = newHealth;
return HookResult.Stop;
}
}
+106
View File
@@ -0,0 +1,106 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
namespace Cs2SuperPowers.Powers;
public class Jetpack(IPlayerHud playerHud) : BasePower(playerHud)
{
private const float MaxFuel = 180.0f;
private const float FuelBurnPerTick = 0.8f;
private const float FuelRechargePerTick = 0.9f;
private const float LiftPerTick = 14.0f;
private const float MaxVerticalSpeed = 260.0f;
private static readonly TimeSpan HudRefreshInterval = TimeSpan.FromMilliseconds(125);
public override int Id => 33;
public override string Name => "Jetpack";
public override string Description => "Hold Jump (Space) to fly with a fuel-based jetpack.";
public override char ChatColor => ChatColors.Orange;
public override string HtmlColor => "#FF8C00";
private readonly Dictionary<CCSPlayerController, float> _fuelByPlayer = new();
private readonly Dictionary<CCSPlayerController, DateTime> _lastHudUpdate = new();
protected override void OnInitialize()
{
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
_fuelByPlayer.Clear();
_lastHudUpdate.Clear();
return HookResult.Continue;
}
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
_fuelByPlayer[player] = MaxFuel;
_lastHudUpdate.Remove(player);
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
_fuelByPlayer.Remove(player);
_lastHudUpdate.Remove(player);
}
private void OnTick()
{
foreach (var player in Utilities.GetPlayers())
{
if (!player.IsValid || !player.PawnIsAlive)
continue;
if (!IsAssignedTo(player))
continue;
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
continue;
float fuel = _fuelByPlayer.GetValueOrDefault(player, MaxFuel);
bool isJumpPressed = player.Buttons.HasFlag(PlayerButtons.Jump);
if (isJumpPressed && fuel > 0.0f)
{
var currentVelocity = pawn.AbsVelocity;
currentVelocity.Z = Math.Min(currentVelocity.Z + LiftPerTick, MaxVerticalSpeed);
pawn.Teleport(pawn.AbsOrigin, null, currentVelocity);
fuel = Math.Max(0.0f, fuel - FuelBurnPerTick);
}
else
{
fuel = Math.Min(MaxFuel, fuel + FuelRechargePerTick);
}
_fuelByPlayer[player] = fuel;
if (isJumpPressed)
{
var now = DateTime.UtcNow;
if (!_lastHudUpdate.TryGetValue(player, out var lastHudUpdate) ||
now - lastHudUpdate >= HudRefreshInterval)
{
_lastHudUpdate[player] = now;
player.PrintToCenterHtml(BuildFuelMessage(fuel));
}
}
}
}
private static string BuildFuelMessage(float fuel)
{
int percent = (int)MathF.Round((fuel / MaxFuel) * 100.0f);
int filledBars = (int)MathF.Round((fuel / MaxFuel) * 10.0f);
filledBars = Math.Clamp(filledBars, 0, 10);
string bar = new string('|', filledBars).PadRight(10, '.');
string color = percent > 60 ? "#4CAF50" : percent > 30 ? "#FFC107" : "#F44336";
return $"<font color='#FFFFFF'>Jetpack Fuel:</font> <font color='{color}'>{percent}%</font><br><font color='{color}'>{bar}</font>";
}
}
+110
View File
@@ -0,0 +1,110 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
using Cs2SuperPowers.Utils;
namespace Cs2SuperPowers.Powers;
public class NotSoDecoy(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 24;
public override string Name => "Not So Decoy";
public override string Description => "Decoys thrown by you spawn 20 grenades when thrown (5 second cooldown).";
public override char ChatColor => ChatColors.Red;
public override string HtmlColor => "#FF0000";
private readonly Dictionary<CCSPlayerController, DateTime> _lastUse = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventDecoyStarted>(OnDecoyStarted);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_lastUse.Clear();
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().IsAlive().Check() || !IsAssignedTo(player!))
{
continue;
}
Server.NextFrame(() =>
{
player.GiveNamedItem("weapon_decoy");
});
}
return HookResult.Continue;
}
private HookResult OnDecoyStarted(EventDecoyStarted @event, GameEventInfo _)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
// Check cooldown (skip if nodelay is enabled)
var now = DateTime.UtcNow;
DateTime lastUseTime = default;
bool isOnCooldown = !GlobalConfig.NoDelayEnabled && _lastUse.TryGetValue(player!, out lastUseTime) && (now - lastUseTime).TotalSeconds < 5;
// Always give a new decoy, regardless of cooldown!
player!.GiveNamedItem("weapon_decoy");
if (isOnCooldown)
{
double secondsRemaining = Math.Ceiling(5 - (now - lastUseTime).TotalSeconds);
player.PrintToCenterAlert($"Not So Decoy is on cooldown! {ChatColors.DarkRed}{secondsRemaining}{ChatColors.Default}s remaining");
return HookResult.Continue;
}
_lastUse[player!] = now;
var pos = new Vector(@event.X, @event.Y, @event.Z);
// Spawn grenades (with boost level) in a loop with random angles and velocities
var random = new Random();
int grenadeCount = GlobalConfig.GetNotSoDecoyCount(20);
for (int i = 0; i < grenadeCount; i++)
{
// Random pitch between -45 and 45 degrees, yaw between 0 and 360
float pitch = (float)(random.NextDouble() * 90.0 - 45.0);
float yaw = (float)(random.NextDouble() * 360.0);
var ang = new QAngle(pitch, yaw, 0);
// Random speed between 400 and 900 units
float speed = (float)(random.NextDouble() * 500.0 + 400.0);
// Convert angles to a forward vector
float pitchRad = pitch * MathF.PI / 180f;
float yawRad = yaw * MathF.PI / 180f;
var forward = new Vector(
MathF.Cos(yawRad) * MathF.Cos(pitchRad),
MathF.Sin(yawRad) * MathF.Cos(pitchRad),
-MathF.Sin(pitchRad));
// Add some random upward velocity for the "jump"
float upward = (float)(random.NextDouble() * 200.0 + 200.0);
var velocity = new Vector(
forward.X * speed,
forward.Y * speed,
forward.Z * speed + upward);
var createdGrenade = GrenadeSpawner.CreateGrenade(pos, ang, velocity, player.PlayerPawn.Value);
}
return HookResult.Continue;
}
}
+66
View File
@@ -0,0 +1,66 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class OneShot(IPlayerHud playerHud) : BasePower(playerHud)
{
private const double InstantKillChance = 0.25;
private readonly Random _random = new();
public override int Id => 23;
public override string Name => "One Shot";
public override string Description => $"Each damaging hit has a {(int)(InstantKillChance * 100)}% chance to instantly eliminate the enemy.";
public override char ChatColor => ChatColors.Magenta;
public override string HtmlColor => "#ff5CD9";
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (!Ensure.Player(attacker).IsValid().IsAlive().Check() ||
!Ensure.Player(victim).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 1 player in the opposing team
var attackerTeam = attacker!.Team;
var enemyCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator && p.Team != attackerTeam);
if (enemyCount < 1)
{
return HookResult.Continue;
}
var victimPawn = victim!.PlayerPawn.Value;
if (victimPawn == null)
{
return HookResult.Continue;
}
if (_random.NextDouble() > InstantKillChance)
{
return HookResult.Continue;
}
victimPawn.Health = 0;
Utilities.SetStateChanged(victimPawn, "CBaseEntity", "m_iHealth");
return HookResult.Continue;
}
}
+123
View File
@@ -0,0 +1,123 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class PanicTeleport(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 16;
public override string Name => "Panic Teleport";
public override string Description => $"Save a single location with the use key, then teleport back to that position whenever you press the use key again.";
public override char ChatColor => ChatColors.BlueGrey;
public override string HtmlColor => "#cdcece";
private readonly List<PanicTeleportInfo> _registrations = new();
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
if (_registrations.All(registration => registration.Player != player))
{
_registrations.Add(new PanicTeleportInfo(player, null));
}
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
var registration = _registrations.FirstOrDefault(r => r.Player == player);
if (registration != null)
{
_registrations.Remove(registration);
}
}
protected override void OnInitialize()
{
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
var registration = _registrations
.FirstOrDefault(r => r.Player == player);
if (registration != null)
{
registration.SavedLocation = null;
registration.WasUsePressedLastTick = false;
}
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
_registrations.Clear();
foreach (var player in PlayerPowers.Instance.GetPlayersWithPower(this))
{
_registrations.Add(new PanicTeleportInfo(player, null));
}
return HookResult.Continue;
}
private void OnTick()
{
foreach (var registration in _registrations)
{
var usePressed = registration.Player.Buttons.HasFlag(PlayerButtons.Use);
if (usePressed && !registration.WasUsePressedLastTick)
{
// Key was just pressed (not held)
HandleUseKeyPress(registration);
}
registration.WasUsePressedLastTick = usePressed;
}
}
private void HandleUseKeyPress(PanicTeleportInfo registration)
{
var player = registration.Player;
if (!Ensure.Player(player).IsValid().Check() || player.PlayerPawn?.Value == null)
{
return;
}
if (registration.SavedLocation == null)
{
// Save current location
var currentPos = player.PlayerPawn.Value.AbsOrigin;
if (currentPos != null)
{
registration.SavedLocation = new Vector(currentPos.X, currentPos.Y, currentPos.Z);
registration.SavedLocation.Z += 10;
player.PrintToCenterAlert($"{ChatColor}Location saved! Press use key again to teleport back.");
}
}
else
{
// Teleport to saved location
var savedPos = registration.SavedLocation;
player.PlayerPawn.Value.Teleport(savedPos, null, null);
registration.SavedLocation = null;
player.PrintToCenterAlert($"{ChatColor}Teleported. Press use to save a new location.");
}
}
}
public record PanicTeleportInfo(CCSPlayerController Player, Vector? SavedLocation)
{
public Vector? SavedLocation { get; set; } = SavedLocation;
public bool WasUsePressedLastTick { get; set; } = false;
}
+81
View File
@@ -0,0 +1,81 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Phoenix(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 2;
public override string Name => "Phoenix Rising";
public override string Description => $"You will be reborn after your first death like Phoenix from the ashes.";
public override char ChatColor => ChatColors.DarkRed;
public override string HtmlColor => "#ff5C0A";
public override int MinimumPlayersRequired => 4;
private HashSet<CCSPlayerController> _phoenixUses = new HashSet<CCSPlayerController>();
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo _)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
if (_phoenixUses.Contains(player!))
{
return HookResult.Continue;
}
Server.NextFrame(() =>
{
var newHealth = 120;
player!.SetNewHealth(newHealth);
player!.Respawn();
_phoenixUses.Add(player);
player.PrintToChat($"{ChatColors.DarkRed}Phoenix {ChatColors.Green}rises.");
});
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_phoenixUses = new HashSet<CCSPlayerController>();
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
Server.NextFrame(() =>
{
player.SetNewHealth(50);
});
}
return HookResult.Continue;
}
}
+66
View File
@@ -0,0 +1,66 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class PowerThief(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 15;
public override string Name => "Power Thief";
public override string Description => $"With a 25% chance on hit, you can permanently strip your enemies of their current superpower.";
public override char ChatColor => ChatColors.LightYellow;
public override string HtmlColor => "#f8e1ab";
private readonly Random _random = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (
!Ensure.Player(victim).IsValid().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 1 player in the opposing team
var attackerTeam = attacker!.Team;
var enemyCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator && p.Team != attackerTeam);
if (enemyCount < 1)
{
return HookResult.Continue;
}
if (_random.NextDouble() <= 0.25)
{
StealSuperpower(victim!, attacker!);
}
return HookResult.Continue;
}
private void StealSuperpower(CCSPlayerController victim, CCSPlayerController attacker)
{
if (!PlayerPowers.Instance.TryGetValue(victim, out var power)) return;
PlayerPowers.Instance.RemovePower(victim);
victim.SetMaxHealth(100);
attacker.PrintToChat($"You have stolen {ChatColors.Red}{victim.PlayerName} {ChatColors.Default}superpower {power!.ChatColor}{power.Name}.");
victim.PrintToChat($"Your superpower {power.ChatColor}{power.Name} {ChatColors.Red}was stolen! You're a regular guy/gal now.");
}
}
+83
View File
@@ -0,0 +1,83 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Rambo(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 5;
public override string Name => "John Rambo";
public override string Description => $"You start with additional random health, negev, kevlar and hand grenades. Enemy team gets 5000 cash.";
public override char ChatColor => ChatColors.Green;
public override string HtmlColor => "#009905";
public override int MinimumPlayersRequired => 4;
protected override void OnInitialize()
{
RegisterEventHandler<EventRoundStart>(OnRoundStart);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
var random = new Random();
var ramboPlayers = PlayerPowers.Instance.GetPlayersWithPower(this).ToList();
var validPlayerCount = Utilities.GetPlayers().Count(player =>
player.IsValid && player.Team != CsTeam.None && player.Team != CsTeam.Spectator);
if (validPlayerCount < MinimumPlayersRequired)
{
return HookResult.Continue;
}
// If there are any Rambo players, give enemy team 5000 cash
if (ramboPlayers.Count > 0)
{
var ramboTeam = ramboPlayers.First().Team;
foreach (var player in Utilities.GetPlayers())
{
if (!Ensure.Player(player).IsValid().Check())
{
continue;
}
if (player.Team != ramboTeam && player.Team != CsTeam.None && player.Team != CsTeam.Spectator)
{
Server.NextFrame(() =>
{
if (player.InGameMoneyServices != null)
{
player.InGameMoneyServices.Account += 5000;
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
}
});
}
}
}
foreach (var player in ramboPlayers)
{
if (!Ensure.Player(player).IsValid().Check())
{
continue;
}
Server.NextFrame(() =>
{
var newHealth = 100 + random.Next(150, 400);
player.GiveNamedItem("weapon_negev");
player.GiveNamedItem("item_assaultsuit");
player.GiveNamedItem("weapon_hegrenade");
player.SetNewHealth(newHealth);
});
}
return HookResult.Continue;
}
}
+70
View File
@@ -0,0 +1,70 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class RandomTeleport(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 29;
public override string Name => "Random Teleport";
public override string Description => "Teleport to a random position on the map every time you get a kill!";
public override char ChatColor => ChatColors.Purple;
public override string HtmlColor => "#800080";
private readonly Random _random = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var attacker = @event.Attacker;
if (!Ensure.Player(attacker).IsValid().IsAlive().Check() || !IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
var pawn = attacker!.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
return HookResult.Continue;
// Find random valid player to teleport near
var validPlayers = Utilities.GetPlayers()
.Where(p => p.IsValid && p.PawnIsAlive && p != attacker)
.ToList();
if (validPlayers.Count > 0)
{
// Teleport near a random player
var targetPlayer = validPlayers[_random.Next(validPlayers.Count)];
var targetPos = targetPlayer.PlayerPawn.Value!.AbsOrigin!;
// Random offset of 100-300 units
var offset = new Vector(
(float)(_random.NextDouble() * 400 - 200),
(float)(_random.NextDouble() * 400 - 200),
50
);
pawn.Teleport(targetPos + offset, null, null);
}
else
{
// If no other players, teleport in a random direction from current position
var currentPos = pawn.AbsOrigin!;
var offset = new Vector(
(float)(_random.NextDouble() * 600 - 300),
(float)(_random.NextDouble() * 600 - 300),
100
);
pawn.Teleport(currentPos + offset, null, null);
}
return HookResult.Continue;
}
}
+59
View File
@@ -0,0 +1,59 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class RiseFromTheDead(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 1;
public override string Name => "Rise From the Dead";
public override string Description => $"You have {(int)(ReviveChance * 100)}% chance to rise from the dead.";
public override char ChatColor => ChatColors.Red;
public override string HtmlColor => "#ff5C0A";
public override int MinimumPlayersRequired => 4;
private const double ReviveChance = 0.35;
private readonly Random _random = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo _)
{
var player = @event.Userid;
if (!Ensure.Player(player).IsValid().Check() || !IsAssignedTo(player!))
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
if (_random.NextDouble() <= ReviveChance)
{
Server.NextFrame(() =>
{
player!.MaxHealth = 70;
player.Health = 70;
player.Respawn();
player.PrintToChat($"You have been raised from {ChatColors.DarkRed}the dead.");
});
}
return HookResult.Continue;
}
}
+114
View File
@@ -0,0 +1,114 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Shield(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 32;
public override string Name => "Shield";
public override string Description => "Hold E (Use) to activate a shield that blocks all damage! Shield has 100 energy points (regenerates 10 per second when not in use).";
public override char ChatColor => ChatColors.Yellow;
public override string HtmlColor => "#FFFF00";
private readonly Dictionary<CCSPlayerController, float> _shieldEnergy = new();
private readonly Dictionary<CCSPlayerController, bool> _shieldActive = new();
protected override void OnInitialize()
{
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo _)
{
_shieldEnergy.Clear();
_shieldActive.Clear();
return HookResult.Continue;
}
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
_shieldEnergy[player] = 100.0f;
_shieldActive[player] = false;
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
_shieldEnergy.Remove(player);
_shieldActive.Remove(player);
}
private void OnTick()
{
foreach (var player in Utilities.GetPlayers())
{
if (!player.IsValid || !player.PawnIsAlive)
continue;
if (!IsAssignedTo(player))
continue;
if (!_shieldEnergy.ContainsKey(player))
continue;
var usePressed = player.Buttons.HasFlag(PlayerButtons.Use);
if (usePressed && _shieldEnergy[player] > 0)
{
// Shield is active!
_shieldActive[player] = true;
_shieldEnergy[player] -= 0.5f; // Consume 0.5 energy per tick
// Update HUD (center alert) every second
if (Math.Abs(_shieldEnergy[player] % 20) < 1)
{
player.PrintToCenterAlert($"{ChatColor}Shield: {Math.Ceiling(_shieldEnergy[player])}%");
}
}
else
{
// Shield not active, regenerate
_shieldActive[player] = false;
if (_shieldEnergy[player] < 100.0f)
{
_shieldEnergy[player] += 0.3f; // Regenerate 0.3 per tick
if (_shieldEnergy[player] > 100.0f)
_shieldEnergy[player] = 100.0f;
}
}
}
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var victim = @event.Userid;
if (!Ensure.Player(victim).IsValid().Check() || !IsAssignedTo(victim!))
{
return HookResult.Continue;
}
var validatedVictim = victim!;
if (_shieldActive.GetValueOrDefault(validatedVictim, false))
{
var pawn = validatedVictim.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid)
{
return HookResult.Continue;
}
pawn.Health = Math.Min(pawn.MaxHealth, pawn.Health + @event.DmgHealth);
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
return HookResult.Handled;
}
return HookResult.Continue;
}
}
+61
View File
@@ -0,0 +1,61 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class TeleportOnKill(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 21;
public override string Name => "Teleport On Kill";
public override string Description => $"When you kill an enemy, you teleport to their location (needs at least 4 players)";
public override char ChatColor => ChatColors.Red;
public override string HtmlColor => "#FF0000";
public override int MinimumPlayersRequired => 4;
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
}
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (
!Ensure.Player(victim).IsValid().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
if (victim!.PlayerPawn?.Value != null && attacker!.PlayerPawn?.Value != null)
{
var victimPos = victim.PlayerPawn.Value.AbsOrigin;
// raise Z a bit to avoid getting stuck in the ground
if (victimPos != null)
{
victimPos.Z += 10;
attacker.PlayerPawn.Value.Teleport(victimPos, null, null);
}
}
return HookResult.Continue;
}
}
+69
View File
@@ -0,0 +1,69 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Twister(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 14;
public override string Name => "Twister";
public override string Description => $"Inflict disorienting spins upon your enemies with a 25% chance on each hit, disrupting their aim and movement.";
public override char ChatColor => ChatColors.Silver;
public override string HtmlColor => "#00FF00";
private readonly Random _random = new();
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (
!Ensure.Player(victim).IsValid().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 1 player in the opposing team
var attackerTeam = attacker!.Team;
var enemyCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator && p.Team != attackerTeam);
if (enemyCount < 1)
{
return HookResult.Continue;
}
if (_random.NextDouble() <= 0.25)
{
RotateEnemy(victim!);
}
return HookResult.Continue;
}
private void RotateEnemy(CCSPlayerController player)
{
var currentPosition = player.PlayerPawn.Value!.AbsOrigin;
var currentAngles = player.PlayerPawn.Value.V_angle;
var newAngles = new QAngle(
currentAngles.X,
currentAngles.Y + _random.Next(-180, 180),
currentAngles.Z
);
player.PlayerPawn.Value.Teleport(currentPosition, newAngles, new Vector(0, 0, 0));
}
}
+63
View File
@@ -0,0 +1,63 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
namespace Cs2SuperPowers.Powers;
public class Vampire(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 12;
public override string Name => "Vampire";
public override string Description => $"Drain the life force of your enemies, healing yourself for {(int)(HealModifier * 100)}% of the damage dealt with each shot.";
public override char ChatColor => ChatColors.Magenta;
public override string HtmlColor => "#ff5CD9";
public override int MinimumPlayersRequired => 4;
private const double HealModifier = 0.7;
protected override void OnInitialize()
{
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var attacker = @event.Attacker;
var victim = @event.Userid;
if (
!Ensure.Player(victim).IsValid().IsAlive().Check() ||
!Ensure.Player(attacker).IsValid().Check() ||
attacker == victim ||
!IsAssignedTo(attacker!))
{
return HookResult.Continue;
}
// Check for at least 4 valid (non-spectator, non-none) players total
var validPlayerCount = Utilities.GetPlayers().Count(p =>
p.IsValid && p.Team != CsTeam.None && p.Team != CsTeam.Spectator);
if (validPlayerCount < 4)
{
return HookResult.Continue;
}
var attackerPawn = attacker!.PlayerPawn.Value;
if (attackerPawn == null)
{
return HookResult.Continue;
}
var healing = (int)(@event.DmgHealth * HealModifier);
var newHealth = Math.Min(attackerPawn.Health + healing, attackerPawn.MaxHealth);
attackerPawn.Health = newHealth;
Utilities.SetStateChanged(attackerPawn, "CBaseEntity", "m_iHealth");
return HookResult.Continue;
}
}
+291
View File
@@ -0,0 +1,291 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Cs2SuperPowers.Players;
using Cs2SuperPowers.Players.PlayerValidations;
using Microsoft.Extensions.Logging;
namespace Cs2SuperPowers.Powers;
// GlowData class
public class GlowData
{
public CDynamicProp GlowEnt { get; set; } = null!;
public CDynamicProp ModelRelay { get; set; } = null!;
}
public class Wallhack(IPlayerHud playerHud) : BasePower(playerHud)
{
public override int Id => 26;
public override string Name => "Wallhack";
public override string Description => "See enemies through walls with a glow!";
public override char ChatColor => ChatColors.Purple;
public override string HtmlColor => "#AB4BD1";
// Static storage
public static List<CCSPlayerController> Wallhackers = [];
public static Dictionary<CCSPlayerController, GlowData> GlowData = [];
private static BasePlugin? _plugin; // Static reference to plugin!
private static readonly List<Delegate> _staticRegisteredDelegates = []; // Store delegates to prevent GC!
// Static references to event handler delegates to prevent GC!
private static readonly BasePlugin.GameEventHandler<EventPlayerDeath> _onPlayerDeathHandler = OnPlayerDeath;
private static readonly BasePlugin.GameEventHandler<EventPlayerDisconnect> _onPlayerDisconnectHandler = OnPlayerDisconnect;
private static readonly BasePlugin.GameEventHandler<EventPlayerSpawn> _onPlayerSpawnHandler = OnPlayerSpawn;
private static readonly BasePlugin.GameEventHandler<EventPlayerTeam> _onPlayerChangeTeamHandler = OnPlayerChangeTeam;
// Also store a static reference to OnPlayerTransmit!
public static readonly Action<CCheckTransmitInfo, CCSPlayerController> _onPlayerTransmitHandler = OnPlayerTransmit;
public override void AssignToPlayer(CCSPlayerController player, bool showMessages)
{
base.AssignToPlayer(player, showMessages);
AddWallhacker(player);
_plugin?.Logger.LogInformation("Wallhack assigned to {PlayerName}", player.PlayerName);
// Ensure all alive players have glows (delay slightly just in case!)
NextFrame(() =>
{
foreach (var existingPlayer in Utilities.GetPlayers())
{
if (IsPlayerValid(existingPlayer) && existingPlayer.PawnIsAlive && existingPlayer.Team >= CsTeam.Terrorist)
{
if (!GlowData.ContainsKey(existingPlayer))
{
_plugin?.Logger.LogInformation("Creating glow for {PlayerName}", existingPlayer.PlayerName);
Glow(existingPlayer);
}
}
}
_plugin?.Logger.LogInformation("Wallhackers: {Count}, GlowData: {GlowCount}", Wallhackers.Count, GlowData.Count);
});
}
public override void UnassignFromPlayer(CCSPlayerController player)
{
base.UnassignFromPlayer(player);
RemoveWallhacker(player);
_plugin?.Logger.LogInformation("Wallhack unassigned from {PlayerName}", player.PlayerName);
}
protected override void OnInitialize()
{
_plugin = Plugin; // Set static plugin reference!
RegisterEventHandler<EventPlayerDeath>(_onPlayerDeathHandler);
RegisterEventHandler<EventPlayerDisconnect>(_onPlayerDisconnectHandler);
RegisterEventHandler<EventPlayerSpawn>(_onPlayerSpawnHandler, HookMode.Post);
RegisterEventHandler<EventPlayerTeam>(_onPlayerChangeTeamHandler, HookMode.Post);
_plugin?.Logger.LogInformation("Wallhack initialized!");
// Delay creating initial glows until global variables are initialized!
NextFrame(() =>
{
_plugin?.Logger.LogInformation("Creating initial glows for existing players!");
foreach (var existingPlayer in Utilities.GetPlayers())
{
if (IsPlayerValid(existingPlayer) && existingPlayer.PawnIsAlive && existingPlayer.Team >= CsTeam.Terrorist)
{
if (!GlowData.ContainsKey(existingPlayer))
{
_plugin?.Logger.LogInformation("Creating glow for {PlayerName} during init", existingPlayer.PlayerName);
Glow(existingPlayer);
}
}
}
});
}
public static HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
{
var player = @event.Userid;
_plugin?.Logger.LogInformation("Wallhack: OnPlayerSpawn called for {PlayerName}", player?.PlayerName ?? "NULL");
if (!IsPlayerValid(player))
{
_plugin?.Logger.LogInformation("Wallhack: Player invalid, skipping");
return HookResult.Continue;
}
if (player!.Team < CsTeam.Terrorist)
{
_plugin?.Logger.LogInformation("Wallhack: Player team invalid, skipping");
return HookResult.Continue;
}
var gameRules = Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules").FirstOrDefault();
if (gameRules?.GameRules?.WarmupPeriod == true)
{
_plugin?.Logger.LogInformation("Wallhack: Warmup period, skipping");
return HookResult.Continue;
}
_plugin?.Logger.LogInformation("Wallhack: Calling Glow for {PlayerName}", player.PlayerName);
Glow(player!);
return HookResult.Continue;
}
public static HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
var player = @event.Userid;
if (!IsPlayerValid(player)) return HookResult.Continue;
if (GlowData.TryGetValue(player!, out var glowData))
{
if (glowData.GlowEnt.IsValid)
glowData.GlowEnt.Remove();
if (glowData.ModelRelay.IsValid)
glowData.ModelRelay.Remove();
GlowData.Remove(player!);
}
Wallhackers.Remove(player!);
return HookResult.Continue;
}
public static HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
if (!IsPlayerValid(player)) return HookResult.Continue;
if (!GlowData.TryGetValue(player!, out var glowData)) return HookResult.Continue;
if (glowData.GlowEnt.IsValid)
glowData.GlowEnt.Remove();
if (glowData.ModelRelay.IsValid)
glowData.ModelRelay.Remove();
GlowData.Remove(player!);
return HookResult.Continue;
}
public static HookResult OnPlayerChangeTeam(EventPlayerTeam @event, GameEventInfo info)
{
var player = @event.Userid;
if (!IsPlayerValid(player)) return HookResult.Continue;
if (!GlowData.TryGetValue(player!, out var glowData)) return HookResult.Continue;
if (!glowData.GlowEnt.IsValid || !glowData.ModelRelay.IsValid) return HookResult.Continue;
NextWorldUpdate(() =>
{
glowData.GlowEnt.SetModel(GetPlayerModel(player!));
glowData.ModelRelay.SetModel(GetPlayerModel(player!));
});
return HookResult.Continue;
}
private static void Glow(CCSPlayerController player)
{
_plugin?.Logger.LogInformation("Wallhack: Glow() called for {PlayerName}", player.PlayerName);
// First remove existing glow for this player if it exists
if (GlowData.TryGetValue(player, out var existingGlow))
{
if (existingGlow.GlowEnt.IsValid)
existingGlow.GlowEnt.Remove();
if (existingGlow.ModelRelay.IsValid)
existingGlow.ModelRelay.Remove();
GlowData.Remove(player);
}
var glowEntity = Utilities.CreateEntityByName<CDynamicProp>("prop_dynamic");
var modelRelay = Utilities.CreateEntityByName<CDynamicProp>("prop_dynamic");
if (glowEntity == null || modelRelay == null)
{
_plugin?.Logger.LogError("Wallhack: Failed to create glow entities for {PlayerName}", player.PlayerName);
return;
}
modelRelay.Spawnflags = 256;
modelRelay.Render = Color.Transparent;
modelRelay.RenderMode = RenderMode_t.kRenderNone;
modelRelay.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags &= ~(1u << 2);
modelRelay.SetModel(GetPlayerModel(player));
glowEntity.Spawnflags = 256;
glowEntity.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags &= ~(1u << 2);
glowEntity.Render = Color.FromArgb(1, 0, 0, 0);
glowEntity.SetModel(GetPlayerModel(player));
glowEntity.DispatchSpawn();
modelRelay.DispatchSpawn();
glowEntity.Glow.GlowRange = 5000;
glowEntity.Glow.GlowRangeMin = 0;
glowEntity.Glow.GlowColorOverride = Color.FromArgb(255, 171, 75, 209);
glowEntity.Glow.GlowTeam = -1;
glowEntity.Glow.GlowType = 3;
modelRelay.AcceptInput("FollowEntity", player.Pawn.Value, null, "!activator");
glowEntity.AcceptInput("FollowEntity", modelRelay, null, "!activator");
GlowData.Add(player, new GlowData
{
GlowEnt = glowEntity,
ModelRelay = modelRelay
});
}
private static string GetPlayerModel(CCSPlayerController player)
{
return player.Pawn.Value!.CBodyComponent!.SceneNode!.GetSkeletonInstance().ModelState.ModelName;
}
private static bool IsPlayerValid(CCSPlayerController? plr) => plr != null &&
plr.IsValid &&
plr.PlayerPawn != null &&
plr.PlayerPawn.IsValid &&
plr.Connected == PlayerConnectedState.PlayerConnected &&
!plr.IsHLTV;
public static void OnPlayerTransmit(CCheckTransmitInfo info, CCSPlayerController player)
{
foreach (var entity in GlowData.ToList())
{
if (Wallhackers.Contains(player!))
{
if (!IsPlayerValid(entity.Key) || !IsPlayerValid(player)) continue;
if (entity.Key.Team != player!.Team && player!.Team != CsTeam.Spectator && entity.Key.Team != CsTeam.Spectator)
{
info.TransmitEntities.Add(entity.Value.ModelRelay);
info.TransmitEntities.Add(entity.Value.GlowEnt);
continue;
}
}
info.TransmitEntities.Remove(entity.Value.ModelRelay);
info.TransmitEntities.Remove(entity.Value.GlowEnt);
}
}
public static void AddWallhacker(CCSPlayerController player)
{
if (!Wallhackers.Contains(player))
{
Wallhackers.Add(player);
}
}
public static void RemoveWallhacker(CCSPlayerController player)
{
Wallhackers.Remove(player);
}
// Helper to wrap Server.NextWorldUpdate and store delegate to prevent GC!
private static void NextWorldUpdate(Action callback)
{
Action? wrappedCallback = null;
wrappedCallback = () =>
{
callback();
if (wrappedCallback != null)
{
_staticRegisteredDelegates.Remove(wrappedCallback);
}
};
_staticRegisteredDelegates.Add(wrappedCallback);
Server.NextWorldUpdate(wrappedCallback);
}
}