initial commit
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s

This commit is contained in:
Kamal Tufekcic 2026-07-05 13:28:35 +03:00
commit d701598350
67 changed files with 9351 additions and 0 deletions

76
Outnumbered/Feel.cs Normal file
View file

@ -0,0 +1,76 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.Logging;
using Outnumbered.Data;
using Outnumbered.Engine;
namespace Outnumbered;
// "Ability is active" feedback. Two layers, sharing one blended colour:
// 1. HUD recolour (RELIABLE): while any ability is active the whole HUD tints to the blended perk colour.
// 2. Screen tint (EXPERIMENTAL): a faint full-screen "movie filter" via the CS2 fade user message — the exact
// message format is unverified and hard to debug blind, so it's toggleable (Abilities.ScreenTint) and the
// HUD recolour is the guaranteed fallback.
// Per-perk colours come from AbilityDef.Tint{R,G,B}; when several are up the colours are averaged.
public sealed partial class OutnumberedPlugin
{
private readonly Dictionary<int, int> _tintState = new(); // slot -> last packed RGBA applied via fade (0 = none)
private int _feelTick;
private bool _fadeWarned;
// Per-player sound cue via the client `play` command (built-in CS2 sounds; empty = none).
private void PlaySound(CCSPlayerController? p, string path)
{
if (!Config.Sounds.Enabled || string.IsNullOrEmpty(path) || p is not { IsValid: true } || p.IsBot) return;
p.ExecuteClientCommand($"play {path}");
}
private void Initialize_Feel() { /* nothing to initialize — OnTick runs via OnTick_All (shared roster walk); teardown is in Shutdown_Feel */ }
private void Shutdown_Feel()
{
foreach (var p in Utilities.GetPlayers()) // don't leave a stuck tint on reload
if (p is { IsValid: true } && !p.IsBot) SendFade(p, 0, 0, 0, 0, clear: true);
_tintState.Clear();
}
// Averaged colour of the player's currently-active abilities.
private (bool any, int r, int g, int b) BlendActiveTint(PlayerData pd)
{
if (!Config.Abilities.Enabled) return (false, 0, 0, 0);
long r = 0, g = 0, b = 0; int n = 0;
for (int i = 0; i < AbilityCount; i++)
if (AbilityActive(pd, i)) { var d = AbilityCfg(i); r += d.TintR; g += d.TintG; b += d.TintB; n++; }
return n == 0 ? (false, 0, 0, 0) : (true, (int)(r / n), (int)(g / n), (int)(b / n));
}
// ---- screen tint (fade) ----
private void OnTick_Feel(List<CCSPlayerController> players)
{
if (!Config.Abilities.Enabled || !Config.Abilities.ScreenTint) return;
if (++_feelTick % 8 != 0) return; // ~8 Hz; only sends on change anyway
foreach (var p in players)
{
if (!IsHuman(p)) continue; // inline predicate (no Where-iterator alloc on this per-tick path)
var pd = PdOf(p);
if (pd is null) continue;
var (any, r, g, b) = BlendActiveTint(pd);
int a = any ? Math.Clamp(Config.Abilities.TintAlpha, 0, 255) : 0;
int packed = any ? ScreenFade.Pack(r, g, b, a) : 0;
if (!_tintState.TryGetValue(p.Slot, out var last)) last = 0;
if (last == packed) continue;
_tintState[p.Slot] = packed;
if (any) SendFade(p, r, g, b, a, clear: false);
// clear by fading the PREVIOUS colour back out (FFADE_IN needs a colour to fade from) + purge the stayout
else SendFade(p, last & 0xff, (last >> 8) & 0xff, (last >> 16) & 0xff, (last >> 24) & 0xff, clear: true);
}
}
// CS2 screen fade via user message (the message format + send live in Engine.ScreenFade); a wrong field name no-ops
// via onError instead of crashing. The blend/decision logic stays here in OnTick_Feel.
private void SendFade(CCSPlayerController p, int r, int g, int b, int a, bool clear) =>
ScreenFade.Send(p, r, g, b, a, clear,
ex => { if (!_fadeWarned) { _fadeWarned = true; Logger.LogWarning(ex, "Outnumbered fade send failed"); } });
}