v1.2.3.b5

## GENERAL
- The HUD loop now runs on the shared 16 Hz frame gate (SkillUtils.IsHudFrame) instead of keeping its own every-other-tick schedule, and fifteen skills that were refreshing their HUD on every single tick were moved onto that same gate.
- Added SkillData.GetSnapshot(), a cached array of the registered skills, so the HUD no longer copies the whole skill bag every frame. The cache is invalidated together with the existing lookup map, so a reload still picks up new skills.
- Added SkillUtils.ForceFullUpdateToAllChunked(), which spreads a full network update across frames (two clients per frame) instead of pushing one for every connected client in a single tick. The original ForceFullUpdateToAll is untouched, so the six skills that already call it keep their current behaviour.
- BlastShot, Cypher and Iana now clear their HUD line in DisableSkill, so a stale line can no longer survive the skill being taken away.

## FIXES
- SoundMaker threw a divide-by-zero on every tick when its cooldown was configured as 0, because the tick gate divides by that value. The cooldown is now clamped to at least 1.
- Regeneration and RadarHack were calling the wrong SkillsInfo.DefaultSkillInfo overload, which silently dropped disableOnFreezeTime, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer and rarity. Every configured field now reaches the base class, so those two skills finally honour their own config.
- Falcon Eye blocked the trigger on the wrong side of the check: picking up a weapon while back in the normal view locked primary and secondary fire, because the pickup handler tested for the player being in their own view instead of in the skill camera.
- Spectator locked primary and secondary fire when its camera could not be created at all, which happens when no enemy is alive to spectate. The player stayed in the normal view with a weapon that would not fire until the round ended.

## SKILL IMPROVEMENTS
- Wallhack:
  -- The glow build and the full network update now start when freeze time ends and are spread across frames, instead of running in a single tick the moment the skill is handed out. On a busy server this used to cost roughly 13 ms in one frame, close to a dropped tick at 64 tick; across four grants in an eight-hour session it no longer reaches the 2 ms profiler threshold at all.
- Jackal:
  -- CheckTransmit resolves the trail relays once per frame rather than once per viewer, so the same entity lookups are no longer repeated for every client on the server.
  -- Removed an opponent list that was gathered in EnableSkill and never used.
- ThrowingKnife:
  -- CheckTransmit builds the knife glow list once per frame, and the viewer index is resolved once per client instead of once per knife.
- Glaz:
  -- The observer target is only resolved for players who do not own the skill themselves, and the lookup now bails out early when there is no observer target at all.
- RadarHack and QuickShot:
  -- The skill check runs before the validity check, so players without the skill are skipped immediately instead of being fully validated first.
- Distancer, HealingChicken and SoundMaker:
  -- These now return straight away while nobody holds the skill, rather than walking the player list every tick.

## DEBUG
- Removed the [HITGROUP] mismatch check. Every recorded case was accounted for: ten were Aimbot rewriting the hit group as designed, and thirteen were non-bullet skill damage, which reports a generic hit group by nature. Dropping the check also removes a dictionary write from every damage event.
- Removed the identity-split tag from the damage log. It was dead by construction, because the victim controller had already been routed before the comparison ran, so it could never report a difference.
- Removed the player-shoot handler, which produced no output on this build.
- [DMG] lines now print the routing fields only when the routed index actually differs from the original one, so the check remains as a tripwire without adding noise to every line.

## TRANSLATIONS
- Turkish: reworded sixty-two entries that padded their sentences with second-person verb endings, dropped the English glosses left in parentheses, and settled every [css_useSkill] line into one shape. Also fixed genuine typos in Dracula, Giant, Thorns and Impostor.
- All eight languages received the strings for the four new skills. (AI)

## NEW SKILLS:
- Chameleon: The first player you kill gives you their skill.
- Grapple Hook: Press [css_useSkill] to fire a hook at the point you are aiming at and pull yourself there.
- Inheritance: When a teammate dies you can take over their skill.
- Ricochet: Your bullets bounce off walls and can still hit enemies.
This commit is contained in:
ByDexter 2026-08-06 04:21:39 +03:00
parent 7b4eff1dba
commit 709ed95614
59 changed files with 2248 additions and 458 deletions

View file

@ -4,7 +4,6 @@ using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using static CounterStrikeSharp.API.Core.Listeners;
using static src.jRandomSkills;
@ -96,33 +95,9 @@ namespace src.player
WriteToDebug($"Map changed: {mapName}.");
});
Instance.RegisterEventHandler<EventPlayerShoot>((@event, info) =>
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return HookResult.Continue;
WriteToDebug($"{(player.IsBot ? "Bot" : "Player")} {player.PlayerName} fired a shot.");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventPlayerHurt>((@event, info) =>
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return HookResult.Continue;
if (!lastNativeHitGroup.TryRemove(victim.Index, out var native)) return HookResult.Continue;
if (native.HitGroup == @event.Hitgroup) return HookResult.Continue;
WriteToDebug($"[HITGROUP] mismatch on {victim.PlayerName}: native={(HitGroup_t)native.HitGroup} event={(HitGroup_t)@event.Hitgroup} " +
$"weapon={@event.Weapon} rawDmg={native.Damage:0.#} appliedDmg={@event.DmgHealth}");
return HookResult.Continue;
});
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Hook(OnTakeDamage, HookMode.Pre);
}
private static readonly ConcurrentDictionary<uint, (int HitGroup, float Damage)> lastNativeHitGroup = [];
public static void Unload()
{
try { VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Unhook(OnTakeDamage, HookMode.Pre); }
@ -131,16 +106,6 @@ namespace src.player
lock (_writeLock) { _writer?.Dispose(); _writer = null; }
}
private static string DescribeIdentitySplit(CCSPlayerController victim)
{
var routed = PlayerManager.GetPlayerEvent(victim);
uint routedIndex = routed?.Index ?? victim.Index;
if (routedIndex == victim.Index) return string.Empty;
return $" SPLIT(idx {victim.Index}->{routedIndex}, skill {PlayerManager.GetPlayerByIndex(victim.Index)?.Skill}" +
$"->{PlayerManager.GetPlayerByIndex(routedIndex)?.Skill}, controllingBot={victim.ControllingBot})";
}
private static HookResult OnTakeDamage(DynamicHook h)
{
CEntityInstance param = h.GetParam<CEntityInstance>(0);
@ -165,12 +130,10 @@ namespace src.player
if (playerInfo == null) return HookResult.Continue;
var nativeHitGroup = SkillUtils.GetHitGroup(param2);
lastNativeHitGroup[victim.Index] = ((int)nativeHitGroup, param2.Damage);
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} took damage from {(attacker.IsBot ? "bot" : "player")} {attacker.PlayerName}. " +
$"[dmg={param2.Damage:0.#} hp={victimPawn.Health}/{victimPawn.MaxHealth} armor={victimPawn.ArmorValue} hitgroup={nativeHitGroup} " +
$"takes={victimPawn.TakesDamage} vskill={PlayerManager.GetPlayerByIndex(victim.Index)?.Skill} askill={playerInfo.Skill}" +
$"{DescribeIdentitySplit(victim)}]");
$"takes={victimPawn.TakesDamage} vskill={PlayerManager.GetPlayerByIndex(victim.Index)?.Skill} askill={playerInfo.Skill}]");
return HookResult.Continue;
}

View file

@ -79,6 +79,7 @@ public enum Skills
C4Camouflage,
CarefulBullets,
Catapult,
Chameleon,
Chicken,
ChillOut,
Cutter,
@ -114,6 +115,7 @@ public enum Skills
Glitch,
Glue,
GodMode,
Grapple,
Grenadier,
HealingChicken,
HealingSmoke,
@ -126,6 +128,7 @@ public enum Skills
Illusionist,
Impostor,
InfiniteAmmo,
Inheritance,
Jackal,
Jammer,
Jester,
@ -173,6 +176,7 @@ public enum Skills
Retreat,
ReturnToSender,
RichBoy,
Ricochet,
RobinHood,
Rubber,
Saper,

View file

