Backend Game Logic

From Soccerverse Wiki
Revision as of 18:52, 17 March 2025 by Andrew.gore (talk | contribs) (Updating the player data: Adjusted some details in this section)
Jump to navigation Jump to search

This wiki walks through all of the backend game logic (GSP), starting at the game world's creation and the creation of its first season, all the way through a season's worth of fixtures, the end of season event and then the creation of a new season.

The match day process flow of pre‑match, match and post match is walked through as well as the daily player fitness increases.

Some source code snippets are provided in certain places to help explain the formulas that are used.

When the game world is created

For each club, the average rating of the best 11 players is calculated and stored. This value is immutable and later used for things such as calculating the average player rating of the league, which is also immutable.

It's stored as:

clubs.rating_start

Then the first season is created.

Creating a new season

  • Each club's transfer limits are reset for transfer in and transfer out.
  • Each club's form is reset.
  • Players' form, yellow cards, red cards, yellow/red cards, banned and stamina are all reset.
  • A player's wage for the season is calculated based on his current rating.

If the player rating is below 60 then the wage is calculated using the formula below:

if (rating < 60)
    return (5 * rating) * moneybase_multiplier;

Where moneybase_multiplier is set to be 50,000 when the game world is created.

If the player value is 60 rated or above then it uses the formula below, basically starting at 1500 then increasing by 20% more than the previous rating. It uses the formula below:

AmountT wage = 300;

for(int ic1 = 1; ic1 <= (rating-60); ic1++)
{
    wage = ((wage * 12 ) / 10);
}

return wage * moneybase_multiplier;

A grid of all the wages to ratings can be seen in the table below:

Rating Wage
60 1,500
61 1,800
62 2,160
63 2,592
64 3,110
65 3,732
66 4,479
67 5,375
68 6,450
69 7,740
70 9,288
71 11,145
72 13,374
73 16,049
74 19,259
75 23,111
76 27,733
77 33,279
78 39,935
79 47,922
80 57,506
81 69,008
82 82,809
83 99,371
84 119,245
85 143,094
86 171,713
87 206,056
88 247,267
89 296,720
90 356,064
91 427,277
92 512,733
93 615,279
94 738,335
95 886,002
96 1,063,203
97 1,275,843
98 1,531,012
99 1,837,214
  • Each player's value is calculated and updated at the start of each season.

It uses the formula below:

AmountT players_value = wage * multiplier;

Where multiplier is set to be 75. So the value of a player is 75 weeks of his wages.

If the player is over 22 years old, then for each year of his age he loses 10% of his value compared to the previous year.

This stops when a player reaches 40 years of age, then players aged 40 and above are all valued the same as a 40 year old player. See the formula below:

if(players_age > 22)
{
    if(players_age > 40)
        players_age = 40;
  
    for(int ic1 = 23; ic1 <= (players_age); ic1++)
    {
        players_value = (players_value * 9) / 10;
    }             
}
  • Players' fitness is reset to be between 85–100, using the formula below:
const int fitness = 85 + rand_get_mod(15);

Creating all the competitions

The competitions are created for the domestic, continental and world tournaments.

Each country has a domestic cup and every club in the country is put into the cup. For the first round it is possible that the number of teams in the cup are not a power of 2, which means some clubs will not have an opponent and will need a bye to the next round. See the formula below:

int num_dummy_teams = 0;
if (round == 1)
{
    const int num_teams_needed = (1 << num_rounds);
    num_dummy_teams = num_teams_needed - num_teams;
}

The leagues are created and the correct teams are put into them for each domestic league and the continental group stages.

If it is the first season then the clubs that go into all the domestic leagues, continental groups and the world club cup are pre‑defined and provided in a data file.

If it is not the first season then the clubs that go into the domestic leagues are based upon the previous seasons with any promotion and relegations factored in.

The continental groups will have the winners of all the previous season's domestic leagues put into the group stage. One spot is allocated for each country apart from ENG, GER and ESP, who each get 2 spots.

The world club cup will have the winners of the previous season's four continental competitions placed in it.

Setup the Economies for the Domestic Leagues and the Continental Competitions

The average league attendance for each league and the average player rating of the clubs in the league are both stored. See the formula below:

int ave_league_attendance;
int ave_club_rating_start;

const int ave_league_attendance = league_attendance / num_teams;
const int ave_club_rating_start = league_rating / num_teams;

These values are immutable and all future seasons use the same values.

At the start of the first season, each club starts off with a balance that would be enough to pay half a season's worth of its wages. See the formula below:

int num_gameweek_wages_for_club_balance = (num_rounds * 5000 / 10000;
AmountT start_balance = club_wages * num_gameweek_wages_for_club_balance;

When the first season is created, the following values are calculated for each domestic league and continental competition: ticket cost, TV money and prize money pot. As below:

AmountT ticket_cost;
AmountT tv_money;
AmountT prize_money_pot;

These values are immutable and all future seasons use the same values.

A season's worth of income for each domestic league is calculated by adding up a season's worth of outgoings for all the clubs in the league, which is the league's wage bill.

league_wages += (league_wages * 500) / 10000;   //inflation rate

AmountT weekly_ticket_pot = (league_wages * gate_reciepts_percentage) / 10000;
AmountT weekly_tv_pot = (league_wages * tv_percentage) / 10000;
int weekly_tickets_sold = league_attendance / 2; //halve games played at home

const AmountT ticket_price = weekly_ticket_pot / weekly_tickets_sold;
const AmountT weekly_tv_money = weekly_tv_pot / num_teams;

The income is set to match the outgoings so that by the end of the season if all the clubs' balances were added up, it would be around the same as at the start of a season (assuming no transfers had taken place).

A 5% inflation rate is added to the income so that there is a little extra money for each of the clubs.

The income is split 50/50 between TV revenue and Gate receipts (which is further split into sponsorship, merchandise). The ticket price for the league is calculated based on the number of fans expected to attend all the matches in a season.

The prize money pot for the league, which is distributed at the end of the season, is calculated by taking 12% of a season's worth of the league's wages.

int num_gameweek_wages_for_prize_pot = (num_rounds * 1200) / 10000;    
AmountT season_prize_money_pot = league_wages * num_gameweek_wages_for_prize_pot;

For the continental competitions, the same income calculation is done for each of the group stage leagues. However, the group with the highest TV Revenue, Ticket Price and Prize Money Pot within a continent is then chosen to be used for all the other groups too.

As previously mentioned, these values are all permanently stored and used for all the future seasons. This means that the season's income is always the same for each league and anchored around how the leagues' outgoings were (its wage bill) at the start of the game.

Schedules

The schedule for the game world as a whole will have 45 gameweeks, each game week falling on Saturdays and Wednesdays.

All leagues play their first game at the same time and their last game at the same date. As leagues all have a different number of clubs in them, they also have different numbers of fixtures during a season. Some leagues will have a few Wednesdays off, to spread the matches evenly.

Some gameweeks will have more than one match played in them as there could be cup games, either domestic, continental or world cup on a Monday.

The schedule can be seen here:

Week Date Weekday Competition Match Day
1 15.07.2024 Monday BREAK bye round in code
17.07.2024 Wednesday BREAK bye round in code
20.07.2024 Saturday League 1
2 22.07.2024 Monday Continental 1
24.07.2024 Wednesday League 2
27.07.2024 Saturday League 3
3 29.07.2024 Monday Continental 2
31.07.2024 Wednesday League 4 bye round league
03.08.2024 Saturday League 5
4 05.08.2024 Monday Cup1
07.08.2024 Wednesday League 6 bye round league
10.08.2024 Saturday League 7
5 12.08.2024 Monday Continental 3
14.08.2024 Wednesday League 8 bye round league
17.08.2024 Saturday League 9
6 19.08.2024 Monday Continental 4
21.08.2024 Wednesday League 10 bye round league
24.08.2024 Saturday League 11
7 26.08.2024 Monday Continental 5
28.08.2024 Wednesday League 12 bye round league
31.08.2024 Saturday League 13
8 02.09.2024 Monday Cup2 bye round cup
04.09.2024 Wednesday League 14 bye round league
07.09.2024 Saturday League 15
9 09.09.2024 Monday Continental 6
11.09.2024 Wednesday League 16 bye round league
14.09.2024 Saturday League 17
10 16.09.2024 Monday Continental 7
18.09.2024 Wednesday League 18 bye round league
21.09.2024 Saturday League 19
11 23.09.2024 Monday Cup3 bye round cup
25.09.2024 Wednesday League 20
28.09.2024 Saturday League 21
12 30.09.2024 Monday Continental 8
02.10.2024 Wednesday League 22 bye round league
05.10.2024 Saturday League 23
13 07.10.2024 Monday Cup4 bye round cup
09.10.2024 Wednesday League 24
12.10.2024 Saturday League 25
14 14.10.2024 Monday Continental 9
16.10.2024 Wednesday League 26 bye round league
19.10.2024 Saturday League 27
15 21.10.2024 Monday Cup5
23.10.2024 Wednesday League 28
26.10.2024 Saturday League 29
16 28.10.2024 Monday Continental 10
30.10.2024 Wednesday League 30 bye round league
02.11.2024 Saturday League 31
17 04.11.2024 Monday Cup6 bye round cup
06.11.2024 Wednesday League 32
09.11.2024 Saturday League 33
18 11.11.2024 Monday Club World 1
13.11.2024 Wednesday Club World 2 bye round league
16.11.2024 Saturday League 34
19 18.11.2024 Monday Cup7 bye round cup
20.11.2024 Wednesday League 35
23.11.2024 Saturday League 36
20 25.11.2024 Monday Continental 1/16
27.10.2024 Wednesday League 37
30.11.2024 Saturday League 38
21 02.12.2024 Monday Cup8 bye round cup
04.12.2024 Wednesday League 39
07.12.2024 Saturday League 40
22 09.12.2024 Monday Continental QF
11.12.2024 Wednesday League 41
14.12.2024 Saturday League 42
23 16.12.2024 Monday Continental SF
18.12.2024 Wednesday League 43
21.12.2024 Saturday League 44
24 23.12.2024 Monday Cup9
25.12.2024 Wednesday League 45
28.12.2024 Saturday Continental F
25 30.12.2024 Monday BREAK bye round in code
01.01.2025 Wednesday BREAK bye round in code
04.01.2025 Saturday BREAK
1/26 06.01.2025 Monday BREAK
08.01.2025 Wednesday BREAK
11.01.2025 Saturday League 1

Each season will start on either the second Saturday of January or the second Saturday of July: See the dates for the first 10 seasons:

Season 1:  
    start: 2025-1-11 0:0:0 (1736553600)  
    end:   2025-7-12 0:0:0 (1752278400)  
    first turn: 2025-1-18 6:0:0 (1737180000)  
    last turn:  2025-6-29 0:0:0 (1751155200)  
Season 2:  
    start: 2025-7-12 0:0:0 (1752278400)  
    end:   2026-1-10 0:0:0 (1768003200)  
    first turn: 2025-7-19 6:0:0 (1752904800)  
    last turn:  2025-12-28 0:0:0 (1766880000)  
Season 3:  
    start: 2026-1-10 0:0:0 (1768003200)  
    end:   2026-7-11 0:0:0 (1783728000)  
    first turn: 2026-1-17 6:0:0 (1768629600)  
    last turn:  2026-6-28 0:0:0 (1782604800)  
Season 4:  
    start: 2026-7-11 0:0:0 (1783728000)  
    end:   2027-1-9 0:0:0 (1799452800)  
    first turn: 2026-7-18 6:0:0 (1784354400)  
    last turn:  2026-12-27 0:0:0 (1798329600)  
Season 5:  
    start: 2027-1-9 0:0:0 (1799452800)  
    end:   2027-7-10 0:0:0 (1815177600)  
    first turn: 2027-1-16 6:0:0 (1800079200)  
    last turn:  2027-6-27 0:0:0 (1814054400)  
Season 6:  
    start: 2027-7-10 0:0:0 (1815177600)  
    end:   2028-1-8 0:0:0 (1830902400)  
    first turn: 2027-7-17 6:0:0 (1815804000)  
    last turn:  2027-12-26 0:0:0 (1829779200)  
Season 7:  
    start: 2028-1-8 0:0:0 (1830902400)  
    end:   2028-7-8 0:0:0 (1846627200)  
    first turn: 2028-1-15 6:0:0 (1831528800)  
    last turn:  2028-6-25 0:0:0 (1845504000)  
Season 8:  
    start: 2028-7-8 0:0:0 (1846627200)  
    end:   2029-1-6 0:0:0 (1862352000)  
    first turn: 2028-7-15 6:0:0 (1847253600)  
    last turn:  2028-12-24 0:0:0 (1861228800)  
Season 9:  
    start: 2029-1-6 0:0:0 (1862352000)  
    end:   2029-7-7 0:0:0 (1878076800)  
    first turn: 2029-1-13 6:0:0 (1862978400)  
    last turn:  2029-6-24 0:0:0 (1876953600)  
Season 10:  
    start: 2029-7-7 0:0:0 (1878076800)  
    end:   2030-1-5 0:0:0 (1893801600)  
    first turn: 2029-7-14 6:0:0 (1878703200)  
    last turn:  2029-12-23 0:0:0 (1892678400)

Stadium Building and Fanbase Changes

Under certain circumstances the stadiums will auto build a little bit at the start of each season, aiming to have a stadium capacity target of either the club's fanbase + 30% or the average attendance of the league + 30%, whichever is smaller.

int target_attendance;
if (ave_attendance < fans_current)
    target_attendance = (ave_attendance * 130) / 100;
else
    target_attendance = (fans_current * 130) / 100;

If the current size of the stadium is smaller than the target attendance then seat capacity will be added somewhere between the current size and the target. Note, this will not happen if the number of new seats to be added is less than 200. See the formula below:

int new_size = stadium_size_current;
int increase = 0;

if (stadium_size_current < target_attendance)
{
    const int max_inc = target_attendance - stadium_size_current;
    const int min_inc = max_inc / 2;

    increase = min_inc + rand_get_mod(max_inc - min_inc);

    if (increase > 200)
    {
        new_size = stadium_size_current + increase;
    }
}

For example: This means that all clubs in the lower leagues have the potential to have a stadium size sufficient enough to hold the average number of fans in the top league + 30%, if they got into that league and stayed in there, allowing them to compete on a more even basis.

Under certain circumstances, at the start of each season, the fan base can rise and fall depending on if the club has been promoted or relegated.

If the club is currently in the division that it started in then its fanbase can rise, tending towards either the league's average attendance or the fanbase it started the game with, whichever is greater.

if (club_division == division_start)
{
    //move to league average or start fanbase (whichever is bigger)

    if (fans_start > ave_attendance)
        target_fanbase = fans_start;
    else
        target_fanbase = ave_attendance;
}

If the club is not in the division that it started in, then it has either been promoted or relegated and its target fanbase will change accordingly. See the formula below:

else  //In non-starting league
{
    if (fans_start > ave_attendance)
        target_fanbase = fans_start - (fans_start - ave_attendance) / 2;
    else
        target_fanbase = ave_attendance;

    if (club_division > division_start)
    {
        //got relegated
    }
    else if (club_division < division_start)
    {
        if (target_fanbase < fans_start)
            target_fanbase = fans_start;
    }
}

Each season the fan base will tend from its current fan base towards its target by 50%.

int new_attendance = fans_current;
if (fans_current > target_fanbase)  //shrink it
    new_attendance = fans_current - (fans_current - target_fanbase) / 2;
if (fans_current < target_fanbase)  //grow it
    new_attendance = fans_current + (target_fanbase - fans_current) / 2;

For example: If a club had a fanbase of 1000 and got promoted, its fanbase would slowly rise each season until it was at the average fanbase of the league that it is now in. An English division 10 club with a 1000 fanbase could eventually have a fanbase of over 35,000 if it got into and stayed in the English division 1.

As mentioned before, the stadium would also gradually increase its capacity, alongside the fanbase.

Fitness

On each matchday, fitness is reduced for each player during the match.

It uses the formula below:

reduce_level = (1 * time_played) / 15;
new_fitness = start_fitness - reduce_level - rand_get_mod(6);

Basically if he played 90 minutes he would lose (randomly) between 24 and 29 fitness points, which is on average 26/27 a match. Of course if he played less, say 45 minutes, then he would lose half of that, on average.

Then each day at 3am UTC time, fitness increases with:

new_fitness = fitness + 5 + rand_get_mod(5);

So it goes up between 5 and 9 points each day, which is on average 7 a day.

As an example: If a player's fitness was 100% and he played two league games a week, say Saturday and Wednesday, he would go down 53 points on average (e.g. 26+27) and up 49 a week on average (e.g. plus 7 for 7 days).

Gradually over a season the player will reduce and will need to be rotated. If the club plays in cup games too then they will reduce even faster, so squad rotation is even more important if the club is also aiming to win cups.

Note: A player's fitness will increase more slowly if he is injured but back in the next two weeks. See below:

if (injured > timestamp)
{
    fitness += 3 + rand_get_mod(5);
}

If the player is injured for two weeks or more then his fitness will not increase.

Matchday

All the fixtures that have not yet played are identified and prepared for processing their matchdays.

For each fixture, the tactics and teamsheets for both teams are processed and validated before the match.

If any player is unhappy then they lose 10% of their rating and if a player is injured or banned then he is considered to be a "dud player" and will not take part in the match.

If the match is a continental competition then the ticket price and TV revenue numbers that are specific to that competition are used. All other competitions use the ticket price and TV revenue numbers that the home team uses for its domestic league games. Each domestic league has its own ticket price and TV revenue that all clubs in that league use.

Attendance

The number of fans that turn up for matches are as follows:

Domestic league and cup games will use the home club's fanbase, assuming they fit into the home club's stadium, as the number of fans that will turn up on matchday.

The fanbase does not grow or shrink if you win or lose games; however, after the sixth domestic league game in the season, the attendance for domestic games can increase by up to 30% from its fanbase depending on how high in the table the club is above the middle of the table, and up to 30% less depending on how low the club is from the middle of the table. See the formula below:

//change the attendance based on the club's domestic league position
if (games_played > 6)
{
  int mid_table = num_teams / 2; //Get the league table size and half it.

  //if in top half of table means more fans
  if (new_position < mid_table)
  {
    int units_of_fans = mid_table - new_position;
    c->fans = c->fans + ((c->fans / 30) * units_of_fans);
  }
  else if(new_position > mid_table)  //bottom half of the table means less fans
  {
    int units_of_fans = new_position - mid_table;
    c->fans = c->fans - ((c->fans / 30) * units_of_fans);
  }
}

If it's a domestic cup or shield game then they usually get more fans than a league game, so it looks to use either the fan base (with the 30% modifier if applicable) or 75% of the stadium size, whichever is bigger. See the formula below:

if((comp_type == COMP_TYPE_NATIONAL_CUP) ||
   (comp_type == COMP_TYPE_NATIONAL_SHIELD))
{
  int minimum_fans = (c->stadium_size / 4) * 3;

  if (c->fans < minimum_fans)
  {
    c->fans = minimum_fans - rand_get_mod(100);
  }
}

Continental and world club cup games almost always have full stadium attendances.

Tactics Autopick

If the club has no manager or it has a manager who has submitted invalid tactics, then the autopick will kick in.

Invalid tactics are:

  • Contains at least one player who is injured or match banned.
  • Has no goalkeeper in the goalkeeper position – unless there is a good reason like all the goalkeepers in the squad are injured or banned.

The autopick will randomly choose one of the following formations: 4-4-2, 4-3-3 or 4-5-1 and then find the best possible player for each of the positions in the chosen formation. The player's fitness will also be taken into consideration when choosing the best players for each position.

Player Rating Adjustments

Each player is checked if it is being played in the correct position that it is meant to be played in; for example, a DC in a DC position.

If the player is not in the correct position then he is penalized. The further out of position a player is the more he is penalized. The grid below shows what percentage should be knocked off the player's goalkeeping, tackling, passing and shooting rating, should he be out of position. See:

const int g_grid_outta_pos_multi[16][9][7] = {
{
    {0, 0, 0, 0, 0, 0, 0}, //G
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40},
    {40, 40, 40, 40, 40, 40, 40}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //LB
    {3, 3, 5, 5, 5, 0, 0},
    {3, 3, 5, 5, 5, 0, 0},
    {3, 3, 5, 5, 5, 0, 0},
    {5, 5, 10, 10, 10, 0, 0},
    {10, 15, 15, 15, 15, 10, 5},
    {25, 25, 25, 25, 25, 25, 20},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //CB
    {5, 0, 0, 0, 0, 0, 5},
    {5, 0, 0, 0, 0, 0, 5},
    {10, 10, 5, 5, 5, 10, 10},
    {10, 10, 5, 5, 5, 10, 10},
    {15, 15, 15, 15, 15, 15, 15},
    {25, 25, 25, 25, 25, 25, 25},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //RB
    {0, 0, 5, 5, 5, 3, 3},
    {0, 0, 5, 5, 5, 3, 3},
    {0, 0, 5, 5, 5, 3, 3},
    {0, 0, 10, 10, 10, 5, 5},
    {5, 10, 15, 15, 15, 15, 10},
    {20, 25, 25, 25, 25, 25, 25},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //DML
    {10, 10, 15, 15, 15, 5, 5},
    {10, 10, 15, 15, 15, 5, 5},
    {2, 2, 2, 2, 1, 0, 0},
    {2, 2, 2, 2, 1, 0, 0},
    {3, 3, 3, 3, 2, 0, 0},
    {15, 15, 15, 15, 10, 5, 5},
    {25, 25, 25, 25, 25, 25, 25},
    {25, 25, 25, 25, 25, 25, 25}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //DMC
    {15, 15, 10, 10, 10, 15, 15},
    {15, 15, 10, 10, 10, 15, 15},
    {3, 0, 0, 0, 0, 0, 3},
    {3, 0, 0, 0, 0, 0, 3},
    {3, 0, 0, 0, 0, 0, 3},
    {15, 15, 10, 10, 10, 15, 15},
    {25, 15, 15, 15, 15, 15, 25},
    {25, 25, 25, 25, 25, 25, 25}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //DMR
    {5, 5, 15, 15, 15, 10, 10},
    {5, 5, 15, 15, 15, 10, 10},
    {0, 0, 1, 2, 2, 2, 2},
    {0, 0, 1, 2, 2, 2, 2},
    {0, 0, 2, 3, 3, 3, 3},
    {5, 5, 10, 15, 15, 15, 15},
    {25, 25, 25, 25, 25, 25, 25},
    {25, 25, 25, 25, 25, 25, 25}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //LM
    {15, 15, 15, 15, 15, 15, 15},
    {15, 15, 15, 15, 15, 15, 15},
    {5, 5, 5, 5, 4, 0, 0},
    {3, 3, 3, 3, 2, 0, 0},
    {1, 1, 1, 1, 1, 0, 0},
    {3, 3, 3, 3, 2, 0, 0},
    {10, 10, 10, 10, 10, 10, 10},
    {15, 15, 15, 15, 15, 15, 15}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //CM
    {15, 15, 15, 15, 15, 15, 15},
    {15, 15, 15, 15, 15, 15, 15},
    {5, 5, 1, 1, 1, 5, 5},
    {3, 3, 0, 0, 0, 3, 3},
    {3, 0, 0, 0, 0, 0, 3},
    {3, 1, 0, 0, 0, 1, 3},
    {15, 5, 5, 5, 5, 5, 15},
    {15, 15, 15, 15, 15, 15, 15}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //RM
    {15, 15, 15, 15, 15, 15, 15},
    {15, 15, 15, 15, 15, 15, 15},
    {0, 0, 4, 5, 5, 5, 5},
    {0, 0, 2, 3, 3, 3, 3},
    {0, 0, 1, 1, 1, 1, 1},
    {0, 0, 2, 3, 3, 3, 3},
    {10, 10, 10, 10, 10, 10, 10},
    {15, 15, 15, 15, 15, 15, 15}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //AML
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {25, 25, 35, 25, 25, 25, 25},
    {15, 15, 15, 15, 15, 15, 10},
    {5, 5, 5, 5, 5, 0, 0},
    {5, 5, 3, 3, 1, 0, 0},
    {5, 5, 3, 3, 1, 0, 0},
    {10, 10, 10, 10, 5, 3, 3}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //AM
    {25, 25, 25, 25, 25, 25, 25},
    {25, 25, 25, 25, 25, 25, 25},
    {15, 15, 15, 15, 15, 15, 15},
    {15, 15, 15, 15, 15, 15, 15},
    {3, 0, 0, 0, 0, 0, 3},
    {3, 0, 0, 0, 0, 0, 3},
    {3, 0, 0, 0, 0, 0, 3},
    {10, 10, 10, 10, 10, 10, 10}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //AMR
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {25, 25, 35, 25, 25, 25, 25},
    {10, 15, 15, 15, 15, 15, 15},
    {0, 0, 5, 5, 5, 5, 5},
    {0, 0, 1, 3, 3, 5, 5},
    {0, 0, 1, 3, 3, 5, 5},
    {3, 3, 5, 10, 10, 10, 10}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //FL
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {25, 25, 25, 25, 25, 25, 25},
    {15, 15, 15, 15, 15, 15, 15},
    {5, 5, 5, 5, 5, 5, 5},
    {3, 2, 2, 1, 1, 0, 0},
    {3, 2, 2, 1, 1, 0, 0}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //FC
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {25, 25, 25, 25, 25, 25, 25},
    {15, 15, 15, 15, 15, 15, 15},
    {5, 5, 5, 5, 5, 5, 5},
    {3, 1, 0, 0, 0, 1, 3},
    {3, 1, 0, 0, 0, 1, 3}
},
{
    {40, 40, 40, 40, 40, 40, 40}, //FR
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {35, 35, 35, 35, 35, 35, 35},
    {25, 25, 25, 25, 25, 25, 25},
    {15, 15, 15, 15, 15, 15, 15},
    {5, 5, 5, 5, 5, 5, 5},
    {0, 0, 1, 1, 2, 3, 3},
    {0, 0, 1, 1, 2, 3, 3}
}
};

