FootballWorldPanel.MatchEffects.cs

Partial class for the football world panel. Builds a shared crossed-ribbon mesh and positions particle objects for sprint dust and ball flight trails each frame.

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

public sealed partial class FootballWorldPanel
{
    Model matchWakeModel;
    Model MatchWakeModel
    {
        get
        {
            if(matchWakeModel!=null)return matchWakeModel;
            // Shared, feathered crossed ribbons, built once. Keep the material
            // strongly owned by DynamicGeometryMaterial for the panel lifetime.
            var vertices=new List<Vertex>(72);
            void Quad(Vector3 a,Vector3 b,Vector3 c,Vector3 d,byte edgeA,byte edgeB)
            {
                Vertex V(Vector3 p,byte alpha)=>new(p,new Color32(255,255,255,alpha));
                vertices.Add(V(a,edgeA));vertices.Add(V(b,edgeA));vertices.Add(V(c,edgeB));
                vertices.Add(V(a,edgeA));vertices.Add(V(c,edgeB));vertices.Add(V(d,edgeB));
            }
            for(int plane=0;plane<2;plane++)for(int band=0;band<3;band++)
            {
                float Edge(int b)=>b switch{0=>-.5f,1=>-.16f,2=>.16f,_=>.5f};
                Vector3 P(float x,float y)=>plane==0?new(x,y,0):new(x,0,y);
                float lo=Edge(band),hi=Edge(band+1);
                Quad(P(-.5f,lo),P(.5f,lo),P(.5f,hi),P(-.5f,hi),
                    (byte)(band==0?0:255),(byte)(band==2?0:255));
            }
            var bounds=new BBox(new Vector3(-.51f),new Vector3(.51f));
            var mesh=new Mesh(DynamicGeometryMaterial);
            mesh.CreateVertexBuffer(vertices.Count,vertices);mesh.Bounds=bounds;
            return matchWakeModel=Model.Builder.AddMesh(mesh).WithViewBounds(bounds).Create();
        }
    }

    void DrawMatchEffects(IReadOnlyList<SprintDust.Puff> puffs,float seconds,IReadOnlyList<FlightMark> trail,
        Point ballPosition,float ballHeight,float speed,bool free,UniqueSkill skill,int grade,LegendaryAbility legendary)
    {
        for(int i=0;i<dustObjects.Length;i++)
        {
            var obj=dustObjects[i];
            if(obj==null)continue;
            obj.Enabled=i<puffs.Count;
            if(!obj.Enabled)continue;
            var p=puffs[i];var sample=SprintDust.Sample(p,seconds);
            obj.Enabled=sample.Visible;
            if(!sample.Visible)continue;
            bool contact=p.Kind is SprintDust.ParticleKind.Glove or SprintDust.ParticleKind.Frame;
            var model=contact?MatchWakeModel:sphere;
            if(obj.Model!=model)obj.Model=model;
            SetPart(obj,sample.A,sample.ZA,sample.B,sample.ZB,sample.Width,sample.Color);
            if(contact)obj.Tint=obj.Tint.WithAlpha(sample.Opacity);
            else
            {
                var transform=obj.WorldTransform;var scale=transform.Scale;
                scale.z*=p.Kind==SprintDust.ParticleKind.Grass?.32f:.65f;
                obj.WorldTransform=new Transform(transform.Position,transform.Rotation,scale);
            }
        }
        for(int i=0;i<flightObjects.Length;i++)
        {
            var obj=flightObjects[i];
            if(obj==null)continue;
            // The restrained double filament replaces the ordinary white wake.
            // Authored skill colours remain their own explicitly selected FX.
            if(skill==UniqueSkill.None && legendary==LegendaryAbility.None){obj.Enabled=false;continue;}
            var stroke=FootballMatchEffects.Flight(trail,i,ballPosition,ballHeight,speed,free,skill,grade,legendary);
            obj.Enabled=stroke.Visible;
            if(!stroke.Visible)continue;
            if(obj.Model!=MatchWakeModel)obj.Model=MatchWakeModel;
            SetPart(obj,stroke.A,stroke.ZA,stroke.B,stroke.ZB,stroke.Width,stroke.Color);
            obj.Tint=obj.Tint.WithAlpha(stroke.Opacity);
        }
    }
}