@ -226,8 +226,11 @@ namespace src.player
if (controller == null || !controller.IsValid) return string.Empty;
uint routedIndex = PlayerManager.GetPlayerEvent(controller)?.Index ?? controller.Index;
return $" on {controller.PlayerName} [idx={controller.Index} skill={PlayerManager.GetPlayerByIndex(controller.Index)?.Skill}" +
$" routedIdx={routedIndex} routedSkill={PlayerManager.GetPlayerByIndex(routedIndex)?.Skill}]";
string routing = routedIndex == controller.Index
? string.Empty
: $" routedIdx={routedIndex} routedSkill={PlayerManager.GetPlayerByIndex(routedIndex)?.Skill}";
return $" on {controller.PlayerName} [idx={controller.Index} skill={PlayerManager.GetPlayerByIndex(controller.Index)?.Skill}{routing}]";
}
catch
{

View file

@ -15,7 +15,7 @@ namespace src.player
Instance.RegisterListener<OnTick>(() =>
{
UpdateGameRules();
if (Server.TickCount % 2 != 0) return;
if (!SkillUtils.IsHudFrame()) return;
if (PerfLog.Enabled && Server.TickCount % 1920 == 0)
{
@ -99,25 +99,19 @@ namespace src.player
bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now || Config.LoadedConfig.DisplayAlwaysDescription;
bool isDescription = true;
var skills = SkillData.Skills;
var skills = SkillData.GetSnapshot();
if (skills == null || skills.IsEmpty)
if (skills.Length == 0)
{
infoLine = player.GetTranslationWithoutIlliterate("your_skill");
skillLine = player.GetTranslationWithoutIlliterate("none");
}
else if (skillPlayer.IsDrawing && player.PawnIsAlive)
{
int skillCount = skills.Count;
var randomSkill = skills[Instance.Random.Next(skills.Length)];
if (skillCount > 0)
{
var skillsArray = skills.ToArray();
var randomSkill = skillsArray[Instance.Random.Next(skillCount)];
infoLine = player.GetTranslationWithoutIlliterate("drawing_skill");
skillLine = $"<font color='{randomSkill.Color}'>{player.GetSkillName(randomSkill.Skill)}</font>";
}
infoLine = player.GetTranslationWithoutIlliterate("drawing_skill");
skillLine = $"<font color='{randomSkill.Color}'>{player.GetSkillName(randomSkill.Skill)}</font>";
}
else
{

View file

@ -45,6 +45,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) return;
SkillPlayerInfo.TryRemove(player.Index, out _);
SkillUtils.ResetPrintHTML(player);
}
public static void OnTick()

View file

@ -0,0 +1,166 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using static src.jRandomSkills;
namespace src.player.skills
{
public class Chameleon : ISkill
{
private const Skills skillName = Skills.Chameleon;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static jRandomSkills? registeredOn;
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
if (ReferenceEquals(registeredOn, Instance)) return;
registeredOn = Instance;
Instance.RegisterEventHandler<EventPlayerDeath>(OnAnyDeath);
}
public static void NewRound()
{
holders.Clear();
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
holders[player.Index] = 0;
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
holders.TryRemove(player.Index, out _);
}
public static void PlayerDisconnect(uint playerIndex)
{
holders.TryRemove(playerIndex, out _);
}
private static HookResult OnAnyDeath(EventPlayerDeath @event, GameEventInfo info)
{
try
{
if (holders.IsEmpty) return HookResult.Continue;
var attacker = PlayerManager.GetPlayerEvent(@event.Attacker);
if (attacker == null || !attacker.IsValid) return HookResult.Continue;
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return HookResult.Continue;
if (attacker.Index == victim.Index) return HookResult.Continue;
if (attacker.Team == victim.Team) return HookResult.Continue;
if (!holders.ContainsKey(attacker.Index)) return HookResult.Continue;
var victimInfo = PlayerManager.GetPlayerByIndex(victim.Index);
if (victimInfo == null || victimInfo.IsDrawing) return HookResult.Continue;
CopySkill(attacker, victim.PlayerName, victimInfo.Skill);
}
catch (Exception ex)
{
Server.PrintToConsole($"[jRandomSkills] Chameleon.OnAnyDeath failed: {ex.Message}");
}
return HookResult.Continue;
}
private static void CopySkill(CCSPlayerController player, string victimName, Skills victimSkill)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player.Index);
if (playerInfo == null) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (!CanCopy(player, victimSkill, out string? blockReason))
{
if (blockReason != null && playerEvent != null && playerEvent.IsValid)
playerEvent.PrintToChat($" {ChatColors.Red}" + playerEvent.GetTranslation(blockReason, victimName));
return;
}
uint playerIndex = player.Index;
Skills previousSkill = playerInfo.Skill;
if (!holders.TryRemove(playerIndex, out _)) return;
Instance.AddTimer(.1f, () =>
{
var target = Utilities.GetPlayerFromIndex((int)playerIndex);
if (target == null || !target.IsValid) return;
var targetInfo = PlayerManager.GetPlayerByIndex(playerIndex);
if (targetInfo == null || targetInfo.Skill != previousSkill) return;
Instance.SkillAction(previousSkill.ToString(), "DisableSkill", [target]);
targetInfo.Skill = victimSkill;
targetInfo.SpecialSkill = skillName;
targetInfo.SkillChance = null;
if (target.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (SkillsInfo.GetValue<bool>(victimSkill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime())
Instance.AddTimer(Math.Max((float)(Event.GetFreezeTimeEnd() - DateTime.Now).TotalSeconds, 0), () =>
{
var heir = Utilities.GetPlayerFromIndex((int)playerIndex);
if (heir == null || !heir.IsValid) return;
if (PlayerManager.GetPlayerByIndex(playerIndex)?.Skill != victimSkill) return;
Instance.SkillAction(victimSkill.ToString(), "EnableSkill", [heir]);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
else
Instance.SkillAction(victimSkill.ToString(), "EnableSkill", [target]);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
if (playerEvent != null && playerEvent.IsValid)
playerEvent.PrintToChat($" {ChatColors.Green}" + playerEvent.GetTranslation("chameleon_copied_info", player.GetSkillName(victimSkill)));
}
private static bool CanCopy(CCSPlayerController player, Skills victimSkill, out string? blockReason)
{
blockReason = null;
if (victimSkill == Skills.None || victimSkill == skillName) return false;
if (SkillData.GetInfo(victimSkill) == null) return false;
bool ctOnly = Event.counterterroristSkills.Any(s => s.Name == victimSkill.ToString());
bool ttOnly = Event.terroristSkills.Any(s => s.Name == victimSkill.ToString());
if ((player.Team == CsTeam.Terrorist && ctOnly) || (player.Team == CsTeam.CounterTerrorist && ttOnly))
{
blockReason = "chameleon_wrong_team_info";
return false;
}
if (!player.IsBot)
{
string permission = SkillsInfo.GetValue<string>(victimSkill, "requiredPermission");
if (!string.IsNullOrEmpty(permission) && !AdminManager.PlayerHasPermissions(player, permission))
return false;
}
if (!SkillsInfo.GetValue<bool>(victimSkill, "needsTeammates")) return true;
return PlayerManager.GetTickPlayers().Any(p =>
p != null && p.IsValid && p.Index != player.Index && p.Team == player.Team
&& p.PlayerPawn?.Value != null && p.PlayerPawn.Value.IsValid && p.PlayerPawn.Value.Health > 0);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5fd98a", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
}
}
}

View file

@ -78,6 +78,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
if (plantingPlayers.IsEmpty) return;
float currentTime = Server.CurrentTime;

View file

@ -71,6 +71,7 @@ namespace src.player.skills
KillCamera(playerSkill);
}
playersInfo.TryRemove(player.Index, out _);
SkillUtils.ResetPrintHTML(player);
}
private static void KillCamera(PlayerSkill playerSkill)

View file

@ -77,7 +77,7 @@ namespace src.player.skills
if (playerInfo?.Skill == skillName)
if (SkillPlayerInfo.TryGetValue(eventPlayer!.Index, out var skillInfo))
{
UpdateHUD(player, skillInfo);
if (SkillUtils.IsHudFrame()) UpdateHUD(player, skillInfo);
HandleDash(player, skillInfo);
}
}

View file

@ -25,6 +25,10 @@ namespace src.player.skills
public static void OnTick()
{
if (distancerPlayers.IsEmpty) return;
if (!SkillUtils.IsHudFrame()) return;
if (SkillUtils.IsFreezeTime()) return;
foreach (var playerIndex in distancerPlayers.Keys)
{
@ -41,8 +45,10 @@ namespace src.player.skills
string closetEnemy = "Bot";
double closetDistance = double.MaxValue;
foreach (var enemy in PlayerManager.GetTickPlayers().Where(p => p.Team != player.Team))
foreach (var enemy in PlayerManager.GetTickPlayers())
{
if (enemy == null || enemy.Team == player.Team) continue;
var enemyPawn = enemy.PlayerPawn.Value;
if (enemyPawn == null || !enemyPawn.IsValid) continue;
if (enemyPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE || playerPawn.AbsOrigin == null || enemyPawn.AbsOrigin == null) continue;

View file

@ -25,6 +25,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
if (SkillUtils.IsFreezeTime()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{

View file

@ -39,7 +39,7 @@ namespace src.player.skills
var pawn = player!.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || pawn.CameraServices == null) return;
if (cameras.TryGetValue(player!.Index, out var cameraInfo) && cameraInfo.Item1 == pawn.CameraServices.ViewEntity.Raw)
if (cameras.TryGetValue(player!.Index, out var cameraInfo) && cameraInfo.Item1 != pawn.CameraServices.ViewEntity.Raw)
BlockWeapon(player, true);
}

View file

@ -54,7 +54,7 @@ namespace src.player.skills
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo))
continue;
UpdateHUD(player, skillInfo);
if (SkillUtils.IsHudFrame()) UpdateHUD(player, skillInfo);
if (!skillInfo.IsActive || skillInfo.LightEntityIndex == 0)
continue;

View file

@ -34,6 +34,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -46,10 +46,17 @@ namespace src.player.skills
if (player == null || !player.IsValid) continue;
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
var observedPlayer = PlayerManager.GetTickPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == player?.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle);
var observerInfo = observedPlayer != null ? PlayerManager.GetPlayerByIndex(observedPlayer.Index) : null;
if (playerInfo?.Skill != skillName)
{
var observerTarget = player.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle ?? nint.Zero;
if (observerTarget == nint.Zero) continue;
var observedPlayer = PlayerManager.GetTickPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == observerTarget);
if (observedPlayer == null) continue;
if (PlayerManager.GetPlayerByIndex(observedPlayer.Index)?.Skill != skillName) continue;
}
if (playerInfo?.Skill != skillName && observerInfo?.Skill != skillName) continue;
foreach (var entityIndex in smokes.Keys)
{
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)entityIndex);

View file

@ -43,6 +43,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -0,0 +1,316 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace src.player.skills
{
public class Grapple : ISkill
{
private const Skills skillName = Skills.Grapple;
private static readonly ConcurrentDictionary<uint, PlayerSkillInfo> SkillPlayerInfo = [];
private static readonly ConcurrentDictionary<uint, byte> liveRopes = [];
private static readonly object setLock = new();
private static readonly Color terroristRope = Color.FromArgb(255, 138, 84, 34);
private static readonly Color counterTerroristRope = Color.FromArgb(255, 52, 92, 138);
private const double attemptCooldown = .3;
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
lock (setLock)
{
foreach (var skillInfo in SkillPlayerInfo.Values)
DestroyRope(skillInfo);
SkillPlayerInfo.Clear();
SweepOrphanRopes();
}
}
public static void RoundEnd()
{
foreach (var skillInfo in SkillPlayerInfo.Values)
DestroyRope(skillInfo);
SweepOrphanRopes();
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
SkillPlayerInfo[player.Index] = new PlayerSkillInfo
{
CanUse = true,
Cooldown = DateTime.MinValue,
};
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
if (SkillPlayerInfo.TryRemove(player.Index, out var skillInfo))
DestroyRope(skillInfo);
SkillUtils.ResetPrintHTML(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
if (SkillPlayerInfo.TryRemove(playerIndex, out var skillInfo))
DestroyRope(skillInfo);
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo))
DestroyRope(skillInfo);
}
public static void UseSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo)) return;
if (skillInfo.Anchor != null)
{
DestroyRope(skillInfo);
return;
}
if (!skillInfo.CanUse) return;
if ((DateTime.Now - skillInfo.LastAttempt).TotalSeconds < attemptCooldown) return;
skillInfo.LastAttempt = DateTime.Now;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (!TryAttach(player, skillInfo))
{
if (playerEvent != null && playerEvent.IsValid)
playerEvent.PrintToChat($" {ChatColors.Red}" + playerEvent.GetTranslation("grapple_no_anchor_info"));
return;
}
skillInfo.CanUse = false;
skillInfo.Cooldown = DateTime.Now;
}
private static bool TryAttach(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
if (!RayTrace.IsAvailable) return false;
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) return false;
var result = RayTrace.EyeTrace(player);
if (result == null || !result.Value.DidHit) return false;
if (result.Value.HitPlayer(out _)) return false;
Vector anchor = new(result.Value.EndPosX, result.Value.EndPosY, result.Value.EndPosZ);
Vector eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
if (SkillUtils.GetDistance(eyePos, anchor) < SkillsInfo.GetValue<float>(skillName, "minDistance")) return false;
skillInfo.Anchor = anchor;
skillInfo.EndTick = Server.TickCount + (int)(SkillsInfo.GetValue<float>(skillName, "maxPullSeconds") * 64);
if (!EntityManager.OverBudget())
{
Color ropeColor = player.Team == CsTeam.Terrorist ? terroristRope : counterTerroristRope;
var rope = EntityManager.CreateTrackedBeam(player.Index, eyePos, anchor, ropeColor);
if (rope != null && rope.IsValid)
{
float width = SkillsInfo.GetValue<float>(skillName, "ropeWidth");
rope.Width = width;
rope.EndWidth = width;
Utilities.SetStateChanged(rope, "CBeam", "m_fWidth");
Utilities.SetStateChanged(rope, "CBeam", "m_fEndWidth");
uint ropeIndex = rope.Index;
skillInfo.RopeIndex = ropeIndex;
liveRopes[ropeIndex] = 0;
jRandomSkills.Instance.AddTimer(SkillsInfo.GetValue<float>(skillName, "maxPullSeconds") + 1f,
() => KillRope(ropeIndex),
CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
}
player.EmitSound("SolidMetal.BulletImpact");
return true;
}
private static void DestroyRope(PlayerSkillInfo skillInfo)
{
skillInfo.Anchor = null;
skillInfo.EndTick = 0;
if (skillInfo.RopeIndex == null) return;
uint ropeIndex = skillInfo.RopeIndex.Value;
skillInfo.RopeIndex = null;
KillRope(ropeIndex);
}
private static void KillRope(uint ropeIndex)
{
if (!liveRopes.TryRemove(ropeIndex, out _)) return;
var rope = Utilities.GetEntityFromIndex<CBeam>((int)ropeIndex);
if (rope != null && rope.IsValid)
{
rope.Width = 0;
rope.EndWidth = 0;
rope.Render = Color.FromArgb(0, 0, 0, 0);
Utilities.SetStateChanged(rope, "CBeam", "m_fWidth");
Utilities.SetStateChanged(rope, "CBeam", "m_fEndWidth");
Utilities.SetStateChanged(rope, "CBaseModelEntity", "m_clrRender");
}
EntityManager.DestroyEntity(ropeIndex);
Server.NextFrame(() =>
{
var leftover = Utilities.GetEntityFromIndex<CBeam>((int)ropeIndex);
if (leftover != null && leftover.IsValid)
leftover.Remove();
});
}
private static void SweepOrphanRopes()
{
foreach (uint ropeIndex in liveRopes.Keys)
KillRope(ropeIndex);
}
public static void OnTick()
{
if (SkillPlayerInfo.IsEmpty) return;
bool hudFrame = SkillUtils.IsHudFrame();
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid) continue;
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo)) continue;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) continue;
if (skillInfo.Anchor != null)
HandlePull(player, skillInfo);
if (hudFrame)
UpdateHUD(player, skillInfo);
}
}
private static void HandlePull(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
DestroyRope(skillInfo);
return;
}
if (Server.TickCount >= skillInfo.EndTick)
{
DestroyRope(skillInfo);
return;
}
Vector anchor = skillInfo.Anchor!;
Vector eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
float dx = anchor.X - eyePos.X;
float dy = anchor.Y - eyePos.Y;
float dz = anchor.Z - eyePos.Z;
float distance = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
if (distance <= SkillsInfo.GetValue<float>(skillName, "stopDistance"))
{
DestroyRope(skillInfo);
return;
}
float pullSpeed = SkillsInfo.GetValue<float>(skillName, "pullSpeed");
float invLength = pullSpeed / distance;
pawn.AbsVelocity.X = dx * invLength;
pawn.AbsVelocity.Y = dy * invLength;
pawn.AbsVelocity.Z = dz * invLength;
if (skillInfo.RopeIndex == null) return;
var rope = Utilities.GetEntityFromIndex<CBeam>((int)skillInfo.RopeIndex.Value);
if (rope == null || !rope.IsValid)
{
liveRopes.TryRemove(skillInfo.RopeIndex.Value, out _);
skillInfo.RopeIndex = null;
return;
}
rope.Teleport(eyePos);
}
private static void UpdateHUD(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
float time = (int)Math.Ceiling((skillInfo.Cooldown.AddSeconds(SkillsInfo.GetValue<float>(skillName, "cooldown")) - DateTime.Now).TotalSeconds);
float cooldown = Math.Max(time, 0);
if (cooldown == 0 && !skillInfo.CanUse)
skillInfo.CanUse = true;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
if (skillInfo.Anchor != null)
playerInfo.PrintHTML = $"<font color='#00FF00'>{player.GetTranslation("grapple_pulling_info")}</font>";
else if (cooldown == 0)
playerInfo.PrintHTML = null;
else
playerInfo.PrintHTML = $"{player.GetTranslation("hud_info", $"<font color='#FF0000'>{cooldown}</font>")}";
}
public class PlayerSkillInfo
{
public bool CanUse { get; set; }
public DateTime Cooldown { get; set; }
public DateTime LastAttempt { get; set; }
public Vector? Anchor { get; set; }
public uint? RopeIndex { get; set; }
public int EndTick { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#38e0c4", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, float cooldown = 10f, float maxDistance = 1500f, float minDistance = 150f, float stopDistance = 90f, float pullSpeed = 850f, float maxPullSeconds = 3f, float ropeWidth = 0.8f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float Cooldown { get; set; } = cooldown;
public float MaxDistance { get; set; } = maxDistance;
public float MinDistance { get; set; } = minDistance;
public float StopDistance { get; set; } = stopDistance;
public float PullSpeed { get; set; } = pullSpeed;
public float MaxPullSeconds { get; set; } = maxPullSeconds;
public float RopeWidth { get; set; } = ropeWidth;
}
}
}