Player ratings are also adjusted when taking into consideration each player's tempo, tackling style and morale.

The tackling style can be set to be more aggressive, which means they will tackle better but also increase their chances of getting a yellow or red card.

const int tackling_style_adjuster = 2000;

if(tackling_style == 0)
{
    c->ply[ply_off].rating_aggression = c->ply[ply_off].rating_aggression - ((c->ply[ply_off].rating_aggression * tackling_style_adjuster) / 10000);
    if(c->ply[ply_off].rating_aggression < 10)
        c->ply[ply_off].rating_aggression = 10;
}
else if(tackling_style == 2)
{
    c->ply[ply_off].rating_aggression = c->ply[ply_off].rating_aggression + ((c->ply[ply_off].rating_aggression * tackling_style_adjuster) / 10000);
    if(c->ply[ply_off].rating_aggression > 99)
        c->ply[ply_off].rating_aggression = 99;
}

The tempo sets the player's rating and stamina. A higher tempo means that the player plays better although his fitness will drop more and faster. A lower tempo does the opposite on the ratings and stamina.

Morale when it is set to "low" will knock 10% off the player's ratings.

Match Results

After the match has been played, certain calculations are made and stored.

Manager points are awarded; 3 for a win and 1 for a draw.

Gate Receipts

For visual effects, the gate receipts are distributed between gate receipts, sponsorship and merchandise, with the combined total actually going 20% over. This extra 20% is then taken off through ground maintenance. See below:

