Partial MatchSimulation that adds a parody media league mode. Seeded low-skill rosters, random pass/shot/dribble/keeper errors, wrong off-ball movement and a 1.5x playback speed, all handled through the normal match pipeline.
using System;
using System.Linq;
namespace Socer;
// A deliberately inept exhibition roster. The ordinary match/rules pipeline
// still moves every ball, resolves contacts and whistles for every exit.
public sealed partial class MatchSimulation
{
public bool IsMediaLeague { get; private set; }
public const float MediaLeaguePlaybackSpeed=1.5f;
public int MediaKeeperMisclears,MediaBadTouches,MediaPanickyPasses,MediaAmbitiousShots;
public int MediaPoorShots,MediaTrackingErrors,MediaTrips,MediaHesitations;
public int MediaMistakes=>MediaKeeperMisclears+MediaBadTouches+MediaPanickyPasses+MediaPoorShots+MediaTrips+MediaHesitations;
Random mediaRandom;
float mediaNextFault;
readonly float[] mediaPlayerFaultAt=new float[22],mediaMovementAt=new float[22],mediaMovementUntil=new float[22],mediaPlanUntil=new float[22];
readonly Point[] mediaMovementTarget=new Point[22];
readonly int[] mediaPlanKind=new int[22],mediaPlanReceiver=new int[22],mediaKeeperFault=new int[22];
static readonly string[] MediaNames={
"Денис «Вынос»", "Илья «Монтаж»", "Макс «Кроссбар»", "Глеб «Контент»", "Паша «Лайк»", "Антон «Без мяча»",
"Рома «Подписка»", "Вова «Не туда»", "Сева «Финт»", "Кирилл «Почти гол»", "Лёша «Сейчас забью»",
"Саша «Надёжный»", "Миша «Повтор»", "Федя «Видеосудья»", "Артём «На опыте»", "Женя «Подкаст»", "Дима «Бровка»",
"Вадим «Технарь»", "Коля «Всё вижу»", "Олег «Турбо»", "Витя «Дальний»", "Серёга «Верняк»"
};
public static MatchSimulation MediaLeagueMatch(int seed)
{
var match=new MatchSimulation(seed){IsMediaLeague=true,HomeName="ФК Контент",AwayName="ФК Хайп",InteractiveSetPieces=false};
match.mediaRandom=new Random(unchecked(seed^0x65A41C3));
match.TeamCohesion[0]=match.TeamCohesion[1]=.04f;
match.SkillSynergiesEnabled[0]=match.SkillSynergiesEnabled[1]=false;
match.Tactics[0]=match.Tactics[1]=1;
match.Approach[0]=1;match.Approach[1]=2;
foreach(var p in match.Players)
{
float quality=.14f+match.MediaNext()*.19f;
p.Skill=quality;p.Pace=4.25f+match.MediaNext()*.8f;p.WeakFootRating=1;
p.Profile=new PlayerProfile {
Id=$"media-{seed}-{p.Id}",Name=MediaNames[p.Id],Style=p.Keeper?"Вратарь":p.Number<=5?"Опорный":"Бегунок",
Overall=18+(int)(quality*40),RoleFamiliarity=.25f,Passing=quality,LongPassing=quality*.7f,
Vision=.12f+match.MediaNext()*.12f,Composure=.08f+match.MediaNext()*.15f,
Finishing=quality*.7f,ShotPower=.25f+match.MediaNext()*.2f,LongShots=.12f,Curve=.1f,
Crossing=.14f,FreeKickAccuracy=.1f,Volleys=.12f,Penalties=.22f,SkillMoves=1,
Defence=.12f+match.MediaNext()*.14f,Interceptions=.15f,StandingTackle=.18f,SlidingTackle=.15f,
Physical=.28f+match.MediaNext()*.18f,HeadingAccuracy=.14f,Jumping=.28f,
Acceleration=.25f+match.MediaNext()*.15f,Endurance=.32f+match.MediaNext()*.18f,
OffBall=.12f+match.MediaNext()*.12f,Aggression=.25f+match.MediaNext()*.2f,
Goalkeeping=.20f,GkDiving=.22f,GkHandling=.15f,GkReflexes=.22f,GkPositioning=.13f,GkKicking=.1f,
SkinTone=p.Id%5,HairTone=(p.Id*7)%6
};
match.mediaPlanReceiver[p.Id]=-1;match.mediaKeeperFault[p.Id]=-1;
}
match.AddEvent("Медиа-лига · футбол ради контента");
return match;
}
float MediaNext()=>(float)mediaRandom.NextDouble();
bool MediaFaultReady(Footballer p)=>IsMediaLeague && Seconds>=mediaNextFault && Seconds>=mediaPlayerFaultAt[p.Id];
void CommitMediaFault(Footballer p)
{
mediaNextFault=Seconds+2.8f+MediaNext()*3.8f;
mediaPlayerFaultAt[p.Id]=Seconds+8+MediaNext()*6;
}
// Commands select the action/recipient normally. The parody changes only
// autonomous decisions and the quality of their physical execution.
bool DecideMediaLeague(Footballer p)
{
if(!IsMediaLeague)return false;
if(Seconds-p.LastSlideAt<2.1f || Seconds<p.ChestUntil || Seconds<p.FeintUntil || Seconds<p.FallUntil || Seconds<p.ReceiveUntil || Seconds<p.HesitateUntil)return true;
if(TryFieldPass(p))return true;
if(p.Keeper)
{
if(p.Held>.85f)BeginKeeperKick(p,ActivePlayers.Where(q=>q.Team==p.Team && !q.Keeper).ToArray());
return true;
}
if(TryFieldShot(p))return true;
if(p.Held<.4f)return true;
if(mediaPlanUntil[p.Id]<=Seconds)
{
float roll=MediaNext();
mediaPlanKind[p.Id]=roll<.51f?1:roll<.73f?2:roll<.82f?3:0;
mediaPlanUntil[p.Id]=Seconds+2.8f+MediaNext()*1.2f;
mediaPlanReceiver[p.Id]=-1;
if(mediaPlanKind[p.Id]==1)
{
Footballer best=null;float value=float.MinValue;
foreach(var q in ActivePlayers)
{
if(q.Team!=p.Team || q.Id==p.Id || q.Keeper)continue;
var offset=q.Position-p.Position;
if(offset.Length<2.7f || offset.Length>34)continue;
// Under pressure they prefer the reassuring backwards or
// sideways option, often overlooking the player in its lane.
float score=MediaNext()*15-Math.Max(0,offset.X*Direction(p.Team))*.25f-offset.Length*.08f;
if(score>value){value=score;best=q;}
}
if(best!=null)mediaPlanReceiver[p.Id]=best.Id;else mediaPlanKind[p.Id]=0;
}
}
int kind=mediaPlanKind[p.Id];
float goalDistance=(World(new Point(105,34),p.Team)-p.Position).Length;
if(kind==1 && mediaPlanReceiver[p.Id]>=0 && !Players[mediaPlanReceiver[p.Id]].SentOff)
{
p.DecisionReason="Медиа-лига · пас от паники";
if(ExecutePlayerPass(p,Players[mediaPlanReceiver[p.Id]],false,CarrierPressure(p)))mediaPlanUntil[p.Id]=0;
return true;
}
if(kind==2 && goalDistance<57 && World(p.Position,p.Team).X>48)
{
p.DecisionReason="Медиа-лига · увидел ворота и поверил в себя";
BeginShot(p,goalDistance);mediaPlanUntil[p.Id]=0;return true;
}
if(kind==3 && MediaFaultReady(p))
{
CommitMediaFault(p);MediaHesitations++;
p.HesitateUntil=Seconds+.65f;p.Decision=.7f;p.Action="Забыл, что хотел сделать";
mediaPlanUntil[p.Id]=0;Emit("miss",p,"А КУДА?",1.2f);return true;
}
p.Action="Медиа-лига · пытается вести";
return true;
}
void ApplyMediaPassError(Footballer p,Footballer receiver,ref Point target,ref float speed)
{
if(!MediaFaultReady(p) || MediaNext()>.64f)return;
CommitMediaFault(p);MediaPanickyPasses++;
var ray=(target-p.Position).Unit;var side=new Point(-ray.Y,ray.X);
// The instructed receiver remains the intended receiver; no forced
// completion, switch of owner or scripted turnover is used.
bool commanded=FieldPasser==p.Id && FieldPassReceiver==receiver.Id;
int mishit=(int)(MediaNext()*3);
if(mishit==0){target+=side*((MediaNext()<.5f?-1:1)*(commanded?3+MediaNext()*4:6+MediaNext()*11));speed*=.9f;}
else if(mishit==1)speed*=.42f+MediaNext()*.2f;
else{target+=ray*(commanded?3:9)+side*((MediaNext()-.5f)*9);speed=Math.Min(32,speed*1.65f);}
Emit("miss",p,mishit==1?"НЕДОДАЛ":mishit==2?"СЛИШКОМ СИЛЬНО":"ОЙ, НЕ ТУДА",1.35f);
}
bool MediaFirstTouch(Footballer p,int passer,float speed,Point incoming)
{
if(!IsMediaLeague || p.Keeper || passer<0 || speed<3 || BallHeight>.95f || !MediaFaultReady(p) || MediaNext()>.38f)return false;
CommitMediaFault(p);MediaBadTouches++;HeavyTouches++;
var ray=incoming.Length>.1f?incoming.Unit:new Point(Direction(p.Team),0);
var side=new Point(-ray.Y,ray.X);
Owner=-1;Passer=-1;LastTouch=p.Team;shot=false;ResetFlight();
Ball=p.ControlPoint;BallVelocity=(ray*.35f+side*((MediaNext()-.5f)*1.6f)).Unit*(4.5f+MediaNext()*4);
VerticalSpeed=.4f+MediaNext()*1.4f;
PassReceiver=p.Id;p.RecoveryChaseUntil=Seconds+2.7f;p.Recovery=.18f;PickupDelay=.12f;
p.Action="Мяч отскочил как от табуретки";Emit("miss",p,"ПРИЁМ… ПОЧТИ",1.3f);
return true;
}
Point MediaMovement(Footballer p,Point ordinary,int possessionTeam)
{
if(!IsMediaLeague || p.Keeper || WindupPlayer==p.Id || DuelDefender==p.Id || Seconds<p.FallUntil ||
Seconds-p.LastSlideAt<2.1f || Seconds<p.InterceptUntil || Seconds<p.RecoveryChaseUntil ||
p.DirectOrder!=DirectOrderKind.None && p.DirectOrderUntil>Seconds ||
p.Team==0 && (Seconds<CoachUntil || Seconds<FieldAttackUntil) ||
Owner==p.Id && (Seconds<p.KickSetupUntil || Seconds<p.ReceiveUntil) ||
Owner<0 && PassReceiver==p.Id)return ordinary;
if(Seconds>=mediaMovementAt[p.Id])
{
mediaMovementAt[p.Id]=Seconds+1.6f+MediaNext()*1.8f;
mediaMovementUntil[p.Id]=0;
float roll=MediaNext();
if(Owner==p.Id)
{
if(roll<.21f)
{mediaMovementTarget[p.Id]=Clamp(p.Position+new Point(-Direction(p.Team)*4,(MediaNext()-.5f)*9));mediaMovementUntil[p.Id]=Seconds+1.1f;MediaTrackingErrors++;}
}
else if(roll<.23f)
{
// They notice the change of possession late and stop to look.
mediaMovementTarget[p.Id]=p.Position;mediaMovementUntil[p.Id]=Seconds+1.0f+MediaNext()*.6f;MediaTrackingErrors++;
}
else if(roll<.78f && (p.Position-Ball).Length<38)
{
float chaseAhead=possessionTeam==p.Team?-2:5;
mediaMovementTarget[p.Id]=Clamp(Ball+new Point(Direction(p.Team)*(chaseAhead+MediaNext()*5),(MediaNext()-.5f)*10));
mediaMovementUntil[p.Id]=Seconds+1.3f+MediaNext()*.7f;MediaTrackingErrors++;
}
}
if(Seconds<mediaMovementUntil[p.Id])
{p.Action=Owner==p.Id?"Побежал не в ту сторону":"Медиа-лига · потерял позицию";return mediaMovementTarget[p.Id];}
return ordinary;
}
void MediaDribbleMistake(Footballer p)
{
if(!MediaFaultReady(p) || Seconds<p.FeintUntil || Seconds<p.ShieldUntil || MediaNext()>.095f)return;
CommitMediaFault(p);mediaPlanUntil[p.Id]=0;
bool trip=MediaNext()<.29f && p.Velocity.Length>1.7f;
var forward=p.Velocity.Length>.1f?p.Velocity.Unit:new Point(Direction(p.Team),0);
BallVelocity=forward*(5.5f+MediaNext()*4)+new Point(-forward.Y,forward.X)*((MediaNext()-.5f)*6);
Owner=-1;Passer=-1;shot=false;flightAge=0;LastTouch=p.Team;
BallHeight=0;VerticalSpeed=.2f+MediaNext()*.6f;p.ChasingTouch=false;PickupDelay=.13f;
if(trip)
{MediaTrips++;FallDown(p,forward,.32f);Emit("miss",p,"ГАЗОН ПОБЕДИЛ",1.5f);}
else
{MediaBadTouches++;DribbleRecoveries++;p.RecoveryChaseUntil=Seconds+2.8f;PassReceiver=p.Id;Emit("miss",p,"ОТПУСТИЛ!",1.3f);}
}
void PlanMediaKeeperKick(Footballer keeper)
{
if(!IsMediaLeague)return;
mediaKeeperFault[keeper.Id]=-1;
if(Seconds<mediaPlayerFaultAt[keeper.Id] || MediaNext()>.77f)return;
int kind=(int)(MediaNext()*3);mediaKeeperFault[keeper.Id]=kind;
var local=World(keeper.Position,keeper.Team);
KeeperKickTarget=World(kind==0?new Point(47+MediaNext()*34,MediaNext()<.5f?-16:84):
kind==1?new Point(114+MediaNext()*10,15+MediaNext()*38):
new Point(local.X+10+MediaNext()*11,Math.Clamp(local.Y+(MediaNext()-.5f)*25,7,61)),keeper.Team);
}
void NoteMediaKeeperRelease(Footballer keeper)
{
if(!IsMediaLeague || mediaKeeperFault[keeper.Id]<0)return;
int kind=mediaKeeperFault[keeper.Id];mediaKeeperFault[keeper.Id]=-1;
mediaPlayerFaultAt[keeper.Id]=Seconds+12;MediaKeeperMisclears++;
string text=kind==0?"выносит куда-то к трибунам":kind==1?"посылает мяч за чужую лицевую":"выносит мяч ровно на чужого нападающего";
AddEvent($"Медиа-лига · {keeper.Profile.Name}: {text}");
Emit("miss",keeper,kind==2?"ВЫНОС… НУ ПОЧТИ":"ДАЛЬНОСТЬ ЕСТЬ",1.8f);
}
void ApplyMediaShotError(Footballer p)
{
if(!IsMediaLeague)return;
float distance=(World(new Point(105,34),p.Team)-p.Position).Length;
if(distance>28)MediaAmbitiousShots++;
if(!MediaFaultReady(p) || MediaNext()>.63f)return;
CommitMediaFault(p);MediaPoorShots++;
if(MediaNext()<.56f)ShotArrivalHeight=3.7f+MediaNext()*4;
else
{
float line=Direction(p.Team)>0?105:0;
float beyond=Math.Abs(ShotTarget.X-p.Position.X)/Math.Max(.2f,Math.Abs(line-p.Position.X));
float wide=34+(MediaNext()<.5f?-1:1)*(7+MediaNext()*10);
ShotTarget.Y=p.Position.Y+(wide-p.Position.Y)*beyond;
}
}
}