View file

@ -139,12 +139,14 @@ namespace src.player.skills
public static void OnTick()
{
if (activeChickens.IsEmpty) return;
int tickCooldown = SkillsInfo.GetValue<int>(skillName, "tickCooldown");
if (Server.TickCount % tickCooldown == 0) return;
float boostFactor = SkillsInfo.GetValue<float>(skillName, "boostFactor");
var players = PlayerManager.GetTickPlayers().ToArray();
var players = PlayerManager.GetTickPlayers();
int healAmount = SkillsInfo.GetValue<int>(skillName, "heal");
float healRadius = SkillsInfo.GetValue<float>(skillName, "healRadius");

View file

@ -90,6 +90,7 @@ namespace src.player.skills
KillClone(playerSkill);
playersInfo.TryRemove(player.Index, out _);
EntityManager.DestroyPlayerEntities(player.Index);
SkillUtils.ResetPrintHTML(player);
}
private static void UpdateWeapons(CCSPlayerController player, PlayerSkill playerSkill)
@ -146,6 +147,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var playerSkill in playersInfo.Values)
UpdateHUD(playerSkill);
}

View file

@ -47,6 +47,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid) continue;

View file

@ -0,0 +1,201 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using static src.jRandomSkills;
namespace src.player.skills
{
public class Inheritance : ISkill
{
private const Skills skillName = Skills.Inheritance;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static readonly ConcurrentDictionary<uint, FallenInfo> fallen = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
holders.Clear();
fallen.Clear();
foreach (var player in PlayerManager.GetTickPlayers())
SkillUtils.CloseMenu(player);
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
holders[player.Index] = 0;
RefreshMenu(player);
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
holders.TryRemove(player.Index, out _);
SkillUtils.CloseMenu(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
holders.TryRemove(playerIndex, out _);
fallen.TryRemove(playerIndex, out _);
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
holders.TryRemove(victim.Index, out _);
var victimInfo = PlayerManager.GetPlayerByIndex(victim.Index);
if (victimInfo == null || victimInfo.IsDrawing) return;
if (victimInfo.Skill == Skills.None || victimInfo.Skill == skillName) return;
if (SkillData.GetInfo(victimInfo.Skill) == null) return;
fallen[victim.Index] = new FallenInfo
{
PlayerName = victim.PlayerName,
Skill = victimInfo.Skill,
Team = victim.Team,
};
foreach (uint holderIndex in holders.Keys)
{
var holder = Utilities.GetPlayerFromIndex((int)holderIndex);
if (holder == null || !holder.IsValid || holder.Team != victim.Team) continue;
RefreshMenu(holder);
}
}
public static void TypeSkill(CCSPlayerController player, string[] commands)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player.Index);
if (playerInfo?.Skill != skillName) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
if (commands == null || commands.Length == 0 || !uint.TryParse(commands[0], out uint fallenIndex))
{
playerEvent.PrintToChat($" {ChatColors.Red}" + playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index"));
return;
}
if (!fallen.TryGetValue(fallenIndex, out var fallenInfo) || !CanInherit(player, fallenInfo))
{
playerEvent.PrintToChat($" {ChatColors.Red}" + playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index"));
return;
}
InheritSkill(player, fallenInfo);
}
private static void InheritSkill(CCSPlayerController player, FallenInfo fallenInfo)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player.Index);
if (playerInfo == null) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
uint playerIndex = player.Index;
Skills inheritedSkill = fallenInfo.Skill;
holders.TryRemove(playerIndex, out _);
SkillUtils.CloseMenu(player);
Instance.AddTimer(.1f, () =>
{
var target = Utilities.GetPlayerFromIndex((int)playerIndex);
if (target == null || !target.IsValid) return;
var targetInfo = PlayerManager.GetPlayerByIndex(playerIndex);
if (targetInfo == null || targetInfo.Skill != skillName) return;
targetInfo.Skill = inheritedSkill;
targetInfo.SpecialSkill = skillName;
SkillUtils.CloseMenu(target);
if (SkillsInfo.GetValue<bool>(inheritedSkill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime())
Instance?.AddTimer(Math.Max((float)(Event.GetFreezeTimeEnd() - DateTime.Now).TotalSeconds, 0), () =>
{
var heir = Utilities.GetPlayerFromIndex((int)playerIndex);
if (heir == null || !heir.IsValid) return;
if (PlayerManager.GetPlayerByIndex(playerIndex)?.Skill != inheritedSkill) return;
Instance?.SkillAction(inheritedSkill.ToString(), "EnableSkill", [heir]);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
else
Instance?.SkillAction(inheritedSkill.ToString(), "EnableSkill", [target]);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
playerEvent.PrintToChat($" {ChatColors.Green}" + playerEvent.GetTranslation("inheritance_player_info", fallenInfo.PlayerName));
}
private static void RefreshMenu(CCSPlayerController player)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) return;
ConcurrentBag<(string, string)> menuItems = [];
foreach (var (fallenIndex, fallenInfo) in fallen)
{
if (fallenIndex == player.Index) continue;
if (!CanInherit(player, fallenInfo)) continue;
menuItems.Add(($"\u202A{fallenInfo.PlayerName}\u202C : {player.GetSkillName(fallenInfo.Skill)}", fallenIndex.ToString()));
}
if (menuItems.IsEmpty) return;
if (SkillUtils.HasMenu(player))
SkillUtils.UpdateMenu(player, menuItems);
else
SkillUtils.CreateMenu(player, menuItems);
}
private static bool CanInherit(CCSPlayerController player, FallenInfo fallenInfo)
{
if (fallenInfo.Team != player.Team) return false;
if (!player.IsBot)
{
string permission = SkillsInfo.GetValue<string>(fallenInfo.Skill, "requiredPermission");
if (!string.IsNullOrEmpty(permission) && !AdminManager.PlayerHasPermissions(player, permission))
return false;
}
if (!SkillsInfo.GetValue<bool>(fallenInfo.Skill, "needsTeammates")) return true;
return PlayerManager.GetTickPlayers().Any(p =>
p != null && p.IsValid && p.Index != player.Index && p.Team == player.Team
&& p.PlayerPawn?.Value != null && p.PlayerPawn.Value.IsValid && p.PlayerPawn.Value.Health > 0);
}
private class FallenInfo
{
public required string PlayerName { get; set; }
public required Skills Skill { get; set; }
public required CsTeam Team { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#c9a227", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = true, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
}
}
}

View file

@ -38,9 +38,27 @@ namespace src.player.skills
playersInAction.Clear();
}
private static readonly List<(CsTeam OwnerTeam, uint RelayEntityIndex)> transmitTrails = [];
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{
var snapshot = activeTrails.ToArray();
if (activeTrails.IsEmpty) return;
transmitTrails.Clear();
foreach (var (trackedPlayerIndex, relayIndex) in activeTrails)
{
if (relayIndex == null) continue;
var trailOwner = Utilities.GetPlayerFromIndex((int)trackedPlayerIndex);
if (trailOwner == null || !trailOwner.IsValid) continue;
var relayEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)relayIndex);
if (relayEntity == null || !relayEntity.IsValid) continue;
transmitTrails.Add((trailOwner.Team, relayEntity.Index));
}
if (transmitTrails.Count == 0) return;
foreach (var (info, player) in infoList)
{
@ -65,19 +83,12 @@ namespace src.player.skills
}
}
foreach (var (trackedPlayerIndex, relayIndex) in snapshot)
var playerTeam = player.Team;
foreach (var (ownerTeam, relayEntityIndex) in transmitTrails)
{
if (relayIndex == null) continue;
var trailOwner = Utilities.GetPlayerFromIndex((int)trackedPlayerIndex);
if (trailOwner == null || !trailOwner.IsValid) continue;
var relayEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)relayIndex);
if (relayEntity == null || !relayEntity.IsValid) continue;
if (player.Team == trailOwner.Team || !isJackalOwner)
if (info.TransmitEntities.Contains(relayEntity.Index))
info.TransmitEntities.Remove(relayEntity.Index);
if (playerTeam == ownerTeam || !isJackalOwner)
if (info.TransmitEntities.Contains(relayEntityIndex))
info.TransmitEntities.Remove(relayEntityIndex);
}
}
}
@ -135,14 +146,6 @@ namespace src.player.skills
Event.EnableTransmit();
playersInAction.TryAdd(player.Index, 0);
var opponents = PlayerManager.GetTickPlayers()
.Where(p => p != null
&& p.IsValid
&& p.Team != player.Team
&& p.PawnIsAlive
&& (p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist))
.ToArray();
mainSkillTimer ??= Instance.AddTimer(2.5f, () => UpdateAllTrails(),
CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT | CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);