AmountT gate_receipts = (AmountT)((c[0].fans * c[0].ticket_cost) * 0.8);
const AmountT sponsor = (AmountT)((c[0].fans * c[0].ticket_cost) * 0.3);
const AmountT merchandise = (AmountT)((c[0].fans * c[0].ticket_cost) * 0.1);

const AmountT ground_maintenance = (AmountT)((c[0].fans * c[0].ticket_cost) * 0.2);

For the domestic cup games, continental knockouts and world club cup, the gate receipts are split evenly between the home team and the away team. There is only TV money for the continental knockout games and it is the same amount for both the home and away team.

Payouts

Payouts to users are made for the club/player influencers and the manager and agent wages.

If it was a domestic league game then:

  • 0.2% of the player's wage is paid out between its influencers.
  • 0.002% of the player's wage is paid out to the agent (if the player has one).
  • If the club has a manager that is not locked and is active, then it gets paid a wage of 0.0004% of the club's TV income.
  • If the club is not in debt before the game then 1% of the club's income is paid out between its influencers. This comes out of the club's balance.

If it was a domestic cup, continental game or world club cup then:

  • If the club is not in debt before the game then 1% of the club's income is paid out between its influencers. This comes out of the club's balance.

Bonuses

  • If the player starts the match then a starting lineup bonus of 0.05% of the player's wage is passed onto his influencers. A minimum of 45 minutes need to be played.
  • If the player scored then a goal bonus of 0.1% of the player's wage is passed onto his influencers for each goal scored.
  • If the player assists then an assist bonus of 0.1% of the player's wage is passed onto his influencers for each assist.
  • If there was a clean sheet then the players who played in goals and defence who were in the starting eleven get a clean sheet bonus of 0.1% of the player's wage.

