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 _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 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"); } }); }