View file

@ -235,6 +235,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
if (SkillUtils.IsFreezeTime()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{

View file

@ -26,6 +26,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
if (SkillUtils.IsFreezeTime()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{

View file

@ -29,6 +29,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -19,31 +19,30 @@ namespace src.player.skills
{
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null) continue;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) continue;
if (!Instance.IsPlayerValid(player)) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill == skillName)
var pawn = player.PlayerPawn.Value!;
var weaponServices = pawn.WeaponServices;
if (weaponServices == null || weaponServices.ActiveWeapon == null || !weaponServices.ActiveWeapon.IsValid) continue;
var weapon = weaponServices.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid || pawn.CameraServices == null) continue;
if (pawn.AimPunchServices != null)
{
var pawn = player.PlayerPawn.Value!;
var weaponServices = pawn.WeaponServices;
if (weaponServices == null || weaponServices.ActiveWeapon == null || !weaponServices.ActiveWeapon.IsValid) continue;
var weapon = weaponServices.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid || pawn.CameraServices == null) continue;
if (pawn.AimPunchServices != null)
{
pawn.AimPunchServices.PredictableBaseTick = 0;
pawn.AimPunchServices.PredictableBaseTickInterpAmount = 0;
pawn.AimPunchServices.UnpredictableBaseTick = 0;
}
pawn.CameraServices.CsViewPunchAngleTick = 0;
pawn.CameraServices.CsViewPunchAngleTickRatio = 0f;
Schema.SetSchemaValue<Int32>(weapon.Handle, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick", Server.TickCount);
Schema.SetSchemaValue<Int32>(weapon.Handle, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick", Server.TickCount);
pawn.AimPunchServices.PredictableBaseTick = 0;
pawn.AimPunchServices.PredictableBaseTickInterpAmount = 0;
pawn.AimPunchServices.UnpredictableBaseTick = 0;
}
pawn.CameraServices.CsViewPunchAngleTick = 0;
pawn.CameraServices.CsViewPunchAngleTickRatio = 0f;
Schema.SetSchemaValue<Int32>(weapon.Handle, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick", Server.TickCount);
Schema.SetSchemaValue<Int32>(weapon.Handle, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick", Server.TickCount);
}
}

View file

@ -21,11 +21,12 @@ namespace src.player.skills
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerEvent = PlayerManager.GetPlayerEvent(player);
if (playerEvent == null) continue;
if (PlayerManager.GetPlayerByIndex(playerEvent.Index)?.Skill != skillName) continue;
if (!Instance.IsPlayerValid(playerEvent)) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(playerEvent!.Index);
if (playerInfo?.Skill == skillName)
SetEnemiesVisibleOnRadar(player);
SetEnemiesVisibleOnRadar(player);
}
}
@ -36,8 +37,10 @@ namespace src.player.skills
// SpottedByMask is indexed by player slot (0-63), not entity index.
int slot = player.Slot;
foreach (var enemy in PlayerManager.GetTickPlayers().FindAll(p => p.Team != player.Team))
foreach (var enemy in PlayerManager.GetTickPlayers())
{
if (enemy == null || enemy.Team == player.Team) continue;
var enemyEvent = PlayerManager.GetPlayerEvent(enemy);
if (enemyEvent == null || !enemyEvent.IsValid) continue;
@ -59,7 +62,7 @@ namespace src.player.skills
public class SkillConfig : SkillsInfo.DefaultSkillInfo
{
public SkillConfig(Skills skill = skillName, bool active = true, string color = "#2effcb", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = utils.Rarity.Common) : base(skill, active, color, onlyTeam, needsTeammates)
public SkillConfig(Skills skill = skillName, bool active = true, string color = "#2effcb", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = utils.Rarity.Common) : base(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
}
}

View file

@ -40,6 +40,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
var players = PlayerManager.GetTickPlayers().ToArray();
foreach (var player in players)
{

View file

@ -25,6 +25,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -33,7 +33,7 @@ namespace src.player.skills
{
public int HealthToAdd { get; set; }
public float Cooldown { get; set; }
public SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff462e", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = utils.Rarity.Common, int healthToAdd = 1, float cooldown = .25f) : base(skill, active, color, onlyTeam, needsTeammates)
public SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff462e", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = utils.Rarity.Common, int healthToAdd = 1, float cooldown = .25f) : base(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
HealthToAdd = healthToAdd;
Cooldown = cooldown;

View file

@ -40,6 +40,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -26,6 +26,7 @@ namespace src.player.skills
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -0,0 +1,661 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
using RayTraceAPI;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
using System.Numerics;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace src.player.skills
{
public class Ricochet : ISkill
{
private const Skills skillName = Skills.Ricochet;
private static readonly ConcurrentDictionary<uint, ImpactBudget> impactBudgets = [];
private static readonly HashSet<uint> ownedTracers = [];
private static readonly List<uint> tracerPool = [];
private static readonly List<Flight> flights = [];
private static readonly object flightLock = new();
private static readonly Color tracerColor = Color.FromArgb(170, 255, 226, 140);
private static readonly ulong worldOnlyMask = (ulong)InteractionLayers.MASK_WORLD_ONLY;
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
impactBudgets.Clear();
ResetPool(false);
}
public static void RoundEnd()
{
ResetPool(true);
}
public static void PlayerDisconnect(uint playerIndex)
{
impactBudgets.TryRemove(playerIndex, out _);
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
impactBudgets.TryRemove(player.Index, out _);
}
public static void BulletImpact(EventBulletImpact @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) return;
if (!RayTrace.IsAvailable)
{
Trace("RayTrace module unavailable");
return;
}
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
Trace("shooter pawn invalid or dead");
return;
}
if (!TryConsumeBudget(player.Index)) return;
Vector3 eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
Vector3 impact = new(@event.X, @event.Y, @event.Z);
Vector3 toImpact = impact - eyePos;
if (toImpact.LengthSquared() < 1f)
{
Trace("impact too close to eye");
return;
}
Vector3 direction = Vector3.Normalize(toImpact);
var surface = RayTrace.TraceShape(player, ToVector(eyePos), ToVector(impact + direction * 8f), worldOnlyMask, 0);
if (surface == null)
{
Trace("surface trace returned null");
return;
}
if (!surface.Value.DidHit)
{
Trace($"surface trace missed (fraction={surface.Value.Fraction:0.###})");
return;
}
if (!TryResolveNormal(player, surface.Value, direction, out Vector3 normal))
{
Trace("could not resolve surface normal");
return;
}
Bounce(player, pawn, surface.Value.EndPos, direction, normal);
}
private static void Bounce(CCSPlayerController player, CCSPlayerPawn pawn, Vector3 point, Vector3 direction, Vector3 normal)
{
int bounces = Math.Max(SkillsInfo.GetValue<int>(skillName, "bounces"), 1);
float segmentDistance = SkillsInfo.GetValue<float>(skillName, "segmentDistance");
float damageFalloff = SkillsInfo.GetValue<float>(skillName, "damageFalloff");
var vdata = GetWeaponVData(pawn);
float damage = GetBaseDamage(vdata);
KillfeedIcons? killfeedIcon = KillfeedIconsExtensions.FromWeaponName(SkillUtils.GetDesignerName(pawn.WeaponServices?.ActiveWeapon?.Value));
List<Vector3> path = [];
for (int i = 0; i < bounces; i++)
{
direction = Vector3.Reflect(direction, normal);
Vector3 start = point + normal * 2f;
if (path.Count == 0) path.Add(start);
Vector3 end = start + direction * segmentDistance;
var hit = RayTrace.TraceShape(player, ToVector(start), ToVector(end));
if (hit == null)
{
Trace($"bounce {i + 1}: segment trace returned null");
break;
}
Vector3 stop = hit.Value.DidHit ? hit.Value.EndPos : end;
path.Add(stop);
if (!hit.Value.DidHit)
{
Trace($"bounce {i + 1}: no surface, len={Vector3.Distance(start, stop):0.#}");
break;
}
if (hit.Value.HitPlayer(out var victim))
{
DealDamage(player, victim, damage, vdata, killfeedIcon);
break;
}
if (!TryResolveNormal(player, hit.Value, direction, out normal))
{
Trace($"bounce {i + 1}: could not resolve normal");
break;
}
point = hit.Value.EndPos;
damage *= damageFalloff;
}
StartFlight(player, path);
}
public static void OnTick()
{
lock (flightLock)
{
if (flights.Count == 0) return;
float speed = MathF.Max(SkillsInfo.GetValue<float>(skillName, "tracerSpeed"), 100f);
float trail = MathF.Max(SkillsInfo.GetValue<float>(skillName, "tracerLength"), 16f);
float step = speed / 64f;
int tick = Server.TickCount;
for (int i = flights.Count - 1; i >= 0; i--)
{
var flight = flights[i];
flight.Travelled += step;
if (flight.Travelled - trail >= flight.TotalLength || tick >= flight.DeadlineTick)
{
ReleaseTracer(flight);
flights.RemoveAt(i);
continue;
}
float head = MathF.Min(flight.Travelled, flight.TotalLength);
int segment = SegmentAt(flight, head);
float tail = MathF.Max(head - trail, flight.Cumulative[segment]);
UpdateTracer(flight, PointAt(flight, tail), PointAt(flight, head));
}
}
}
private static void StartFlight(CCSPlayerController player, List<Vector3> path)
{
if (path.Count < 2) return;
if (!SkillsInfo.GetValue<bool>(skillName, "showTracer")) return;
if (EntityManager.OverBudget()) return;
var points = path.ToArray();
var cumulative = new float[points.Length];
for (int i = 1; i < points.Length; i++)
cumulative[i] = cumulative[i - 1] + Vector3.Distance(points[i - 1], points[i]);
float total = cumulative[^1];
if (total < 1f) return;
float speed = MathF.Max(SkillsInfo.GetValue<float>(skillName, "tracerSpeed"), 100f);
lock (flightLock)
{
if (flights.Count >= Math.Max(SkillsInfo.GetValue<int>(skillName, "maxActiveTracers"), 1)) return;
flights.Add(new Flight
{
OwnerIndex = player.Index,
Points = points,
Cumulative = cumulative,
TotalLength = total,
DeadlineTick = Server.TickCount + (int)(total / speed * 64f) + 128,
});
}
}
private static int SegmentAt(Flight flight, float distance)
{
for (int i = flight.Points.Length - 2; i >= 0; i--)
if (distance >= flight.Cumulative[i]) return i;
return 0;
}
private static Vector3 PointAt(Flight flight, float distance)
{
int i = SegmentAt(flight, distance);
float segmentStart = flight.Cumulative[i];
float segmentLength = flight.Cumulative[i + 1] - segmentStart;
if (segmentLength <= .0001f) return flight.Points[i];
float t = Math.Clamp((distance - segmentStart) / segmentLength, 0f, 1f);
return Vector3.Lerp(flight.Points[i], flight.Points[i + 1], t);
}
private static void UpdateTracer(Flight flight, Vector3 tail, Vector3 head)
{
CBeam? beam = null;
if (flight.BeamIndex != null)
{
beam = ResolveTracer(flight.BeamIndex.Value);
if (beam == null)
{
ownedTracers.Remove(flight.BeamIndex.Value);
flight.BeamIndex = null;
}
}
beam ??= AcquireTracer(flight);
if (beam == null) return;
beam.Teleport(ToVector(tail));
beam.EndPos.X = head.X;
beam.EndPos.Y = head.Y;
beam.EndPos.Z = head.Z;
Utilities.SetStateChanged(beam, "CBeam", "m_vecEndPos");
}
private static CBeam? AcquireTracer(Flight flight)
{
float width = SkillsInfo.GetValue<float>(skillName, "tracerWidth");
while (tracerPool.Count > 0)
{
uint pooledIndex = tracerPool[^1];
tracerPool.RemoveAt(tracerPool.Count - 1);
var pooled = ResolveTracer(pooledIndex);
if (pooled == null)
{
ownedTracers.Remove(pooledIndex);
continue;
}
pooled.Width = width;
pooled.EndWidth = width;
pooled.Render = tracerColor;
Utilities.SetStateChanged(pooled, "CBeam", "m_fWidth");
Utilities.SetStateChanged(pooled, "CBeam", "m_fEndWidth");
Utilities.SetStateChanged(pooled, "CBaseModelEntity", "m_clrRender");
flight.BeamIndex = pooledIndex;
return pooled;
}
if (ownedTracers.Count >= Math.Max(SkillsInfo.GetValue<int>(skillName, "maxActiveTracers"), 1)) return null;
if (EntityManager.OverBudget()) return null;
var beam = CreateTracerBeam(flight.Points[0], width);
if (beam == null) return null;
ownedTracers.Add(beam.Index);
flight.BeamIndex = beam.Index;
return beam;
}
private static CBeam? CreateTracerBeam(Vector3 at, float width)
{
try
{
var beam = Utilities.CreateEntityByName<CBeam>("beam");
if (beam == null || !beam.IsValid) return null;
beam.Render = tracerColor;
beam.Width = width;
beam.EndWidth = width;
beam.Teleport(ToVector(at));
beam.EndPos.X = at.X;
beam.EndPos.Y = at.Y;
beam.EndPos.Z = at.Z;
beam.DispatchSpawn();
return beam;
}
catch (Exception ex)
{
Trace($"beam create failed: {ex.Message}");
return null;
}
}
private static void ReleaseTracer(Flight flight)
{
if (flight.BeamIndex == null) return;
uint beamIndex = flight.BeamIndex.Value;
flight.BeamIndex = null;
if (!ownedTracers.Contains(beamIndex)) return;
var beam = ResolveTracer(beamIndex);
if (beam == null)
{
ownedTracers.Remove(beamIndex);
Trace($"tracer {beamIndex} vanished before release");
return;
}
HideTracer(beam);
tracerPool.Add(beamIndex);
}
private static void HideTracer(CBeam beam)
{
beam.Width = 0;
beam.EndWidth = 0;
beam.Render = Color.FromArgb(0, 0, 0, 0);
Utilities.SetStateChanged(beam, "CBeam", "m_fWidth");
Utilities.SetStateChanged(beam, "CBeam", "m_fEndWidth");
Utilities.SetStateChanged(beam, "CBaseModelEntity", "m_clrRender");
if (beam.AbsOrigin == null) return;
beam.EndPos.X = beam.AbsOrigin.X;
beam.EndPos.Y = beam.AbsOrigin.Y;
beam.EndPos.Z = beam.AbsOrigin.Z;
Utilities.SetStateChanged(beam, "CBeam", "m_vecEndPos");
}
private static void ResetPool(bool hideEntities)
{
lock (flightLock)
{
foreach (var flight in flights)
flight.BeamIndex = null;
flights.Clear();
if (hideEntities)
foreach (uint beamIndex in ownedTracers)
{
var beam = ResolveTracer(beamIndex);
if (beam != null) HideTracer(beam);
}
if (ownedTracers.Count > 0)
Trace($"pool reset: {ownedTracers.Count} tracer(s) released");
tracerPool.Clear();
ownedTracers.Clear();
}
}
private static CBeam? ResolveTracer(uint beamIndex)
{
var beam = Utilities.GetEntityFromIndex<CBeam>((int)beamIndex);
if (beam == null || !beam.IsValid) return null;
return beam.DesignerName == "beam" ? beam : null;
}
private static CCSWeaponBaseVData? GetWeaponVData(CCSPlayerPawn pawn)
{
var activeWeapon = pawn.WeaponServices?.ActiveWeapon?.Value;
if (activeWeapon == null || !activeWeapon.IsValid) return null;
try
{
return activeWeapon.As<CCSWeaponBase>().GetVData<CCSWeaponBaseVData>();
}
catch
{
return null;
}
}
private static float GetBaseDamage(CCSWeaponBaseVData? vdata)
{
float fallback = SkillsInfo.GetValue<float>(skillName, "fallbackDamage");
if (vdata == null) return fallback;
float weaponDamage = vdata.Damage;
if (weaponDamage <= 0) return fallback;
return weaponDamage * SkillsInfo.GetValue<float>(skillName, "damageMultiplier");
}
private static void DealDamage(CCSPlayerController attacker, CCSPlayerController? victim, float damage, CCSWeaponBaseVData? vdata, KillfeedIcons? killfeedIcon)
{
if (victim == null || !victim.IsValid) return;
var victimEvent = PlayerManager.GetPlayerEvent(victim);
if (victimEvent == null || !victimEvent.IsValid) return;
if (victimEvent.Index == attacker.Index || victimEvent.Team == attacker.Team) return;
var victimPawn = victimEvent.PlayerPawn?.Value;
if (victimPawn == null || !victimPawn.IsValid || victimPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (vdata != null && victimEvent.PawnArmor > 0 && SkillsInfo.GetValue<bool>(skillName, "respectArmor"))
damage *= vdata.ArmorRatio * .5f;
int finalDamage = (int)MathF.Round(damage);
if (finalDamage <= 0) return;
Trace($"hit {victimEvent.PlayerName} for {finalDamage}");
SkillUtils.TakeHealth(victimPawn, finalDamage, attacker, killfeedIcon ?? KillfeedIcons.Leg);
}
private static bool TryResolveNormal(CCSPlayerController player, CustomTraceResult result, Vector3 direction, out Vector3 normal)
{
normal = Vector3.Zero;
if (!TryRoughNormal(player, result, direction, out Vector3 rough)) return false;
if (TryRefineNormal(player, result.EndPos, direction, rough, out normal)) return true;
Trace("surface refine failed, using rough normal");
normal = rough;
return true;
}
private static bool TryRoughNormal(CCSPlayerController player, CustomTraceResult result, Vector3 direction, out Vector3 rough)
{
rough = result.Normal;
if (rough.LengthSquared() > .0001f)
{
rough = Vector3.Normalize(rough);
if (Vector3.Dot(direction, rough) < -.05f) return true;
}
return TryPlaneFitNormal(player, result.EndPos, direction, out rough);
}
private static bool TryRefineNormal(CCSPlayerController player, Vector3 baseHit, Vector3 direction, Vector3 rough, out Vector3 normal)
{
normal = Vector3.Zero;
Vector3 axis = MathF.Abs(rough.Z) > .9f ? Vector3.UnitX : Vector3.UnitZ;
Vector3 first = Vector3.Cross(rough, axis);
if (first.LengthSquared() < .0001f) return false;
first = Vector3.Normalize(first);
Vector3 second = Vector3.Normalize(Vector3.Cross(rough, first));
float offset = MathF.Max(SkillsInfo.GetValue<float>(skillName, "normalProbeOffset"), 1f);
float tolerance = offset * 2.5f;
Span<Vector3> ring = stackalloc Vector3[4];
int count = 0;
if (SampleSurface(player, baseHit, rough, first * offset, tolerance, out Vector3 firstPlus)) ring[count++] = firstPlus;
if (SampleSurface(player, baseHit, rough, second * offset, tolerance, out Vector3 secondPlus)) ring[count++] = secondPlus;
if (SampleSurface(player, baseHit, rough, first * -offset, tolerance, out Vector3 firstMinus)) ring[count++] = firstMinus;
if (SampleSurface(player, baseHit, rough, second * -offset, tolerance, out Vector3 secondMinus)) ring[count++] = secondMinus;
if (count == 2 && SampleSurface(player, baseHit, rough, Vector3.Zero, tolerance, out Vector3 center))
ring[count++] = center;
if (count < 3) return false;
normal = NewellNormal(ring[..count]);
if (normal.LengthSquared() < .0001f) return false;
normal = Vector3.Normalize(normal);
float dot = Vector3.Dot(direction, normal);
if (MathF.Abs(dot) < .05f) return false;
if (dot > 0f) normal = -normal;
return true;
}
private static bool SampleSurface(CCSPlayerController player, Vector3 baseHit, Vector3 rough, Vector3 tangentOffset, float tolerance, out Vector3 point)
{
point = Vector3.Zero;
Vector3 anchor = baseHit + tangentOffset;
Vector3 start = anchor + rough * 16f;
Vector3 end = anchor - rough * 48f;
var sample = RayTrace.TraceShape(player, ToVector(start), ToVector(end), worldOnlyMask, 0);
if (sample == null || !sample.Value.DidHit) return false;
point = sample.Value.EndPos;
return MathF.Abs(Vector3.Dot(point - baseHit, rough)) <= tolerance;
}
private static Vector3 NewellNormal(ReadOnlySpan<Vector3> points)
{
Vector3 normal = Vector3.Zero;
for (int i = 0; i < points.Length; i++)
{
Vector3 current = points[i];
Vector3 next = points[(i + 1) % points.Length];
normal.X += (current.Y - next.Y) * (current.Z + next.Z);
normal.Y += (current.Z - next.Z) * (current.X + next.X);
normal.Z += (current.X - next.X) * (current.Y + next.Y);
}
return normal;
}
private static bool TryPlaneFitNormal(CCSPlayerController player, Vector3 baseHit, Vector3 direction, out Vector3 normal)
{
normal = Vector3.Zero;
Vector3 axis = MathF.Abs(direction.Z) > .9f ? Vector3.UnitX : Vector3.UnitZ;
Vector3 right = Vector3.Cross(direction, axis);
if (right.LengthSquared() < .0001f) return false;
right = Vector3.Normalize(right);
Vector3 up = Vector3.Normalize(Vector3.Cross(right, direction));
float offset = MathF.Max(SkillsInfo.GetValue<float>(skillName, "normalProbeOffset"), 1f);
float tolerance = offset * 6f;
Vector3 origin = baseHit - direction * 24f;
if (!Probe(player, origin, direction, right * offset, baseHit, tolerance, out Vector3 rightHit)) return false;
if (!Probe(player, origin, direction, right * -offset, baseHit, tolerance, out Vector3 leftHit)) return false;
if (!Probe(player, origin, direction, up * offset, baseHit, tolerance, out Vector3 upHit)) return false;
if (!Probe(player, origin, direction, up * -offset, baseHit, tolerance, out Vector3 downHit)) return false;
normal = Vector3.Cross(rightHit - leftHit, upHit - downHit);
if (normal.LengthSquared() < .0001f) return false;
normal = Vector3.Normalize(normal);
float dot = Vector3.Dot(direction, normal);
if (MathF.Abs(dot) < .05f) return false;
if (dot > 0f) normal = -normal;
return true;
}
private static bool Probe(CCSPlayerController player, Vector3 origin, Vector3 direction, Vector3 offset, Vector3 baseHit, float tolerance, out Vector3 point)
{
point = Vector3.Zero;
Vector3 start = origin + offset;
Vector3 end = start + direction * 96f;
var probe = RayTrace.TraceShape(player, ToVector(start), ToVector(end), worldOnlyMask, 0);
if (probe == null || !probe.Value.DidHit) return false;
point = probe.Value.EndPos;
return Vector3.Distance(point, baseHit) <= tolerance;
}
private static void Trace(string message)
{
Debug.WriteToDebug($"[Ricochet] {message}");
}
private static bool TryConsumeBudget(uint playerIndex)
{
int tick = Server.TickCount;
var budget = impactBudgets.GetOrAdd(playerIndex, _ => new ImpactBudget());
if (budget.Tick != tick)
{
budget.Tick = tick;
budget.Count = 0;
}
if (budget.Count >= Math.Max(SkillsInfo.GetValue<int>(skillName, "maxImpactsPerTick"), 1)) return false;
budget.Count++;
return true;
}
private static Vector ToVector(Vector3 vector) => new(vector.X, vector.Y, vector.Z);
private class ImpactBudget
{
public int Tick { get; set; } = int.MinValue;
public int Count { get; set; }
}
private class Flight
{
public required uint OwnerIndex { get; set; }
public required Vector3[] Points { get; set; }
public required float[] Cumulative { get; set; }
public required float TotalLength { get; set; }
public required int DeadlineTick { get; set; }
public float Travelled { get; set; }
public uint? BeamIndex { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffd75e", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare, int bounces = 3, float segmentDistance = 1200f, float damageMultiplier = .5f, float fallbackDamage = 25f, bool respectArmor = true, float damageFalloff = .75f, int maxImpactsPerTick = 2, bool showTracer = true, float tracerWidth = 0.5f, float tracerSpeed = 2600f, float tracerLength = 220f, int maxActiveTracers = 12, float normalProbeOffset = 6f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public int Bounces { get; set; } = bounces;
public float SegmentDistance { get; set; } = segmentDistance;
public float DamageMultiplier { get; set; } = damageMultiplier;
public float FallbackDamage { get; set; } = fallbackDamage;
public bool RespectArmor { get; set; } = respectArmor;
public float DamageFalloff { get; set; } = damageFalloff;
public int MaxImpactsPerTick { get; set; } = maxImpactsPerTick;
public bool ShowTracer { get; set; } = showTracer;
public float TracerWidth { get; set; } = tracerWidth;
public float TracerSpeed { get; set; } = tracerSpeed;
public float TracerLength { get; set; } = tracerLength;
public int MaxActiveTracers { get; set; } = maxActiveTracers;
public float NormalProbeOffset { get; set; } = normalProbeOffset;
}
}
}

View file

@ -50,6 +50,8 @@ namespace src.player.skills
public static void PlayerMakeSound(UserMessage um)
{
if (SkillPlayerInfo.IsEmpty) return;
var soundevent = um.ReadUInt("soundevent_hash");
if (soundevent != soundEventHash) return;
@ -96,7 +98,11 @@ namespace src.player.skills
public static void OnTick()
{
if (Server.TickCount % (60 * SkillsInfo.GetValue<int>(skillName, "cooldown")) != 0) return;
if (SkillPlayerInfo.IsEmpty) return;
int cooldown = SkillsInfo.GetValue<int>(skillName, "cooldown");
if (cooldown < 1) cooldown = 1;
if (Server.TickCount % (60 * cooldown) != 0) return;
foreach (var player in PlayerManager.GetTickPlayers()
.Where(p => p != null && p.IsValid && p.PlayerPawn.Value != null && p.PlayerPawn.Value.IsValid && p.PlayerPawn.Value.Health > 0))

View file

@ -133,7 +133,10 @@ namespace src.player.skills
pawn.CameraServices.ViewEntity.Raw = defaultCam ? orginalCameraRaw : newCameraRaw;
}
else
{
defaultCam = true;
pawn.CameraServices.ViewEntity.Raw = orginalCameraRaw;
}
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_pCameraServices");

View file

@ -45,7 +45,7 @@ namespace src.player.skills
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo)) continue;
GetTeamate(player, skillInfo);
UpdateHUD(player, skillInfo);
if (SkillUtils.IsHudFrame()) UpdateHUD(player, skillInfo);
}
}