The player needs to have played at least 75 minutes of the game.

Fitness

Fitness is reduced for all players that played in the match. The more minutes they play the more fitness they lose. Goalkeepers lose a lot less, currently four times less, than outfield players. See below:

int reduce_level;
if (ic2 > 0)
    reduce_level = (4 * time_played) / 15;
else
    reduce_level = (1 * time_played) / 15;

fitness = (fitness < 35 ? 1 : fitness - reduce_level - rand_get_mod(6));

Players can get injured for between 2 and 160 days, although it is weighted with more chance towards the lower end. See below:

injury_days = rand_get_mod( rand_get_mod (rand_get_mod (rand_get_mod (159) + 1) + 1) + 1) + 2;

The more days a player has been injured, the more fitness he will lose. See:

if (injury_days > 120)
{
    reduce_level = 40 + rand_get_mod(5);
}
else if (injury_days > 80)
{
    reduce_level += 30 + rand_get_mod(5);
}
else if (injury_days > 40)
{
    reduce_level += 10 + rand_get_mod(5);
}

Cup Final Prize Monday and Payouts

During the season, cup finals are played for domestic, continental and world tournaments that result in prize money for the clubs and payouts for the influence holders.

<how the prize pots are calculated> <what the clubs get> <what the club influencers get> <what the player influencers get> <what manager points are given>

