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 _lastUse = new(); protected override void OnInitialize() { RegisterEventHandler(OnRoundStart); RegisterEventHandler(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; } }