View file

@ -400,14 +400,35 @@ namespace src.player.skills
return (closet, minKnifeInfo);
}
private static readonly List<(uint OwnerIndex, uint GlowEntityIndex)> transmitGlows = [];
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{
var snapshot = knivesInfo.ToArray();
if (knivesInfo.IsEmpty) return;
transmitGlows.Clear();
foreach (var (knifeOwnerIndex, knifeInfo) in knivesInfo)
{
if (knifeInfo.GlowIndex == null) continue;
var glowEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)knifeInfo.GlowIndex);
if (glowEntity == null || !glowEntity.IsValid)
{
knifeInfo.GlowIndex = null;
continue;
}
transmitGlows.Add((knifeOwnerIndex, glowEntity.Index));
}
if (transmitGlows.Count == 0) return;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid) continue;
uint viewerIndex = PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index;
uint? observedPlayerIndex = null;
var playerPawn = player.Pawn?.Value;
if (playerPawn != null && playerPawn.IsValid)
@ -423,23 +444,13 @@ namespace src.player.skills
}
}
foreach ((uint knifeOwnerIndex, KnifeInfo knifeInfo) in snapshot)
foreach (var (knifeOwnerIndex, glowEntityIndex) in transmitGlows)
{
if (knifeInfo.GlowIndex == null) continue;
if (knifeOwnerIndex == viewerIndex) continue;
if (observedPlayerIndex.HasValue && observedPlayerIndex.Value == knifeOwnerIndex) continue;
var glowEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)knifeInfo.GlowIndex);
if (glowEntity == null || !glowEntity.IsValid)
{
knifeInfo.GlowIndex = null;
continue;
}
bool isOwner = knifeOwnerIndex == (PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
bool isObservingOwner = observedPlayerIndex.HasValue && observedPlayerIndex.Value == knifeOwnerIndex;
if (!isOwner && !isObservingOwner)
if (info.TransmitEntities.Contains(glowEntity.Index))
info.TransmitEntities.Remove(glowEntity.Index);
if (info.TransmitEntities.Contains(glowEntityIndex))
info.TransmitEntities.Remove(glowEntityIndex);
}
}
}