End of Season

When the last fixture of the season has been played, prize money is paid out to each of the clubs in all of the domestic leagues.

Manager points are awarded; 10 for 1st place in a league and 5 for second place.

The first 50% of the league prize money pot is split equally between the clubs.

The other 50% of the total league prize money pot is then split in a linear way with the 1st placed club getting the most and the last placed club getting the least. For example, a league with 20 clubs would have the percentage split as follows: 1st: 9.5%......10th: 5%.......20th: 0.5%. See the formula below:

const int equal = 100 / num_teams;
int percdue = (num_teams - position) * equal;
int potperc = ((percdue << 8) / 100) * (2 * equal);

if (potperc == 0)
{
    percdue = 100 - (num_teams - 1) * equal;
    potperc = ((percdue << 8) / 100) * (2 * equal);
}

AmountT prize_money = (potperc / 100) * prize_money_pot;

prize_money = ((prize_money >> 8));
clubs_amount = (prize_money * 5000) / 10000;

10% of a club's league prize money is split between all club influencers, provided the club isn't in debt.

shareholders_amount = prize_money - clubs_amount;
shareholders_amount = (shareholders_amount * 1000) / 10000;

0.1% of a club's league prize money is for the influencers of each player, discounted with the player's rating.

const AmountT the_prize_share = (prize_money * 5) / 10000;   
AmountT sent_prize_share = (the_prize_share * player_rating) / 100;

