FootballTutorialHighlight.cs

Part of the FootballWorldPanel UI/controller. Draws a pulsing material tint highlight over the 22 player meshes during a match tutorial, caching tinted material copies keyed by the original and restoring overrides afterward. Also exposes a small debug object.

using Sandbox;
using System;
using System.Collections.Generic;

public sealed partial class FootballWorldPanel
{
    // Reuse the real player's meshes. Copies only modulate the material tint; the
    // original palette materials stay immutable and no renderers are added.
    readonly Dictionary<Material,Material> tutorialTintMaterials=new();
    readonly ModelRenderer[] tutorialHighlightedParts=new ModelRenderer[13];
    readonly Material[] tutorialOriginalMaterials=new Material[13];
    int tutorialHighlightedCount;
    int tutorialHighlightedPlayer=-1;
    float tutorialPulse;

    void RestoreTutorialPlayerHighlight()
    {
        for(int i=0;i<tutorialHighlightedCount;i++)
        {
            var part=tutorialHighlightedParts[i];
            if(part?.GameObject?.IsValid==true)part.Materials.SetOverride(0,tutorialOriginalMaterials[i]);
            tutorialHighlightedParts[i]=null;tutorialOriginalMaterials[i]=null;
        }
        tutorialHighlightedCount=0;tutorialHighlightedPlayer=-1;tutorialPulse=0;
    }

    void DrawTutorialPlayerHighlight()
    {
        if(Game?.MatchTutorialNeedsAction!=true || Game.CommandWheelOpen || CallInput?.Dragging==true)return;
        int player=Game.MatchTutorialFocusPlayer;
        if(player<0 || player>=22 || player>=actors.Count || Game.Match.Players[player].SentOff)return;
        float wave=.5f+.5f*MathF.Cos(Time.Now*MathF.PI*2/1.35f);
        tutorialPulse=wave*wave;tutorialHighlightedPlayer=player;
        // Warm brightness pulses through the body and boots. Never disappear
        // the model or flash the whole screen while waiting for an action.
        var tint=new Vector4(1+1.65f*tutorialPulse,1+1.25f*tutorialPulse,1-.2f*tutorialPulse,1);
        foreach(var part in actors[player])
        {
            if(tutorialHighlightedCount>=13)break;
            if(!part.Enabled || part.Materials.Count==0)continue;
            var original=part.Materials.GetOverride(0);
            var source=original??part.Materials.GetOriginal(0);
            if(source==null)continue;
            if(!tutorialTintMaterials.TryGetValue(source,out var highlighted))
            {
                highlighted=source.CreateCopy("football-tutorial-pulse-"+tutorialTintMaterials.Count);
                tutorialTintMaterials[source]=highlighted;
            }
            int slot=tutorialHighlightedCount++;
            tutorialHighlightedParts[slot]=part;tutorialOriginalMaterials[slot]=original;
            highlighted.Set("g_vColorTint",tint);
            part.Materials.SetOverride(0,highlighted);
        }
    }

    public object TutorialHighlightDebug()=>new{Player=tutorialHighlightedPlayer,Pulse=tutorialPulse,
        Parts=tutorialHighlightedCount,CachedMaterials=tutorialTintMaterials.Count};
}