GoalNetVisual.cs

Partial class for FootballWorldPanel that animates the goal net. It tracks ball impacts and replay crossing data, then fills a 2 by 6 by 10 grid of net offsets from FootballNetMotion.Displacement so the panel can draw a deformed net.

Rough Code
using Sandbox;
using Socer;
using System;

public sealed partial class FootballWorldPanel
{
    const int NetRows=6,NetColumns=10;
    readonly float[,,] netOffset=new float[2,NetRows,NetColumns];
    MatchSimulation netMatch;
    int netGoals;
    Point netImpact;
    float netImpactHeight,netImpactSpeed,netImpactAt=-100;
    public float GoalNetPeakDisplacement
    {
        get
        {
            float peak=0;
            foreach(float value in netOffset)peak=Math.Max(peak,Math.Abs(value));
            return peak;
        }
    }
    Point NetPoint(int side,int row,int column)
    {
        float rear=side==0?-2.4f:107.4f;
        float y=PitchPerspective.PostLeft+(PitchPerspective.PostRight-PitchPerspective.PostLeft)*column/(NetColumns-1);
        return new Point(rear+(side==0?-1:1)*netOffset[side,row,column],y);
    }
    float NetHeight(int row)=>PitchPerspective.CrossbarCentre*row/(NetRows-1);
    void UpdateGoalNets(MatchSimulation match)
    {
        InitializeGoalStrokes();
        int goals=match.Goals[0]+match.Goals[1]+match.ShootoutScores[0]+match.ShootoutScores[1];
        if(netMatch!=match)
        {
            netMatch=match;netGoals=Math.Max(0,goals-(match.GoalHoldRemaining>0?1:0));
            netImpactAt=-100;netImpactSpeed=0;
        }
        if(goals>netGoals)
        {
            netImpact=match.Ball;netImpactHeight=match.BallHeight;
            netImpactSpeed=match.BallSpeed/.58f;netImpactAt=match.Seconds;
        }
        netGoals=goals;
        float age=match.GoalHoldRemaining>0?match.GoalCelebrationElapsed:match.Seconds-netImpactAt;
        var replay=Game.Replay;
        // Capture the crossing before the ball settles. Replay time continues
        // after the football clock freezes for the team gathering.
        if(replay.Playing || (match.GoalHoldRemaining>0 && replay.ScorerId==match.GoalScorerId && replay.GoalImpactSpeed>0))
        {
            netImpact=replay.GoalImpactPoint;netImpactHeight=replay.GoalImpactHeight;netImpactSpeed=replay.GoalImpactSpeed;
            if(replay.Playing)age=replay.PlaybackTime-replay.GoalTime;
        }
        Array.Clear(netOffset);
        if(!replay.Playing && match.GoalHoldRemaining>0 && age>=FootballNetMotion.Duration)netImpactSpeed=0;
        if(age<=0 || age>=FootballNetMotion.Duration || netImpactSpeed<=0)return;
        int side=netImpact.X<52.5f?0:1;
        for(int row=1;row<NetRows-1;row++)for(int column=1;column<NetColumns-1;column++)
        {
            float y=PitchPerspective.PostLeft+(PitchPerspective.PostRight-PitchPerspective.PostLeft)*column/(NetColumns-1);
            netOffset[side,row,column]=FootballNetMotion.Displacement(y,NetHeight(row),netImpact.Y,netImpactHeight,age,netImpactSpeed);
        }
    }
}