The season then remains the current season for a few more days, before a new season is created.


Events Between Seasons

All the other end of season events (such as contracts, auctioning players) is done only at the point when the new season is created and the old season is replaced by it.

A ProcessSeasonEnd script is run

   void ProcessSeasonEnd (DbHandleData& db, const IdT seasonId)

Taking each of the events within it in turn:

Updating the player data

The players database is updated at the end of each season, just before the new season is created.

New players are added, existing players updated and some players retire.

Adding new players:

  • If a player in real life has been playing for a club, then he will get added to the game and to the free bench.

Updating players:

  • Some of the current players will have their rating, position, side and influence price adjusted, all depending on how they have been performing in real life since the last update. Players that didn't have a date of birth set previously might get it added.

Retiring players:

  • When a player retires he leaves his current club and he goes on the freebench, however, he can not be bought by anyone.
  • All transfer auctions involving the player are cancelled.
  • Users are no longer able to buy influence from the game for this player.
  • However, users can trade any existing influence in this player between themselves.
  • Since the player can not play for any club, he will not generate any influence payouts.

All players will also have their value (and wage if on the free bench and not on an active contract) recalculated, as they are now a bit older and potentially worth a little less, and also the ratings will have changed.

Player Contracts

  • Every player who is at a club has his contract reduced by one season.
  • If the contract runs out then the player gets moved to the free bench.
  • However, under certain conditions the contract will get auto-renewed for one season:
    • If the club is not actively managed and allow_renew is set by the agent.
    • If the manager is active, a limit of 2 players is attempted to be the upper limit on how many can leave the club.
    • An attempt is also made to ensure that the squad structure remains in place, e.g. enough goalkeepers. (Players set to "do not renew" by the agent will always leave, though, even if that will exceed these desired limits.)
    • The worst players are released first and a priority is given to try not to go below minimum thresholds for each position.