View file

@ -93,6 +93,7 @@ namespace src.player.skills
public static void NewRound()
{
buildInProgress = false;
buildScheduled = false;
foreach (var kvp in glows)
{
@ -139,10 +140,21 @@ namespace src.player.skills
Event.EnableTransmit();
playersInAction.TryAdd(player.Index, 0);
if (glows.IsEmpty && !buildInProgress)
SetGlowEffectForAll();
if (buildScheduled || buildInProgress || !glows.IsEmpty) return;
SkillUtils.ForceFullUpdateToAll();
buildScheduled = true;
float delay = SkillUtils.IsFreezeTime()
? Math.Max((float)(Event.GetFreezeTimeEnd() - DateTime.Now).TotalSeconds, 0f)
: 0f;
jRandomSkills.Instance.AddTimer(delay, () =>
{
buildScheduled = false;
if (playersInAction.IsEmpty || buildInProgress || !glows.IsEmpty) return;
SetGlowEffectForAll();
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
public static void DisableSkill(CCSPlayerController player)
@ -158,6 +170,7 @@ namespace src.player.skills
private const int GlowsPerFrame = 4;
private static bool buildInProgress;
private static bool buildScheduled;
private static void SetGlowEffectForAll()
{
@ -180,6 +193,7 @@ namespace src.player.skills
if (start >= enemies.Count)
{
buildInProgress = false;
SkillUtils.ForceFullUpdateToAllChunked();
return;
}