Clubs in Debt - Auto Auction

If a club is in debt then it will attempt to sell off the club's best player by putting him on the transfer market auction with a minimum bid of zero. If this is not possible, because there are no available players, then nothing happens.

There are three reasons for this:

  • It will reduce the club's wage bill so that its costs are lower in the future.
  • Other clubs may decide to bid and buy the player on the auction system, potentially getting some money into the club.
  • Losing the best player is an incentive to manage the club's finances properly.

If the club already had one of its players up on the transfer market, then it will be cancelled to make way for the best player, as long as there were no bids on it.

Fixing Squad Size

An attempt is made to ensure that a clubs overall squad size is not below the minimum squad size and also that each position has a minimum number of players.

The minimum parameters can be seen below:

const unsigned MIN_TEAM_SIZE = 21;

const std::map<RequiredSquadPosition, unsigned> MIN_TEAM_PER_POS = {
  {RequiredSquadPosition::GK, 2},
  {RequiredSquadPosition::DEFENDER, 5},
  {RequiredSquadPosition::MIDFIELD, 5},
  {RequiredSquadPosition::ATTACKER, 3},
};

The worst players on the freebench are chosen as long as no other club are bidding for them. They are transferred for free to the club.


Players wages and values

  • The values for all players are recalculated, based on possible rating changes and their changes in age for the current season.
  • The wages for all players on the free bench are calculated.

(Players at a club have their wages fixed until a new contract gets signed.)

      const AmountT wage = player_calculate_wage (rating, baseMult);
      const AmountT value
          = player_calculate_base_value (wage, valueMult, startDate, dob);

      if (freeBench)
        {
          stmtUpdBoth.Bind (1, playerId);
          stmtUpdBoth.Bind (2, value);
          stmtUpdBoth.Bind (3, wage);
          stmtUpdBoth.Execute ();
          stmtUpdBoth.Reset ();
        }
      else
        {
          stmtUpdValue.Bind (1, playerId);
          stmtUpdValue.Bind (2, value);
          stmtUpdValue.Execute ();
          stmtUpdValue.Reset ();
        }

Club Dilution Events

  • When a club is in debt at a season end a dilution event to raise SVC for the club is started immediately.
  • When not in debt, a dilution proposal is created, and club influence holders can vote it down. This ensures that lost wallet keys won't make clubs permanently lost, and also helps with clubs in debt.
  • The amount of new influence to be created in an event or proposal is 5%
  • The cost of the new influence created is ?????

A new season is created.