]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/client/hud/panel/scoreboard.qc
Scoreboard UI: ctrl-c to change default / all / custom columns layout. Other minor...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / client / hud / panel / scoreboard.qc
1 #include "scoreboard.qh"
2
3 #include <client/draw.qh>
4 #include <client/hud/panel/chat.qh>
5 #include <client/hud/panel/physics.qh>
6 #include <client/hud/panel/quickmenu.qh>
7 #include <client/hud/panel/racetimer.qh>
8 #include <client/hud/panel/weapons.qh>
9 #include <common/constants.qh>
10 #include <common/ent_cs.qh>
11 #include <common/mapinfo.qh>
12 #include <common/minigames/cl_minigames.qh>
13 #include <common/net_linked.qh>
14 #include <common/scores.qh>
15 #include <common/stats.qh>
16 #include <common/teams.qh>
17 #include <common/items/inventory.qh>
18
19 // Scoreboard (#24)
20
21 void Scoreboard_Draw_Export(int fh)
22 {
23         // allow saving cvars that aesthetically change the panel into hud skin files
24         HUD_Write_Cvar("hud_panel_scoreboard_fadeinspeed");
25         HUD_Write_Cvar("hud_panel_scoreboard_fadeoutspeed");
26         HUD_Write_Cvar("hud_panel_scoreboard_respawntime_decimals");
27         HUD_Write_Cvar("hud_panel_scoreboard_table_bg_alpha");
28         HUD_Write_Cvar("hud_panel_scoreboard_table_bg_scale");
29         HUD_Write_Cvar("hud_panel_scoreboard_table_fg_alpha");
30         HUD_Write_Cvar("hud_panel_scoreboard_table_fg_alpha_self");
31         HUD_Write_Cvar("hud_panel_scoreboard_table_highlight");
32         HUD_Write_Cvar("hud_panel_scoreboard_table_highlight_alpha");
33         HUD_Write_Cvar("hud_panel_scoreboard_table_highlight_alpha_self");
34         HUD_Write_Cvar("hud_panel_scoreboard_table_highlight_alpha_eliminated");
35         HUD_Write_Cvar("hud_panel_scoreboard_bg_teams_color_team");
36         HUD_Write_Cvar("hud_panel_scoreboard_accuracy_doublerows");
37         HUD_Write_Cvar("hud_panel_scoreboard_accuracy_nocolors");
38         HUD_Write_Cvar("hud_panel_scoreboard_spectators_position");
39 }
40
41 const int MAX_SBT_FIELDS = MAX_SCORE;
42
43 PlayerScoreField sbt_field[MAX_SBT_FIELDS + 1];
44 float sbt_field_size[MAX_SBT_FIELDS + 1];
45 string sbt_field_title[MAX_SBT_FIELDS + 1];
46 int sbt_num_fields;
47
48 string autocvar_hud_fontsize;
49 string hud_fontsize_str;
50 float max_namesize;
51
52 float sbt_bg_alpha;
53 float sbt_fg_alpha;
54 float sbt_fg_alpha_self;
55 bool sbt_highlight;
56 float sbt_highlight_alpha;
57 float sbt_highlight_alpha_self;
58 float sbt_highlight_alpha_eliminated;
59
60 // provide basic panel cvars to old clients
61 // TODO remove them after a future release (0.8.2+)
62 noref string autocvar_hud_panel_scoreboard_pos = "0.150000 0.150000";
63 noref string autocvar_hud_panel_scoreboard_size = "0.700000 0.700000";
64 noref string autocvar_hud_panel_scoreboard_bg = "border_default";
65 noref string autocvar_hud_panel_scoreboard_bg_color = "0 0.3 0.5";
66 noref string autocvar_hud_panel_scoreboard_bg_color_team = "";
67 noref string autocvar_hud_panel_scoreboard_bg_alpha = "0.7";
68 noref string autocvar_hud_panel_scoreboard_bg_border = "";
69 noref string autocvar_hud_panel_scoreboard_bg_padding = "";
70
71 float autocvar_hud_panel_scoreboard_fadeinspeed = 10;
72 float autocvar_hud_panel_scoreboard_fadeoutspeed = 5;
73 float autocvar_hud_panel_scoreboard_respawntime_decimals = 1;
74 float autocvar_hud_panel_scoreboard_table_bg_alpha = 0;
75 float autocvar_hud_panel_scoreboard_table_bg_scale = 0.25;
76 float autocvar_hud_panel_scoreboard_table_fg_alpha = 0.9;
77 float autocvar_hud_panel_scoreboard_table_fg_alpha_self = 1;
78 bool autocvar_hud_panel_scoreboard_table_highlight = true;
79 float autocvar_hud_panel_scoreboard_table_highlight_alpha = 0.2;
80 float autocvar_hud_panel_scoreboard_table_highlight_alpha_self = 0.4;
81 float autocvar_hud_panel_scoreboard_table_highlight_alpha_eliminated = 0.6;
82 float autocvar_hud_panel_scoreboard_bg_teams_color_team = 0;
83 float autocvar_hud_panel_scoreboard_namesize = 15;
84 float autocvar_hud_panel_scoreboard_team_size_position = 0;
85 float autocvar_hud_panel_scoreboard_spectators_position = 1;
86
87 bool autocvar_hud_panel_scoreboard_accuracy = true;
88 bool autocvar_hud_panel_scoreboard_accuracy_doublerows = false;
89 bool autocvar_hud_panel_scoreboard_accuracy_nocolors = false;
90 float autocvar_hud_panel_scoreboard_accuracy_showdelay = 2;
91 float autocvar_hud_panel_scoreboard_accuracy_showdelay_minpos = 0.75;
92
93 bool autocvar_hud_panel_scoreboard_itemstats = true;
94 bool autocvar_hud_panel_scoreboard_itemstats_doublerows = false;
95 int autocvar_hud_panel_scoreboard_itemstats_filter = 1;
96 int autocvar_hud_panel_scoreboard_itemstats_filter_mask = 12;
97 float autocvar_hud_panel_scoreboard_itemstats_showdelay = 2.2; // slightly more delayed than accuracy
98 float autocvar_hud_panel_scoreboard_itemstats_showdelay_minpos = 0.75;
99
100 bool autocvar_hud_panel_scoreboard_dynamichud = false;
101
102 float autocvar_hud_panel_scoreboard_maxheight = 0.6;
103 bool autocvar_hud_panel_scoreboard_others_showscore = true;
104 bool autocvar_hud_panel_scoreboard_spectators_showping = true;
105 bool autocvar_hud_panel_scoreboard_spectators_aligned = false;
106 float autocvar_hud_panel_scoreboard_minwidth = 0.4;
107 bool autocvar_hud_panel_scoreboard_playerid = false;
108 string autocvar_hud_panel_scoreboard_playerid_prefix = "#";
109 string autocvar_hud_panel_scoreboard_playerid_suffix = " ";
110
111 float scoreboard_time;
112
113 // mode 0: returns translated label
114 // mode 1: prints name and description of all the labels
115 string Label_getInfo(string label, int mode)
116 {
117         if (mode == 1)
118                 label = "bckills"; // first case in the switch
119
120         switch(label)
121         {
122                 case "bckills":      if (!mode) return CTX(_("SCO^bckills"));      else LOG_HELP(strcat("^3", "bckills", "            ^7", _("Number of ball carrier kills")));
123                 case "bctime":       if (!mode) return CTX(_("SCO^bctime"));       else LOG_HELP(strcat("^3", "bctime", "             ^7", _("Total amount of time holding the ball in Keepaway")));
124                 case "caps":         if (!mode) return CTX(_("SCO^caps"));         else LOG_HELP(strcat("^3", "caps", "               ^7", _("How often a flag (CTF) or a key (KeyHunt) was captured")));
125                 case "captime":      if (!mode) return CTX(_("SCO^captime"));      else LOG_HELP(strcat("^3", "captime", "            ^7", _("Time of fastest capture (CTF)")));
126                 case "deaths":       if (!mode) return CTX(_("SCO^deaths"));       else LOG_HELP(strcat("^3", "deaths", "             ^7", _("Number of deaths")));
127                 case "destroyed":    if (!mode) return CTX(_("SCO^destroyed"));    else LOG_HELP(strcat("^3", "destroyed", "          ^7", _("Number of keys destroyed by pushing them into void")));
128                 case "dmg":          if (!mode) return CTX(_("SCO^damage"));       else LOG_HELP(strcat("^3", "dmg", "                ^7", _("The total damage done")));
129                 case "dmgtaken":     if (!mode) return CTX(_("SCO^dmgtaken"));     else LOG_HELP(strcat("^3", "dmgtaken", "           ^7", _("The total damage taken")));
130                 case "drops":        if (!mode) return CTX(_("SCO^drops"));        else LOG_HELP(strcat("^3", "drops", "              ^7", _("Number of flag drops")));
131                 case "elo":          if (!mode) return CTX(_("SCO^elo"));          else LOG_HELP(strcat("^3", "elo", "                ^7", _("Player ELO")));
132                 case "fastest":      if (!mode) return CTX(_("SCO^fastest"));      else LOG_HELP(strcat("^3", "fastest", "            ^7", _("Time of fastest lap (Race/CTS)")));
133                 case "faults":       if (!mode) return CTX(_("SCO^faults"));       else LOG_HELP(strcat("^3", "faults", "             ^7", _("Number of faults committed")));
134                 case "fckills":      if (!mode) return CTX(_("SCO^fckills"));      else LOG_HELP(strcat("^3", "fckills", "            ^7", _("Number of flag carrier kills")));
135                 case "fps":          if (!mode) return CTX(_("SCO^fps"));          else LOG_HELP(strcat("^3", "fps", "                ^7", _("FPS")));
136                 case "frags":        if (!mode) return CTX(_("SCO^frags"));        else LOG_HELP(strcat("^3", "frags", "              ^7", _("Number of kills minus suicides")));
137                 case "goals":        if (!mode) return CTX(_("SCO^goals"));        else LOG_HELP(strcat("^3", "goals", "              ^7", _("Number of goals scored")));
138                 case "kckills":      if (!mode) return CTX(_("SCO^kckills"));      else LOG_HELP(strcat("^3", "kckills", "            ^7", _("Number of keys carrier kills")));
139                 case "kd":           if (!mode) return CTX(_("SCO^k/d"));          else LOG_HELP(strcat("^3", "kd", "                 ^7", _("The kill-death ratio")));
140                 case "kdr":          if (!mode) return CTX(_("SCO^kdr"));          else LOG_HELP(strcat("^3", "kdr", "                ^7", _("The kill-death ratio")));
141                 case "kdratio":      if (!mode) return CTX(_("SCO^kdratio"));      else LOG_HELP(strcat("^3", "kdratio", "            ^7", _("The kill-death ratio")));
142                 case "kills":        if (!mode) return CTX(_("SCO^kills"));        else LOG_HELP(strcat("^3", "kills", "              ^7", _("Number of kills")));
143                 case "laps":         if (!mode) return CTX(_("SCO^laps"));         else LOG_HELP(strcat("^3", "laps", "               ^7", _("Number of laps finished (Race/CTS)")));
144                 case "lives":        if (!mode) return CTX(_("SCO^lives"));        else LOG_HELP(strcat("^3", "lives", "              ^7", _("Number of lives (LMS)")));
145                 case "losses":       if (!mode) return CTX(_("SCO^losses"));       else LOG_HELP(strcat("^3", "losses", "             ^7", _("Number of times a key was lost")));
146                 case "name":         if (!mode) return CTX(_("SCO^name"));         else LOG_HELP(strcat("^3", "name", "               ^7", _("Player name")));
147                 case "nick":         if (!mode) return CTX(_("SCO^nick"));         else LOG_HELP(strcat("^3", "nick", "               ^7", _("Player name")));
148                 case "objectives":   if (!mode) return CTX(_("SCO^objectives"));   else LOG_HELP(strcat("^3", "objectives", "         ^7", _("Number of objectives destroyed")));
149                 case "pickups":      if (!mode) return CTX(_("SCO^pickups"));      else LOG_HELP(strcat("^3", "pickups", "            ^7", _("How often a flag (CTF) or a key (KeyHunt) or a ball (Keepaway) was picked up")));
150                 case "ping":         if (!mode) return CTX(_("SCO^ping"));         else LOG_HELP(strcat("^3", "ping", "               ^7", _("Ping time")));
151                 case "pl":           if (!mode) return CTX(_("SCO^pl"));           else LOG_HELP(strcat("^3", "pl", "                 ^7", _("Packet loss")));
152                 case "pushes":       if (!mode) return CTX(_("SCO^pushes"));       else LOG_HELP(strcat("^3", "pushes", "             ^7", _("Number of players pushed into void")));
153                 case "rank":         if (!mode) return CTX(_("SCO^rank"));         else LOG_HELP(strcat("^3", "rank", "               ^7", _("Player rank")));
154                 case "returns":      if (!mode) return CTX(_("SCO^returns"));      else LOG_HELP(strcat("^3", "returns", "            ^7", _("Number of flag returns")));
155                 case "revivals":     if (!mode) return CTX(_("SCO^revivals"));     else LOG_HELP(strcat("^3", "revivals", "           ^7", _("Number of revivals")));
156                 case "rounds":       if (!mode) return CTX(_("SCO^rounds won"));   else LOG_HELP(strcat("^3", "rounds", "             ^7", _("Number of rounds won")));
157                 case "score":        if (!mode) return CTX(_("SCO^score"));        else LOG_HELP(strcat("^3", "score", "              ^7", _("Total score")));
158                 case "suicides":     if (!mode) return CTX(_("SCO^suicides"));     else LOG_HELP(strcat("^3", "suicides", "           ^7", _("Number of suicides")));
159                 case "sum":          if (!mode) return CTX(_("SCO^sum"));          else LOG_HELP(strcat("^3", "sum", "                ^7", _("Number of kills minus deaths")));
160                 case "takes":        if (!mode) return CTX(_("SCO^takes"));        else LOG_HELP(strcat("^3", "takes", "              ^7", _("Number of domination points taken (Domination)")));
161                 case "teamkills":    if (!mode) return CTX(_("SCO^teamkills"));    else LOG_HELP(strcat("^3", "teamkills", "          ^7", _("Number of teamkills")));
162                 case "ticks":        if (!mode) return CTX(_("SCO^ticks"));        else LOG_HELP(strcat("^3", "ticks", "              ^7", _("Number of ticks (Domination)")));
163                 case "time":         if (!mode) return CTX(_("SCO^time"));         else LOG_HELP(strcat("^3", "time", "               ^7", _("Total time raced (Race/CTS)")));
164                 default: return label;
165         }
166         return label;
167 }
168
169 void HUD_Scoreboard_UI_Disable()
170 {
171         scoreboard_showscores = false;
172         scoreboard_ui_enabled = 0;
173         scoreboard_selected_panel = 0;
174         scoreboard_selected_player = NULL;
175         scoreboard_selected_team = NULL;
176         scoreboard_ui_disabled_time = time;
177 }
178
179 // mode: 0 normal, 1 team selection
180 void Scoreboard_UI_Enable(int mode)
181 {
182         if (mode == 1)
183         {
184                 if (scoreboard_ui_enabled == 2 || !teamplay)
185                         return;
186                 // Do not allow reopening the team selection for some time after it has been closed.
187                 // It works around a bug caused by the server sending scoreboard_team_selection every frame
188                 // until client selects a team and joins: scoreboard_team_selection gets executed even
189                 // after client already joined
190                 // For the record, with the menu dialog this workaround is not needed because it takes
191                 // some time to fade away
192                 if (time < scoreboard_ui_disabled_time + 0.5)
193                         return;
194                 scoreboard_ui_enabled = 2;
195                 scoreboard_selected_panel = SB_PANEL_SCOREBOARD;
196         }
197         else
198         {
199                 if (scoreboard_ui_enabled == 1)
200                         return;
201                 scoreboard_ui_enabled = 1;
202                 scoreboard_selected_panel = SB_PANEL_FIRST;
203         }
204         scoreboard_selected_player = NULL;
205         scoreboard_selected_team = NULL;
206         scoreboard_selected_panel_time = time;
207 }
208
209 int rankings_start_column;
210 int rankings_rows = 0;
211 int rankings_columns = 0;
212 int rankings_cnt = 0;
213 float HUD_Scoreboard_InputEvent(float bInputType, float nPrimary, float nSecondary)
214 {
215         string s;
216
217         if(!scoreboard_ui_enabled)
218                 return false;
219
220         if(bInputType == 3)
221         {
222                 mousepos.x = nPrimary;
223                 mousepos.y = nSecondary;
224                 return true;
225         }
226
227         if(bInputType == 2)
228                 return false;
229
230         // at this point bInputType can only be 0 or 1 (key pressed or released)
231         bool key_pressed = (bInputType == 0);
232
233         // ESC to exit (TAB-ESC works too)
234         if(nPrimary == K_ESCAPE)
235         {
236                 if (!key_pressed)
237                         return true;
238                 HUD_Scoreboard_UI_Disable();
239                 return true;
240         }
241
242         // block any input while a menu dialog is fading
243         if(autocvar__menu_alpha)
244         {
245                 hudShiftState = 0;
246                 return true;
247         }
248
249         // allow console bind to work
250         string con_keys = findkeysforcommand("toggleconsole", 0);
251         int keys = tokenize(con_keys); // findkeysforcommand returns data for this
252
253         bool hit_con_bind = false;
254         int i;
255         for (i = 0; i < keys; ++i)
256         {
257                 if(nPrimary == stof(argv(i)))
258                         hit_con_bind = true;
259         }
260
261         if(key_pressed) {
262                 if(nPrimary == K_ALT) hudShiftState |= S_ALT;
263                 if(nPrimary == K_CTRL) hudShiftState |= S_CTRL;
264                 if(nPrimary == K_SHIFT) hudShiftState |= S_SHIFT;
265                 if(nPrimary == K_TAB) hudShiftState |= S_TAB;
266         }
267         else {
268                 if(nPrimary == K_ALT) hudShiftState -= (hudShiftState & S_ALT);
269                 if(nPrimary == K_CTRL) hudShiftState -= (hudShiftState & S_CTRL);
270                 if(nPrimary == K_SHIFT) hudShiftState -= (hudShiftState & S_SHIFT);
271                 if(nPrimary == K_TAB) hudShiftState -= (hudShiftState & S_TAB);
272         }
273
274         if(nPrimary == K_TAB)
275         {
276                 if (!key_pressed)
277                         return true;
278                 if (scoreboard_ui_enabled == 2)
279                         goto downarrow_action;
280
281                 ++scoreboard_selected_panel;
282                 if (scoreboard_selected_panel == SB_PANEL_RANKINGS && !rankings_cnt)
283                         ++scoreboard_selected_panel;
284                 if (scoreboard_selected_panel >= SB_PANEL_MAX)
285                         scoreboard_selected_panel = 1;
286
287                 scoreboard_selected_panel_time = time;
288         }
289         else if(nPrimary == K_DOWNARROW)
290         {
291                 if (!key_pressed)
292                         return true;
293                 LABEL(downarrow_action);
294                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
295                 {
296                         if (scoreboard_ui_enabled == 2)
297                         {
298                                 entity curr_team = NULL;
299                                 bool scoreboard_selected_team_found = false;
300                                 if (!scoreboard_selected_team)
301                                         scoreboard_selected_team_found = true;
302
303                                 for(entity tm = teams.sort_next; tm; tm = tm.sort_next)
304                                 {
305                                         if(tm.team == NUM_SPECTATOR)
306                                                 continue;
307                                         curr_team = tm;
308                                         if (scoreboard_selected_team_found)
309                                                 goto ok_team;
310                                         if (scoreboard_selected_team == tm)
311                                                 scoreboard_selected_team_found = true;
312                                 }
313                                 LABEL(ok_team);
314                                 if (curr_team == scoreboard_selected_team) // loop reached the last team
315                                         curr_team = NULL;
316                                 scoreboard_selected_team = curr_team;
317                         }
318                         else
319                         {
320                                 entity pl, tm;
321                                 entity curr_pl = NULL;
322                                 bool scoreboard_selected_player_found = false;
323                                 if (!scoreboard_selected_player)
324                                         scoreboard_selected_player_found = true;
325
326                                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
327                                 {
328                                         if(tm.team != NUM_SPECTATOR)
329                                         for(pl = players.sort_next; pl; pl = pl.sort_next)
330                                         {
331                                                 if(pl.team != tm.team)
332                                                         continue;
333                                                 curr_pl = pl;
334                                                 if (scoreboard_selected_player_found)
335                                                         goto ok_done;
336                                                 if (scoreboard_selected_player == pl)
337                                                         scoreboard_selected_player_found = true;
338                                         }
339                                 }
340                                 LABEL(ok_done);
341                                 if (curr_pl == scoreboard_selected_player) // loop reached the last player
342                                         curr_pl = NULL;
343                                 scoreboard_selected_player = curr_pl;
344                         }
345                 }
346         }
347         else if(nPrimary == K_UPARROW)
348         {
349                 if (!key_pressed)
350                         return true;
351                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
352                 {
353                         if (scoreboard_ui_enabled == 2)
354                         {
355                                 entity prev_team = NULL;
356                                 for(entity tm = teams.sort_next; tm; tm = tm.sort_next)
357                                 {
358                                         if(tm.team == NUM_SPECTATOR)
359                                                 continue;
360                                         if (tm == scoreboard_selected_team)
361                                                 goto ok_team2;
362                                         prev_team = tm;
363                                 }
364                                 LABEL(ok_team2);
365                                 scoreboard_selected_team = prev_team;
366                         }
367                         else
368                         {
369                                 entity prev_pl = NULL;
370                                 entity pl, tm;
371                                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
372                                 {
373                                         if(tm.team != NUM_SPECTATOR)
374                                         for(pl = players.sort_next; pl; pl = pl.sort_next)
375                                         {
376                                                 if(pl.team != tm.team)
377                                                         continue;
378                                                 if (pl == scoreboard_selected_player)
379                                                         goto ok_done2;
380                                                 prev_pl = pl;
381                                         }
382                                 }
383                                 LABEL(ok_done2);
384                                 scoreboard_selected_player = prev_pl;
385                         }
386                 }
387         }
388         else if(nPrimary == K_RIGHTARROW)
389         {
390                 if (!key_pressed)
391                         return true;
392                 if (scoreboard_selected_panel == SB_PANEL_RANKINGS)
393                         rankings_start_column = min(rankings_start_column + 1, (ceil(RANKINGS_RECEIVED_CNT / rankings_rows) - rankings_columns));
394         }
395         else if(nPrimary == K_LEFTARROW)
396         {
397                 if (!key_pressed)
398                         return true;
399                 if (scoreboard_selected_panel == SB_PANEL_RANKINGS)
400                         rankings_start_column = max(rankings_start_column - 1, 0);
401         }
402         else if(nPrimary == K_ENTER || nPrimary == K_SPACE || nPrimary == K_KP_ENTER)
403         {
404                 if (!key_pressed)
405                         return true;
406                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
407                 {
408                         if (scoreboard_ui_enabled == 2)
409                         {
410                                 string team_name;
411                                 if (!scoreboard_selected_team || (hudShiftState & S_SHIFT))
412                                         team_name = "auto";
413                                 else
414                                         team_name = Static_Team_ColorName(scoreboard_selected_team.team);
415                                 localcmd(sprintf("cmd selectteam %s; cmd join\n", team_name));
416                                 HUD_Scoreboard_UI_Disable();
417                         }
418                         else if (!scoreboard_selected_player || (hudShiftState & S_SHIFT))
419                         {
420                                 localcmd("join\n");
421                                 HUD_Scoreboard_UI_Disable();
422                         }
423                         else
424                                 localcmd(sprintf("spectate %d\n", scoreboard_selected_player.sv_entnum + 1));
425                 }
426         }
427         else if(nPrimary == 'c' && (hudShiftState & S_CTRL))
428         {
429                 if (!key_pressed)
430                         return true;
431                 if (scoreboard_ui_enabled == 1 && scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
432                 {
433                         switch (scoreboard_selected_columns_layout)
434                         {
435                                 case 0:
436                                         if (autocvar_scoreboard_columns != "" && autocvar_scoreboard_columns != "all" && autocvar_scoreboard_columns != "default")
437                                         {
438                                                 localcmd(sprintf("scoreboard_columns_set\n")); // sets the layout saved in scoreboard_columns
439                                                 scoreboard_selected_columns_layout = 1;
440                                                 break;
441                                         }
442                                         // fallthrough
443                                 case 1:
444                                         localcmd(sprintf("scoreboard_columns_set default\n"));
445                                         scoreboard_selected_columns_layout = 2;
446                                         break;
447                                 case 2:
448                                         localcmd(sprintf("scoreboard_columns_set all\n"));
449                                         scoreboard_selected_columns_layout = 0;
450                                         break;
451                         }
452                 }
453         }
454         else if(nPrimary == 't' && (hudShiftState & S_CTRL))
455         {
456                 if (!key_pressed)
457                         return true;
458                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
459                 {
460                         if (scoreboard_selected_player)
461                         {
462                                 localcmd(sprintf("commandmode tell \"%s^7\"\n", entcs_GetName(scoreboard_selected_player.sv_entnum)));
463                                 HUD_Scoreboard_UI_Disable();
464                         }
465                 }
466         }
467         else if(nPrimary == 'k' && (hudShiftState & S_CTRL))
468         {
469                 if (!key_pressed)
470                         return true;
471                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
472                 {
473                         if (scoreboard_selected_player)
474                                 localcmd(sprintf("vcall kick \"%s^7\"\n", entcs_GetName(scoreboard_selected_player.sv_entnum)));
475                 }
476         }
477         else if(hit_con_bind || nPrimary == K_PAUSE)
478                 return false;
479
480         return true;
481 }
482
483 void PrintScoresLabels() { Label_getInfo(string_null, 1); }
484 string TranslateScoresLabel(string label) { return Label_getInfo(label, 0); }
485
486 #define SB_EXTRA_SORTING_FIELDS 5
487 PlayerScoreField sb_extra_sorting_field[SB_EXTRA_SORTING_FIELDS];
488 void Scoreboard_InitScores()
489 {
490         int i, f;
491
492         ps_primary = ps_secondary = NULL;
493         ts_primary = ts_secondary = -1;
494         FOREACH(Scores, true, {
495                 f = (scores_flags(it) & SFL_SORT_PRIO_MASK);
496                 if(f == SFL_SORT_PRIO_PRIMARY)
497                         ps_primary = it;
498                 if(f == SFL_SORT_PRIO_SECONDARY)
499                         ps_secondary = it;
500                 if(ps_primary == it || ps_secondary == it)
501                         continue;
502                 if (scores_label(it) == "kills")      sb_extra_sorting_field[0] = it;
503                 if (scores_label(it) == "deaths")     sb_extra_sorting_field[1] = it;
504                 if (scores_label(it) == "suicides")   sb_extra_sorting_field[2] = it;
505                 if (scores_label(it) == "dmg")        sb_extra_sorting_field[3] = it;
506                 if (scores_label(it) == "dmgtaken")   sb_extra_sorting_field[4] = it;
507         });
508         if(ps_secondary == NULL)
509                 ps_secondary = ps_primary;
510
511         for(i = 0; i < MAX_TEAMSCORE; ++i)
512         {
513                 f = (teamscores_flags(i) & SFL_SORT_PRIO_MASK);
514                 if(f == SFL_SORT_PRIO_PRIMARY)
515                         ts_primary = i;
516                 if(f == SFL_SORT_PRIO_SECONDARY)
517                         ts_secondary = i;
518         }
519         if(ts_secondary == -1)
520                 ts_secondary = ts_primary;
521
522         Cmd_Scoreboard_SetFields(0);
523 }
524
525 //float lastpnum;
526 void Scoreboard_UpdatePlayerTeams()
527 {
528         static float update_time;
529         if (time <= update_time)
530                 return;
531         update_time = time;
532
533         entity pl, tmp;
534         //int num = 0;
535         for(pl = players.sort_next; pl; pl = pl.sort_next)
536         {
537                 //num += 1;
538                 int Team = entcs_GetScoreTeam(pl.sv_entnum);
539                 if(SetTeam(pl, Team))
540                 {
541                         tmp = pl.sort_prev;
542                         Scoreboard_UpdatePlayerPos(pl);
543                         if(tmp)
544                                 pl = tmp;
545                         else
546                                 pl = players.sort_next;
547                 }
548         }
549         /*
550         if(num != lastpnum)
551                 print(strcat("PNUM: ", ftos(num), "\n"));
552         lastpnum = num;
553         */
554 }
555
556 int Scoreboard_CompareScore(int vl, int vr, int f)
557 {
558         TC(int, vl); TC(int, vr); TC(int, f);
559         if(f & SFL_ZERO_IS_WORST)
560         {
561                 if(vl == 0 && vr != 0)
562                         return 1;
563                 if(vl != 0 && vr == 0)
564                         return 0;
565         }
566         if(vl > vr)
567                 return IS_INCREASING(f);
568         if(vl < vr)
569                 return IS_DECREASING(f);
570         return -1;
571 }
572
573 float Scoreboard_ComparePlayerScores(entity left, entity right)
574 {
575         int vl = (left.gotscores) ? entcs_GetTeam(left.sv_entnum) : NUM_SPECTATOR;
576         int vr = (right.gotscores) ? entcs_GetTeam(right.sv_entnum) : NUM_SPECTATOR;
577
578         if(vl > vr)
579                 return true;
580         if(vl < vr)
581                 return false;
582
583         if(vl == NUM_SPECTATOR)
584         {
585                 // FIRST the one with scores (spectators), THEN the ones without (downloaders)
586                 // no other sorting
587                 if(!left.gotscores && right.gotscores)
588                         return true;
589                 return false;
590         }
591
592         entity fld = NULL;
593         int r;
594         for (int i = -2; i < SB_EXTRA_SORTING_FIELDS; ++i)
595         {
596                 if (i < 0)
597                 {
598                         if (!fld) fld = ps_primary;
599                         else if (ps_secondary == ps_primary) continue;
600                         else fld = ps_secondary;
601                 }
602                 else
603                 {
604                         fld = sb_extra_sorting_field[i];
605                         if (fld == ps_primary || fld == ps_secondary) continue;
606                 }
607                 if (!fld) continue;
608
609                 r = Scoreboard_CompareScore(left.scores(fld), right.scores(fld), scores_flags(fld));
610                 if (r >= 0) return r;
611         }
612
613         if (left.sv_entnum < right.sv_entnum)
614                 return true;
615
616         return false;
617 }
618
619 void Scoreboard_UpdatePlayerPos(entity player)
620 {
621         entity ent;
622         for(ent = player.sort_next; ent && Scoreboard_ComparePlayerScores(player, ent); ent = player.sort_next)
623         {
624                 SORT_SWAP(player, ent);
625         }
626         for(ent = player.sort_prev; ent != players && Scoreboard_ComparePlayerScores(ent, player); ent = player.sort_prev)
627         {
628                 SORT_SWAP(ent, player);
629         }
630 }
631
632 float Scoreboard_CompareTeamScores(entity left, entity right)
633 {
634         if(left.team == NUM_SPECTATOR)
635                 return 1;
636         if(right.team == NUM_SPECTATOR)
637                 return 0;
638
639         int fld_idx = -1;
640         int r;
641         for(int i = -2; i < MAX_TEAMSCORE; ++i)
642         {
643                 if (i < 0)
644                 {
645                         if (fld_idx == -1) fld_idx = ts_primary;
646                         else if (ts_secondary == ts_primary) continue;
647                         else fld_idx = ts_secondary;
648                 }
649                 else
650                 {
651                         fld_idx = i;
652                         if (fld_idx == ts_primary || fld_idx == ts_secondary) continue;
653                 }
654
655                 r = Scoreboard_CompareScore(left.teamscores(fld_idx), right.teamscores(fld_idx), teamscores_flags(fld_idx));
656                 if (r >= 0) return r;
657         }
658
659         if (left.team < right.team)
660                 return true;
661
662         return false;
663 }
664
665 void Scoreboard_UpdateTeamPos(entity Team)
666 {
667         entity ent;
668         for(ent = Team.sort_next; ent && Scoreboard_CompareTeamScores(Team, ent); ent = Team.sort_next)
669         {
670                 SORT_SWAP(Team, ent);
671         }
672         for(ent = Team.sort_prev; ent != teams && Scoreboard_CompareTeamScores(ent, Team); ent = Team.sort_prev)
673         {
674                 SORT_SWAP(ent, Team);
675         }
676 }
677
678 void Cmd_Scoreboard_Help()
679 {
680         LOG_HELP(_("You can modify the scoreboard using the ^2scoreboard_columns_set command."));
681         LOG_HELP(_("Usage:"));
682         LOG_HELP("^2scoreboard_columns_set ^3default");
683         LOG_HELP(_("^2scoreboard_columns_set ^3field1 field2 ..."));
684         LOG_HELP(_("^2scoreboard_columns_set ^7without arguments reads the arguments from the cvar scoreboard_columns"));
685         LOG_HELP(_("  ^5Note: ^7scoreboard_columns_set without arguments is executed on every map start"));
686         LOG_HELP(_("^2scoreboard_columns_set ^3expand_default ^7loads default layout and expands it into the cvar scoreboard_columns so you can edit it"));
687         LOG_HELP(_("You can use a ^3|^7 to start the right-aligned fields."));
688         LOG_HELP(_("The following field names are recognized (case insensitive):"));
689         LOG_HELP("");
690
691         PrintScoresLabels();
692         LOG_HELP("");
693
694         LOG_HELP(_("Before a field you can put a + or - sign, then a comma separated list\n"
695                 "of game types, then a slash, to make the field show up only in these\n"
696                 "or in all but these game types. You can also specify 'all' as a\n"
697                 "field to show all fields available for the current game mode."));
698         LOG_HELP("");
699
700         LOG_HELP(_("The special game type names 'teams' and 'noteams' can be used to\n"
701                 "include/exclude ALL teams/noteams game modes."));
702         LOG_HELP("");
703
704         LOG_HELP(_("Example: scoreboard_columns_set name ping pl | +ctf/field3 -dm/field4"));
705         LOG_HELP(_("will display name, ping and pl aligned to the left, and the fields\n"
706                 "right of the vertical bar aligned to the right."));
707         LOG_HELP(_("'field3' will only be shown in CTF, and 'field4' will be shown in all\n"
708                         "other gamemodes except DM."));
709 }
710
711 // NOTE: adding a gametype with ? to not warn for an optional field
712 // make sure it's excluded in a previous exclusive rule, if any
713 // otherwise the previous exclusive rule warns anyway
714 // e.g. -teams,rc,cts,lms/kills ?+rc/kills
715 #define SCOREBOARD_DEFAULT_COLUMNS \
716 "ping pl fps name |" \
717 " -teams,rc,cts,inv,lms/kills +ft,tdm/kills ?+rc,inv/kills" \
718 " -teams,lms/deaths +ft,tdm/deaths" \
719 " +tdm/sum" \
720 " -teams,lms,rc,cts,inv,ka/suicides +ft,tdm/suicides ?+rc,inv/suicides" \
721 " -cts,dm,tdm,ka,ft/frags" /* tdm already has this in "score" */ \
722 " +tdm,ft,dom,ons,as/teamkills"\
723 " -rc,cts,nb/dmg -rc,cts,nb/dmgtaken" \
724 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
725 " +lms/lives +lms/rank" \
726 " +kh/kckills +kh/losses +kh/caps" \
727 " ?+rc/laps ?+rc/time +rc,cts/fastest" \
728 " +as/objectives +nb/faults +nb/goals" \
729 " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
730 " +dom/ticks +dom/takes" \
731 " -lms,rc,cts,inv,nb/score"
732
733 void Cmd_Scoreboard_SetFields(int argc)
734 {
735         TC(int, argc);
736         int i, slash;
737         string str, pattern;
738         bool have_name = false, have_primary = false, have_secondary = false, have_separator = false;
739         int missing;
740
741         if(!gametype)
742                 return; // do nothing, we don't know gametype and scores yet
743
744         // sbt_fields uses strunzone on the titles!
745         if(!sbt_field_title[0])
746                 for(i = 0; i < MAX_SBT_FIELDS; ++i)
747                         sbt_field_title[i] = strzone("(null)");
748
749         // TODO: re enable with gametype dependant cvars?
750         if(argc < 3) // no arguments provided
751                 argc = tokenizebyseparator(strcat("0 1 ", autocvar_scoreboard_columns), " ");
752
753         if(argc < 3)
754                 argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
755
756         if(argc == 3)
757         {
758                 if(argv(2) == "default" || argv(2) == "expand_default")
759                 {
760                         if(argv(2) == "expand_default")
761                                 cvar_set("scoreboard_columns", SCOREBOARD_DEFAULT_COLUMNS);
762                         argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
763                 }
764                 else if(argv(2) == "all" || argv(2) == "ALL")
765                 {
766                         string s = "ping pl name |"; // scores without label (not really scores)
767                         if(argv(2) == "ALL")
768                         {
769                                 // scores without label
770                                 s = strcat(s, " ", "sum");
771                                 s = strcat(s, " ", "kdratio");
772                                 s = strcat(s, " ", "frags");
773                         }
774                         FOREACH(Scores, true, {
775                                 if(it != ps_primary)
776                                 if(it != ps_secondary)
777                                 if(scores_label(it) != "")
778                                         s = strcat(s, " ", scores_label(it));
779                         });
780                         if(ps_secondary != ps_primary)
781                                 s = strcat(s, " ", scores_label(ps_secondary));
782                         s = strcat(s, " ", scores_label(ps_primary));
783                         argc = tokenizebyseparator(strcat("0 1 ", s), " ");
784                 }
785         }
786
787
788         sbt_num_fields = 0;
789
790         hud_fontsize = HUD_GetFontsize("hud_fontsize");
791
792         for(i = 1; i < argc - 1; ++i)
793         {
794                 str = argv(i+1);
795                 bool nocomplain = false;
796                 if(substring(str, 0, 1) == "?")
797                 {
798                         nocomplain = true;
799                         str = substring(str, 1, strlen(str) - 1);
800                 }
801
802                 slash = strstrofs(str, "/", 0);
803                 if(slash >= 0)
804                 {
805                         pattern = substring(str, 0, slash);
806                         str = substring(str, slash + 1, strlen(str) - (slash + 1));
807
808                         if (!isGametypeInFilter(gametype, teamplay, false, pattern))
809                                 continue;
810                 }
811
812                 str = strtolower(str);
813                 strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(str));
814                 sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
815
816                 PlayerScoreField j;
817                 switch(str)
818                 {
819                         // fields without a label (not networked via the score system)
820                         case "ping": sbt_field[sbt_num_fields] = SP_PING; break;
821                         case "pl": sbt_field[sbt_num_fields] = SP_PL; break;
822                         case "name": case "nick": sbt_field[sbt_num_fields] = SP_NAME; have_name = true; break;
823                         case "|": sbt_field[sbt_num_fields] = SP_SEPARATOR; have_separator = true; break;
824                         case "kd": case "kdr": case "kdratio": sbt_field[sbt_num_fields] = SP_KDRATIO; break;
825                         case "sum": case "diff": case "k-d": sbt_field[sbt_num_fields] = SP_SUM; break;
826                         case "frags": sbt_field[sbt_num_fields] = SP_FRAGS; break;
827                         default: // fields with a label
828                         {
829                                 // map alternative labels
830                                 if (str == "damage") str = "dmg";
831                                 if (str == "damagetaken") str = "dmgtaken";
832
833                                 FOREACH(Scores, true, {
834                                         if (str == strtolower(scores_label(it))) {
835                                                 j = it;
836                                                 goto found; // sorry, but otherwise fteqcc -O3 miscompiles this and warns about "unreachable code"
837                                         }
838                                 });
839
840                                 // NOTE: can't check STAT(SHOWFPS) here, if checked too early it returns false anyway
841                                 if(!nocomplain && str != "fps") // server can disable the fps field
842                                         LOG_INFOF("^1Error:^7 Unknown score field: '%s'", str);
843
844                                 strfree(sbt_field_title[sbt_num_fields]);
845                                 sbt_field_size[sbt_num_fields] = 0;
846                                 continue;
847
848                                 LABEL(found)
849                                 sbt_field[sbt_num_fields] = j;
850                                 if(j == ps_primary)
851                                         have_primary = true;
852                                 if(j == ps_secondary)
853                                         have_secondary = true;
854
855                         }
856                 }
857                 ++sbt_num_fields;
858                 if(sbt_num_fields >= MAX_SBT_FIELDS)
859                         break;
860         }
861
862         if(scores_flags(ps_primary) & SFL_ALLOW_HIDE)
863                 have_primary = true;
864         if(scores_flags(ps_secondary) & SFL_ALLOW_HIDE)
865                 have_secondary = true;
866         if(ps_primary == ps_secondary)
867                 have_secondary = true;
868         missing = (!have_primary) + (!have_secondary) + (!have_separator) + (!have_name);
869
870         if(sbt_num_fields + missing < MAX_SBT_FIELDS)
871         {
872                 if(!have_name)
873                 {
874                         strfree(sbt_field_title[sbt_num_fields]);
875                         for(i = sbt_num_fields; i > 0; --i)
876                         {
877                                 sbt_field_title[i] = sbt_field_title[i-1];
878                                 sbt_field_size[i] = sbt_field_size[i-1];
879                                 sbt_field[i] = sbt_field[i-1];
880                         }
881                         sbt_field_title[0] = strzone(TranslateScoresLabel("name"));
882                         sbt_field[0] = SP_NAME;
883                         ++sbt_num_fields;
884                         LOG_INFO("fixed missing field 'name'");
885
886                         if(!have_separator)
887                         {
888                                 strfree(sbt_field_title[sbt_num_fields]);
889                                 for(i = sbt_num_fields; i > 1; --i)
890                                 {
891                                         sbt_field_title[i] = sbt_field_title[i-1];
892                                         sbt_field_size[i] = sbt_field_size[i-1];
893                                         sbt_field[i] = sbt_field[i-1];
894                                 }
895                                 sbt_field_title[1] = strzone("|");
896                                 sbt_field[1] = SP_SEPARATOR;
897                                 sbt_field_size[1] = stringwidth("|", false, hud_fontsize);
898                                 ++sbt_num_fields;
899                                 LOG_INFO("fixed missing field '|'");
900                         }
901                 }
902                 else if(!have_separator)
903                 {
904                         strcpy(sbt_field_title[sbt_num_fields], "|");
905                         sbt_field_size[sbt_num_fields] = stringwidth("|", false, hud_fontsize);
906                         sbt_field[sbt_num_fields] = SP_SEPARATOR;
907                         ++sbt_num_fields;
908                         LOG_INFO("fixed missing field '|'");
909                 }
910                 if(!have_secondary)
911                 {
912                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_secondary)));
913                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
914                         sbt_field[sbt_num_fields] = ps_secondary;
915                         ++sbt_num_fields;
916                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_secondary));
917                 }
918                 if(!have_primary)
919                 {
920                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_primary)));
921                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
922                         sbt_field[sbt_num_fields] = ps_primary;
923                         ++sbt_num_fields;
924                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_primary));
925                 }
926         }
927
928         sbt_field[sbt_num_fields] = SP_END;
929 }
930
931 string Scoreboard_AddPlayerId(string pl_name, entity pl)
932 {
933         string pref = autocvar_hud_panel_scoreboard_playerid_prefix;
934         string suf = autocvar_hud_panel_scoreboard_playerid_suffix;
935         return strcat(pref, itos(pl.sv_entnum + 1), suf, pl_name);
936 }
937
938 // MOVEUP::
939 vector sbt_field_rgb;
940 string sbt_field_icon0;
941 string sbt_field_icon1;
942 string sbt_field_icon2;
943 vector sbt_field_icon0_rgb;
944 vector sbt_field_icon1_rgb;
945 vector sbt_field_icon2_rgb;
946 string Scoreboard_GetName(entity pl)
947 {
948         if(ready_waiting && pl.ready)
949         {
950                 sbt_field_icon0 = "gfx/scoreboard/player_ready";
951         }
952         else if(!teamplay)
953         {
954                 int f = entcs_GetClientColors(pl.sv_entnum);
955                 {
956                         sbt_field_icon0 = "gfx/scoreboard/playercolor_base";
957                         sbt_field_icon1 = "gfx/scoreboard/playercolor_shirt";
958                         sbt_field_icon1_rgb = colormapPaletteColor(floor(f / 16), 0);
959                         sbt_field_icon2 = "gfx/scoreboard/playercolor_pants";
960                         sbt_field_icon2_rgb = colormapPaletteColor(f % 16, 1);
961                 }
962         }
963         return entcs_GetName(pl.sv_entnum);
964 }
965
966 string Scoreboard_GetField(entity pl, PlayerScoreField field)
967 {
968         float tmp, num, denom;
969         int f;
970         string str;
971         sbt_field_rgb = '1 1 1';
972         sbt_field_icon0 = "";
973         sbt_field_icon1 = "";
974         sbt_field_icon2 = "";
975         sbt_field_icon0_rgb = '1 1 1';
976         sbt_field_icon1_rgb = '1 1 1';
977         sbt_field_icon2_rgb = '1 1 1';
978         switch(field)
979         {
980                 case SP_PING:
981                         if (!pl.gotscores)
982                                 return "\xE2\x96\xB6\xE2\x96\xB6\xE2\x96\xB6"; // >>> sign using U+25B6 (Black Right-Pointing Triangle)
983                         //str = getplayerkeyvalue(pl.sv_entnum, "ping");
984                         f = pl.ping;
985                         if(f == 0)
986                                 return _("N/A");
987                         tmp = max(0, min(220, f-80)) / 220;
988                         sbt_field_rgb = '1 1 1' - '0 1 1' * tmp;
989                         return ftos(f);
990
991                 case SP_PL:
992                         if (!pl.gotscores)
993                                 return _("N/A");
994                         f = pl.ping_packetloss;
995                         tmp = pl.ping_movementloss;
996                         if(f == 0 && tmp == 0)
997                                 return "";
998                         str = ftos(ceil(f * 100));
999                         if(tmp != 0)
1000                                 str = strcat(str, "~", ftos(ceil(tmp * 100)));
1001                         tmp = bound(0, f / 0.2 + tmp / 0.04, 1); // 20% is REALLY BAD pl
1002                         sbt_field_rgb = '1 0.5 0.5' - '0 0.5 0.5' * tmp;
1003                         return str;
1004
1005                 case SP_NAME:
1006                         str = Scoreboard_GetName(pl);
1007                         if (autocvar_hud_panel_scoreboard_playerid)
1008                                 str = Scoreboard_AddPlayerId(str, pl);
1009                         return str;
1010
1011                 case SP_FRAGS:
1012                         f = pl.(scores(SP_KILLS));
1013                         f -= pl.(scores(SP_SUICIDES));
1014                         return ftos(f);
1015
1016                 case SP_KDRATIO:
1017                         num = pl.(scores(SP_KILLS));
1018                         denom = pl.(scores(SP_DEATHS));
1019
1020                         if(denom == 0) {
1021                                 sbt_field_rgb = '0 1 0';
1022                                 str = sprintf("%d", num);
1023                         } else if(num <= 0) {
1024                                 sbt_field_rgb = '1 0 0';
1025                                 str = sprintf("%.1f", num/denom);
1026                         } else
1027                                 str = sprintf("%.1f", num/denom);
1028                         return str;
1029
1030                 case SP_SUM:
1031                         f = pl.(scores(SP_KILLS));
1032                         f -= pl.(scores(SP_DEATHS));
1033
1034                         if(f > 0) {
1035                                 sbt_field_rgb = '0 1 0';
1036                         } else if(f == 0) {
1037                                 sbt_field_rgb = '1 1 1';
1038                         } else {
1039                                 sbt_field_rgb = '1 0 0';
1040                         }
1041                         return ftos(f);
1042
1043                 case SP_ELO:
1044                 {
1045                         float elo = pl.(scores(SP_ELO));
1046                         switch (elo) {
1047                                 case -1: return "...";
1048                                 case -2: return _("N/A");
1049                                 default: return ftos(elo);
1050                         }
1051                 }
1052
1053                 case SP_FPS:
1054                 {
1055                         float fps = pl.(scores(SP_FPS));
1056                         if(fps == 0)
1057                         {
1058                                 sbt_field_rgb = '1 1 1';
1059                                 return ((pl.ping == 0) ? _("N/A") : "..."); // if 0 ping, either connecting or bot (either case can't show proper score)
1060                         }
1061                         //sbt_field_rgb = HUD_Get_Num_Color(fps, 200, true);
1062                         sbt_field_rgb = '1 0 0' + '0 1 1' * (bound(0, fps, 60) / 60);
1063                         return ftos(fps);
1064                 }
1065
1066                 case SP_DMG: case SP_DMGTAKEN:
1067                         return sprintf("%.1f k", pl.(scores(field)) / 1000);
1068
1069                 default: case SP_SCORE:
1070                         tmp = pl.(scores(field));
1071                         f = scores_flags(field);
1072                         if(field == ps_primary)
1073                                 sbt_field_rgb = '1 1 0';
1074                         else if(field == ps_secondary)
1075                                 sbt_field_rgb = '0 1 1';
1076                         else
1077                                 sbt_field_rgb = '1 1 1';
1078                         return ScoreString(f, tmp);
1079         }
1080         //return "error";
1081 }
1082
1083 float sbt_fixcolumnwidth_len;
1084 float sbt_fixcolumnwidth_iconlen;
1085 float sbt_fixcolumnwidth_marginlen;
1086
1087 string Scoreboard_FixColumnWidth(int i, string str)
1088 {
1089         TC(int, i);
1090         float f;
1091         vector sz;
1092
1093         sbt_fixcolumnwidth_iconlen = 0;
1094
1095         if(sbt_field_icon0 != "")
1096         {
1097                 sz = draw_getimagesize(sbt_field_icon0);
1098                 f = sz.x / sz.y;
1099                 if(sbt_fixcolumnwidth_iconlen < f)
1100                         sbt_fixcolumnwidth_iconlen = f;
1101         }
1102
1103         if(sbt_field_icon1 != "")
1104         {
1105                 sz = draw_getimagesize(sbt_field_icon1);
1106                 f = sz.x / sz.y;
1107                 if(sbt_fixcolumnwidth_iconlen < f)
1108                         sbt_fixcolumnwidth_iconlen = f;
1109         }
1110
1111         if(sbt_field_icon2 != "")
1112         {
1113                 sz = draw_getimagesize(sbt_field_icon2);
1114                 f = sz.x / sz.y;
1115                 if(sbt_fixcolumnwidth_iconlen < f)
1116                         sbt_fixcolumnwidth_iconlen = f;
1117         }
1118
1119         if(sbt_fixcolumnwidth_iconlen != 0)
1120         {
1121                 sbt_fixcolumnwidth_iconlen *= hud_fontsize.y / hud_fontsize.x; // fix icon aspect
1122                 sbt_fixcolumnwidth_marginlen = stringwidth(" ", false, hud_fontsize);
1123         }
1124         else
1125                 sbt_fixcolumnwidth_marginlen = 0;
1126
1127         if(sbt_field[i] == SP_NAME) // name gets all remaining space
1128         {
1129                 int j;
1130                 float remaining_space = 0;
1131                 for(j = 0; j < sbt_num_fields; ++j)
1132                         if(j != i)
1133                                 if (sbt_field[i] != SP_SEPARATOR)
1134                                         remaining_space += sbt_field_size[j] + hud_fontsize.x;
1135                 sbt_field_size[i] = panel_size.x - remaining_space;
1136
1137                 if (sbt_fixcolumnwidth_iconlen != 0)
1138                         remaining_space += sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
1139                 float namesize = panel_size.x - remaining_space;
1140                 str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
1141                 sbt_fixcolumnwidth_len = stringwidth(str, true, hud_fontsize);
1142
1143                 max_namesize = vid_conwidth - remaining_space;
1144         }
1145         else
1146                 sbt_fixcolumnwidth_len = stringwidth(str, false, hud_fontsize);
1147
1148         f = sbt_fixcolumnwidth_len + sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
1149         if(sbt_field_size[i] < f)
1150                 sbt_field_size[i] = f;
1151
1152         return str;
1153 }
1154
1155 void Scoreboard_initFieldSizes()
1156 {
1157         for(int i = 0; i < sbt_num_fields; ++i)
1158         {
1159                 sbt_field_size[i] = stringwidth(sbt_field_title[i], false, hud_fontsize);
1160                 Scoreboard_FixColumnWidth(i, "");
1161         }
1162 }
1163
1164 vector Scoreboard_DrawHeader(vector pos, vector rgb, bool other_players)
1165 {
1166         int i;
1167         vector column_dim = eY * panel_size.y;
1168         if(other_players)
1169                 column_dim.y -= 1.25 * hud_fontsize.y;
1170         vector text_offset = eY * (1.25 - 1) / 2 * hud_fontsize.y;
1171         pos.x += hud_fontsize.x * 0.5;
1172         for(i = 0; i < sbt_num_fields; ++i)
1173         {
1174                 if(sbt_field[i] == SP_SEPARATOR)
1175                         break;
1176                 column_dim.x = sbt_field_size[i] + hud_fontsize.x;
1177                 if (sbt_highlight)
1178                         if (i % 2)
1179                                 drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1180                 drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
1181                 pos.x += column_dim.x;
1182         }
1183         if(sbt_field[i] == SP_SEPARATOR)
1184         {
1185                 pos.x = panel_pos.x + panel_size.x - hud_fontsize.x * 0.5;
1186                 for(i = sbt_num_fields - 1; i > 0; --i)
1187                 {
1188                         if(sbt_field[i] == SP_SEPARATOR)
1189                                 break;
1190
1191                         pos.x -= sbt_field_size[i];
1192
1193                         if (sbt_highlight)
1194                                 if (!(i % 2))
1195                                 {
1196                                         column_dim.x = sbt_field_size[i] + hud_fontsize.x;
1197                                         drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1198                                 }
1199
1200                         text_offset.x = sbt_field_size[i] - stringwidth(sbt_field_title[i], false, hud_fontsize);
1201                         drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
1202                         pos.x -= hud_fontsize.x;
1203                 }
1204         }
1205
1206         pos.x = panel_pos.x;
1207         pos.y += 1.25 * hud_fontsize.y;
1208         return pos;
1209 }
1210
1211 void Scoreboard_DrawItem(vector item_pos, vector rgb, entity pl, bool is_self, int pl_number)
1212 {
1213         TC(bool, is_self); TC(int, pl_number);
1214         string str;
1215         bool is_spec = (entcs_GetSpecState(pl.sv_entnum) == ENTCS_SPEC_PURE);
1216
1217         vector h_pos = item_pos;
1218         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
1219         // alternated rows highlighting
1220         if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD && scoreboard_ui_enabled == 1)
1221         {
1222                 if (pl == scoreboard_selected_player)
1223                         drawfill(h_pos, h_size, rgb, 0.44 * panel_fg_alpha, DRAWFLAG_NORMAL);
1224         }
1225         else if(is_self)
1226                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);
1227         else if((sbt_highlight) && (!(pl_number % 2)))
1228                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
1229
1230         float fg_alpha = (is_self ? sbt_fg_alpha_self : sbt_fg_alpha);
1231
1232         vector pos = item_pos;
1233         // put a "self indicator" beside the self row, unicode U+25C0 (black left-pointing triangle)
1234         if (is_self)
1235                 drawstring(pos + eX * (panel_size.x + 0.5 * hud_fontsize.x) + eY, "\xE2\x97\x80", hud_fontsize, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
1236
1237         pos.x += hud_fontsize.x * 0.5;
1238         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
1239         vector tmp = '0 0 0';
1240         int i;
1241         PlayerScoreField field;
1242         for(i = 0; i < sbt_num_fields; ++i)
1243         {
1244                 field = sbt_field[i];
1245                 if(field == SP_SEPARATOR)
1246                         break;
1247
1248                 if(is_spec && field != SP_NAME && field != SP_PING) {
1249                         pos.x += sbt_field_size[i] + hud_fontsize.x;
1250                         continue;
1251                 }
1252                 str = Scoreboard_GetField(pl, field);
1253                 str = Scoreboard_FixColumnWidth(i, str);
1254
1255                 pos.x += sbt_field_size[i] + hud_fontsize.x;
1256
1257                 if(field == SP_NAME) {
1258                         tmp.x = sbt_field_size[i] - hud_fontsize.x * sbt_fixcolumnwidth_iconlen - sbt_fixcolumnwidth_marginlen + hud_fontsize.x;
1259                         drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
1260                 } else {
1261                         tmp.x = sbt_fixcolumnwidth_len + hud_fontsize.x;
1262                         drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
1263                 }
1264
1265                 tmp.x = sbt_field_size[i] + hud_fontsize.x;
1266                 if(sbt_field_icon0 != "")
1267                         drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
1268                 if(sbt_field_icon1 != "")
1269                         drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
1270                 if(sbt_field_icon2 != "")
1271                         drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
1272         }
1273
1274         if(sbt_field[i] == SP_SEPARATOR)
1275         {
1276                 pos.x = item_pos.x + panel_size.x - hud_fontsize.x * 0.5;
1277                 for(i = sbt_num_fields-1; i > 0; --i)
1278                 {
1279                         field = sbt_field[i];
1280                         if(field == SP_SEPARATOR)
1281                                 break;
1282
1283                         if(is_spec && field != SP_NAME && field != SP_PING) {
1284                                 pos.x -= sbt_field_size[i] + hud_fontsize.x;
1285                                 continue;
1286                         }
1287
1288                         str = Scoreboard_GetField(pl, field);
1289                         str = Scoreboard_FixColumnWidth(i, str);
1290
1291                         if(field == SP_NAME) {
1292                                 tmp.x = sbt_fixcolumnwidth_len; // left or right aligned? let's put it right...
1293                                 drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
1294                         } else {
1295                                 tmp.x = sbt_fixcolumnwidth_len;
1296                                 drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
1297                         }
1298
1299                         tmp.x = sbt_field_size[i];
1300                         if(sbt_field_icon0 != "")
1301                                 drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
1302                         if(sbt_field_icon1 != "")
1303                                 drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
1304                         if(sbt_field_icon2 != "")
1305                                 drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
1306                         pos.x -= sbt_field_size[i] + hud_fontsize.x;
1307                 }
1308         }
1309
1310         if(pl.eliminated)
1311                 drawfill(h_pos, h_size, '0 0 0', sbt_highlight_alpha_eliminated, DRAWFLAG_NORMAL);
1312 }
1313
1314 vector Scoreboard_DrawOthers(vector item_pos, vector rgb, int this_team, entity ignored_pl, entity pl, int pl_number)
1315 {
1316         int i = 0;
1317         vector h_pos = item_pos;
1318         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
1319
1320         bool complete = (this_team == NUM_SPECTATOR);
1321
1322         if(!complete)
1323         if((sbt_highlight) && (!(pl_number % 2)))
1324                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
1325
1326         vector pos = item_pos;
1327         pos.x += hud_fontsize.x * 0.5;
1328         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
1329
1330         float width_limit = item_pos.x + panel_size.x - hud_fontsize.x;
1331         if(!complete)
1332                 width_limit -= stringwidth("...", false, hud_fontsize);
1333         float namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
1334         static float max_name_width = 0;
1335         string field = "";
1336         float fieldsize = 0;
1337         float min_fieldsize = 0;
1338         float fieldpadding = hud_fontsize.x * 0.25;
1339         if(this_team == NUM_SPECTATOR)
1340         {
1341                 if(autocvar_hud_panel_scoreboard_spectators_showping)
1342                         min_fieldsize = stringwidth("999", false, hud_fontsize);
1343         }
1344         else if(autocvar_hud_panel_scoreboard_others_showscore)
1345                 min_fieldsize = stringwidth("99", false, hud_fontsize);
1346         for(i = 0; pl; pl = pl.sort_next)
1347         {
1348                 if(pl.team != this_team)
1349                         continue;
1350                 if(pl == ignored_pl)
1351                         continue;
1352
1353                 field = "";
1354                 if(this_team == NUM_SPECTATOR)
1355                 {
1356                         if(autocvar_hud_panel_scoreboard_spectators_showping)
1357                                 field = Scoreboard_GetField(pl, SP_PING);
1358                 }
1359                 else if(autocvar_hud_panel_scoreboard_others_showscore)
1360                         field = Scoreboard_GetField(pl, SP_SCORE);
1361
1362                 string str = entcs_GetName(pl.sv_entnum);
1363                 if (autocvar_hud_panel_scoreboard_playerid)
1364                         str = Scoreboard_AddPlayerId(str, pl);
1365                 str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
1366                 float column_width = stringwidth(str, true, hud_fontsize);
1367                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
1368                 {
1369                         if(column_width > max_name_width)
1370                                 max_name_width = column_width;
1371                         column_width = max_name_width;
1372                 }
1373                 if(field != "")
1374                 {
1375                         fieldsize = stringwidth(field, false, hud_fontsize);
1376                         column_width += hud_fontsize.x * 0.25 + max(fieldsize, min_fieldsize) + 2 * fieldpadding;
1377                 }
1378
1379                 if(pos.x + column_width > width_limit)
1380                 {
1381                         ++i;
1382                         if(!complete)
1383                         {
1384                                 drawstring(pos, "...", hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1385                                 break;
1386                         }
1387                         else
1388                         {
1389                                 pos.x = item_pos.x + hud_fontsize.x * 0.5;
1390                                 pos.y += hud_fontsize.y * 1.25;
1391                         }
1392                 }
1393
1394                 if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD && scoreboard_ui_enabled == 1)
1395                 {
1396                         if (pl == scoreboard_selected_player)
1397                         {
1398                                 h_size.x = column_width + hud_fontsize.x * 0.25;
1399                                 h_size.y = hud_fontsize.y;
1400                                 drawfill(pos - hud_fontsize.x * 0.25 * eX, h_size, rgb, 0.44 * panel_fg_alpha, DRAWFLAG_NORMAL);
1401                         }
1402                 }
1403
1404                 vector name_pos = pos;
1405                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
1406                         name_pos.x += max(fieldsize, min_fieldsize) + 2 * fieldpadding + hud_fontsize.x * 0.25;
1407                 drawcolorcodedstring(name_pos, str, hud_fontsize, sbt_fg_alpha, DRAWFLAG_NORMAL);
1408                 if(field != "")
1409                 {
1410                         h_size.x = max(fieldsize, min_fieldsize) + 2 * fieldpadding;
1411                         h_size.y = hud_fontsize.y;
1412                         vector field_pos = pos;
1413                         if(!((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned))
1414                                 field_pos.x += column_width - h_size.x;
1415                         if(sbt_highlight)
1416                                 drawfill(field_pos, h_size, '1 1 1', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1417                         field_pos.x += fieldpadding + (max(fieldsize, min_fieldsize) - fieldsize) * 0.5;
1418                         drawstring(field_pos, field, hud_fontsize, sbt_field_rgb, sbt_fg_alpha, DRAWFLAG_NORMAL);
1419                 }
1420                 if(pl.eliminated)
1421                 {
1422                         h_size.x = column_width + hud_fontsize.x * 0.25;
1423                         h_size.y = hud_fontsize.y;
1424                         drawfill(pos - hud_fontsize.x * 0.25 * eX, h_size, '0 0 0', sbt_highlight_alpha_eliminated, DRAWFLAG_NORMAL);
1425                 }
1426                 pos.x += column_width;
1427                 pos.x += hud_fontsize.x;
1428         }
1429         return vec2(item_pos.x, item_pos.y + i * hud_fontsize.y * 1.25);
1430 }
1431
1432 vector Scoreboard_MakeTable(vector pos, entity tm, vector rgb, vector bg_size)
1433 {
1434         int max_players = 999;
1435         if(autocvar_hud_panel_scoreboard_maxheight > 0)
1436         {
1437                 float height = autocvar_hud_panel_scoreboard_maxheight * vid_conheight;
1438                 if(teamplay)
1439                 {
1440                         height -= (panel_bg_padding * 2 + hud_fontsize.y * 1.25) * team_count; // - padding and header
1441                         height -= hud_fontsize.y * (team_count - 1); // - spacing between tables
1442                         height /= team_count;
1443                 }
1444                 else
1445                         height -= panel_bg_padding * 2; // - padding
1446                 max_players = floor(height / (hud_fontsize.y * 1.25));
1447                 if(max_players <= 1)
1448                         max_players = 1;
1449                 if(max_players == tm.team_size)
1450                         max_players = 999;
1451         }
1452
1453         entity pl;
1454         entity me = playerslots[current_player];
1455         panel_pos = pos;
1456         panel_size.y = 1.25 * hud_fontsize.y * (1 + bound(1, tm.team_size, max_players));
1457         panel_size.y += panel_bg_padding * 2;
1458
1459         vector scoreboard_selected_hl_pos = pos;
1460         vector scoreboard_selected_hl_size = '0 0 0';
1461         scoreboard_selected_hl_size.x = scoreboard_right - scoreboard_left;
1462         scoreboard_selected_hl_size.y = panel_size.y;
1463
1464         HUD_Panel_DrawBg();
1465
1466         vector end_pos = panel_pos + eY * (panel_size.y + 0.5 * hud_fontsize.y);
1467         if(panel.current_panel_bg != "0")
1468                 end_pos.y += panel_bg_border * 2;
1469
1470         if(panel_bg_padding)
1471         {
1472                 panel_pos += '1 1 0' * panel_bg_padding;
1473                 panel_size -= '2 2 0' * panel_bg_padding;
1474         }
1475
1476         pos = panel_pos;
1477         vector tmp = vec2(panel_size.x, 1.25 * hud_fontsize.y);
1478
1479         // rounded header
1480         if (sbt_bg_alpha)
1481                 drawpic(pos, "gfx/scoreboard/scoreboard_tableheader", tmp, rgb + '0.5 0.5 0.5', sbt_bg_alpha, DRAWFLAG_NORMAL);
1482
1483         pos.y += 1.25 * hud_fontsize.y;
1484
1485         // table background
1486         tmp.y = panel_size.y - 1.25 * hud_fontsize.y;
1487         if (sbt_bg_alpha)
1488                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1489
1490
1491         // print header row and highlight columns
1492         pos = Scoreboard_DrawHeader(panel_pos, rgb, (max_players < tm.team_size));
1493
1494         // fill the table and draw the rows
1495         bool is_self = false;
1496         bool self_shown = false;
1497         int i = 0;
1498         for(pl = players.sort_next; pl; pl = pl.sort_next)
1499         {
1500                 if(pl.team != tm.team)
1501                         continue;
1502                 if(i == max_players - 2 && pl != me)
1503                 {
1504                         if(!self_shown && me.team == tm.team)
1505                         {
1506                                 Scoreboard_DrawItem(pos, rgb, me, true, i);
1507                                 self_shown = true;
1508                                 pos.y += 1.25 * hud_fontsize.y;
1509                                 ++i;
1510                         }
1511                 }
1512                 if(i >= max_players - 1)
1513                 {
1514                         pos = Scoreboard_DrawOthers(pos, rgb, tm.team, (self_shown ? me : NULL), pl, i);
1515                         break;
1516                 }
1517                 is_self = (pl.sv_entnum == current_player);
1518                 Scoreboard_DrawItem(pos, rgb, pl, is_self, i);
1519                 if(is_self)
1520                         self_shown = true;
1521                 pos.y += 1.25 * hud_fontsize.y;
1522                 ++i;
1523         }
1524
1525         if (scoreboard_selected_panel == SB_PANEL_SCOREBOARD)
1526         {
1527                 if (scoreboard_ui_enabled == 1 || (tm && scoreboard_selected_team == tm))
1528                 {
1529                         float _alpha = (scoreboard_ui_enabled == 2) ? 0.2 : 0.3 * max(0, (1 - (time - scoreboard_selected_panel_time) * 2));
1530                         _alpha *= panel_fg_alpha;
1531                         if (_alpha)
1532                                 drawfill(scoreboard_selected_hl_pos, scoreboard_selected_hl_size, '1 1 1', _alpha, DRAWFLAG_NORMAL);
1533                 }
1534         }
1535
1536         panel_size.x += panel_bg_padding * 2; // restore initial width
1537         return end_pos;
1538 }
1539
1540 bool Scoreboard_WouldDraw()
1541 {
1542         if (scoreboard_ui_enabled)
1543                 return true;
1544         else if (MUTATOR_CALLHOOK(DrawScoreboard))
1545                 return false;
1546         else if (QuickMenu_IsOpened())
1547                 return false;
1548         else if (HUD_Radar_Clickable())
1549                 return false;
1550         else if (scoreboard_showscores)
1551                 return true;
1552         else if (intermission == 1)
1553                 return true;
1554         else if (intermission == 2)
1555                 return false;
1556         else if (spectatee_status != -1 && STAT(HEALTH) <= 0 && autocvar_cl_deathscoreboard && !MUTATOR_CALLHOOK(DrawDeathScoreboard)
1557                 && (!HUD_MinigameMenu_IsOpened() || !active_minigame))
1558         {
1559                 return true;
1560         }
1561         else if (scoreboard_showscores_force || MUTATOR_CALLHOOK(DrawScoreboard_Force))
1562                 return true;
1563         return false;
1564 }
1565
1566 float average_accuracy;
1567 vector Scoreboard_AccuracyStats_Draw(vector pos, vector rgb, vector bg_size)
1568 {
1569         scoreboard_acc_fade_alpha = min(scoreboard_fade_alpha, scoreboard_acc_fade_alpha + frametime * 10);
1570
1571         WepSet weapons_stat = WepSet_GetFromStat();
1572         WepSet weapons_inmap = WepSet_GetFromStat_InMap();
1573         int disownedcnt = 0;
1574         int nHidden = 0;
1575         FOREACH(Weapons, it != WEP_Null, {
1576                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1577
1578                 WepSet set = it.m_wepset;
1579                 if(it.spawnflags & WEP_TYPE_OTHER)
1580                 {
1581                         ++nHidden;
1582                         continue;
1583                 }
1584                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1585                 {
1586                         if (it.spawnflags & (WEP_FLAG_HIDDEN | WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_SPECIALATTACK))
1587                                 ++nHidden;
1588                         else
1589                                 ++disownedcnt;
1590                 }
1591         });
1592
1593         int weapon_cnt = (REGISTRY_COUNT(Weapons) - 1) - disownedcnt - nHidden;
1594         if (weapon_cnt <= 0) return pos;
1595
1596         int rows = 1;
1597         if (autocvar_hud_panel_scoreboard_accuracy_doublerows && weapon_cnt >= floor((REGISTRY_COUNT(Weapons) - nHidden - 1) * 0.5))
1598                 rows = 2;
1599         int columns = ceil(weapon_cnt / rows);
1600
1601         float aspect = max(0.001, autocvar_hud_panel_weapons_aspect);
1602         float weapon_height = hud_fontsize.y * 2.3 / aspect;
1603         float height = weapon_height + hud_fontsize.y;
1604
1605         drawstring(pos + eX * panel_bg_padding, sprintf(_("Accuracy stats (average %d%%)"), average_accuracy), hud_fontsize, '1 1 1', panel_fg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1606         pos.y += 1.25 * hud_fontsize.y;
1607         if(panel.current_panel_bg != "0")
1608                 pos.y += panel_bg_border;
1609
1610         panel_pos = pos;
1611         panel_size.y = height * rows;
1612         panel_size.y += panel_bg_padding * 2;
1613
1614         float panel_bg_alpha_save = panel_bg_alpha;
1615         panel_bg_alpha *= scoreboard_acc_fade_alpha;
1616         HUD_Panel_DrawBg();
1617         panel_bg_alpha = panel_bg_alpha_save;
1618
1619         vector end_pos = panel_pos + eY * (panel_size.y + 0.5 * hud_fontsize.y);
1620         if(panel.current_panel_bg != "0")
1621                 end_pos.y += panel_bg_border * 2;
1622
1623         if(panel_bg_padding)
1624         {
1625                 panel_pos += '1 1 0' * panel_bg_padding;
1626                 panel_size -= '2 2 0' * panel_bg_padding;
1627         }
1628
1629         pos = panel_pos;
1630         vector tmp = panel_size;
1631
1632         float weapon_width = tmp.x / columns / rows;
1633
1634         if (sbt_bg_alpha)
1635                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1636
1637         if(sbt_highlight)
1638         {
1639                 // column highlighting
1640                 for (int i = 0; i < columns; ++i)
1641                         if ((i % 2) == 0)
1642                                 drawfill(pos + eX * weapon_width * rows * i, vec2(weapon_width * rows, height * rows), '0 0 0', sbt_highlight_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1643
1644                 // row highlighting
1645                 for (int i = 0; i < rows; ++i)
1646                         drawfill(pos + eY * (weapon_height + height * i), vec2(tmp.x, hud_fontsize.y), rgb, sbt_highlight_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1647         }
1648
1649         average_accuracy = 0;
1650         int weapons_with_stats = 0;
1651         if (rows == 2)
1652                 pos.x += weapon_width / 2;
1653
1654         if (autocvar_hud_panel_scoreboard_accuracy_nocolors)
1655                 rgb = '1 1 1';
1656         else
1657                 Accuracy_LoadColors();
1658
1659         float oldposx = pos.x;
1660         vector tmpos = pos;
1661
1662         int column = 0;
1663         FOREACH(Weapons, it != WEP_Null, {
1664                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1665
1666                 WepSet set = it.m_wepset;
1667                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1668                         continue;
1669                 if (it.spawnflags & WEP_TYPE_OTHER)
1670                         continue;
1671
1672                 float weapon_alpha;
1673                 if (weapon_stats >= 0)
1674                         weapon_alpha = sbt_fg_alpha;
1675                 else
1676                         weapon_alpha = 0.2 * sbt_fg_alpha;
1677
1678                 // weapon icon
1679                 drawpic_aspect_skin(tmpos, it.model2, vec2(weapon_width, weapon_height), '1 1 1', weapon_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1680                 // the accuracy
1681                 if (weapon_stats >= 0) {
1682                         weapons_with_stats += 1;
1683                         average_accuracy += weapon_stats; // store sum of all accuracies in average_accuracy
1684
1685                         string s = sprintf("%d%%", weapon_stats * 100);
1686                         float padding = (weapon_width - stringwidth(s, false, hud_fontsize)) / 2;
1687
1688                         if(!autocvar_hud_panel_scoreboard_accuracy_nocolors)
1689                                 rgb = Accuracy_GetColor(weapon_stats);
1690
1691                         drawstring(tmpos + vec2(padding, weapon_height), s, hud_fontsize, rgb, sbt_fg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1692                 }
1693                 tmpos.x += weapon_width * rows;
1694                 pos.x += weapon_width * rows;
1695                 if (rows == 2 && column == columns - 1) {
1696                         tmpos.x = oldposx;
1697                         tmpos.y += height;
1698                         pos.y += height;
1699                 }
1700                 ++column;
1701         });
1702
1703         if (weapons_with_stats)
1704                 average_accuracy = floor((average_accuracy * 100 / weapons_with_stats) + 0.5);
1705
1706         panel_size.x += panel_bg_padding * 2; // restore initial width
1707
1708         return end_pos;
1709 }
1710
1711 bool is_item_filtered(entity it)
1712 {
1713         if (!autocvar_hud_panel_scoreboard_itemstats_filter)
1714                 return false;
1715         int mask = autocvar_hud_panel_scoreboard_itemstats_filter_mask;
1716         if (mask <= 0)
1717                 return false;
1718         if (it.instanceOfArmor || it.instanceOfHealth)
1719         {
1720                 int ha_mask = floor(mask) % 10;
1721                 switch (ha_mask)
1722                 {
1723                         default: return false;
1724                         case 4: if (it == ITEM_HealthMega || it == ITEM_ArmorMega) return true; // else fallthrough
1725                         case 3: if (it == ITEM_HealthBig || it == ITEM_ArmorBig) return true; // else fallthrough
1726                         case 2: if (it == ITEM_HealthMedium || it == ITEM_ArmorMedium) return true; // else fallthrough
1727                         case 1: if (it == ITEM_HealthSmall || it == ITEM_ArmorSmall) return true; // else fallthrough
1728                 }
1729         }
1730         if (it.instanceOfAmmo)
1731         {
1732                 int ammo_mask = floor(mask / 10) % 10;
1733                 return (ammo_mask == 1);
1734         }
1735         return false;
1736 }
1737
1738 vector Scoreboard_ItemStats_Draw(vector pos, vector rgb, vector bg_size)
1739 {
1740         scoreboard_itemstats_fade_alpha = min(scoreboard_fade_alpha, scoreboard_itemstats_fade_alpha + frametime * 10);
1741
1742         int disowned_cnt = 0;
1743         int uninteresting_cnt = 0;
1744         IL_EACH(default_order_items, true, {
1745                 int q = g_inventory.inv_items[it.m_id];
1746                 //q = 1; // debug: display all items
1747                 if (is_item_filtered(it))
1748                         ++uninteresting_cnt;
1749                 else if (!q)
1750                         ++disowned_cnt;
1751         });
1752         int items_cnt = REGISTRY_COUNT(Items) - uninteresting_cnt;
1753         int n = items_cnt - disowned_cnt;
1754         if (n <= 0) return pos;
1755
1756         int rows = (autocvar_hud_panel_scoreboard_itemstats_doublerows && n >= floor(REGISTRY_COUNT(Items) / 2)) ? 2 : 1;
1757         int columns = max(6, ceil(n / rows));
1758
1759         float item_height = hud_fontsize.y * 2.3;
1760         float height = item_height + hud_fontsize.y;
1761
1762         drawstring(pos + eX * panel_bg_padding, _("Item stats"), hud_fontsize, '1 1 1', panel_fg_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1763         pos.y += 1.25 * hud_fontsize.y;
1764         if(panel.current_panel_bg != "0")
1765                 pos.y += panel_bg_border;
1766
1767         panel_pos = pos;
1768         panel_size.y = height * rows;
1769         panel_size.y += panel_bg_padding * 2;
1770
1771         float panel_bg_alpha_save = panel_bg_alpha;
1772         panel_bg_alpha *= scoreboard_itemstats_fade_alpha;
1773         HUD_Panel_DrawBg();
1774         panel_bg_alpha = panel_bg_alpha_save;
1775
1776         vector end_pos = panel_pos + eY * (panel_size.y + 0.5 * hud_fontsize.y);
1777         if(panel.current_panel_bg != "0")
1778                 end_pos.y += panel_bg_border * 2;
1779
1780         if(panel_bg_padding)
1781         {
1782                 panel_pos += '1 1 0' * panel_bg_padding;
1783                 panel_size -= '2 2 0' * panel_bg_padding;
1784         }
1785
1786         pos = panel_pos;
1787         vector tmp = panel_size;
1788
1789         float item_width = tmp.x / columns / rows;
1790
1791         if (sbt_bg_alpha)
1792                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1793
1794         if(sbt_highlight)
1795         {
1796                 // column highlighting
1797                 for (int i = 0; i < columns; ++i)
1798                         if ((i % 2) == 0)
1799                                 drawfill(pos + eX * item_width * rows * i, vec2(item_width * rows, height * rows), '0 0 0', sbt_highlight_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1800
1801                 // row highlighting
1802                 for (int i = 0; i < rows; ++i)
1803                         drawfill(pos + eY * (item_height + height * i), vec2(panel_size.x, hud_fontsize.y), rgb, sbt_highlight_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1804         }
1805
1806         if (rows == 2)
1807                 pos.x += item_width / 2;
1808
1809         float oldposx = pos.x;
1810         vector tmpos = pos;
1811
1812         int column = 0;
1813         IL_EACH(default_order_items, !is_item_filtered(it), {
1814                 int n = g_inventory.inv_items[it.m_id];
1815                 //n = 1 + floor(i * 3 + 4.8) % 7; // debug: display a value for each item
1816                 if (n <= 0) continue;
1817                 drawpic_aspect_skin(tmpos, it.m_icon, eX * item_width + eY * item_height, '1 1 1', panel_fg_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1818                 string s = ftos(n);
1819                 float padding = (item_width - stringwidth(s, false, hud_fontsize)) / 2;
1820                 drawstring(tmpos + vec2(padding, item_height), s, hud_fontsize, '1 1 1', panel_fg_alpha * scoreboard_itemstats_fade_alpha, DRAWFLAG_NORMAL);
1821                 tmpos.x += item_width * rows;
1822                 pos.x += item_width * rows;
1823                 if (rows == 2 && column == columns - 1) {
1824                         tmpos.x = oldposx;
1825                         tmpos.y += height;
1826                         pos.y += height;
1827                 }
1828                 ++column;
1829         });
1830
1831         panel_size.x += panel_bg_padding * 2; // restore initial width
1832
1833         return end_pos;
1834 }
1835
1836 vector MapStats_DrawKeyValue(vector pos, string key, string value) {
1837         float px = pos.x;
1838         pos.x += hud_fontsize.x * 0.25;
1839         drawstring(pos, key, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1840         pos.x = panel_pos.x + panel_size.x - stringwidth(value, false, hud_fontsize) - hud_fontsize.x * 0.25;
1841         drawstring(pos, value, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1842         pos.x = px;
1843         pos.y += hud_fontsize.y;
1844
1845         return pos;
1846 }
1847
1848 vector Scoreboard_MapStats_Draw(vector pos, vector rgb, vector bg_size) {
1849         float stat_secrets_found, stat_secrets_total;
1850         float stat_monsters_killed, stat_monsters_total;
1851         float rows = 0;
1852         string val;
1853
1854         // get monster stats
1855         stat_monsters_killed = STAT(MONSTERS_KILLED);
1856         stat_monsters_total = STAT(MONSTERS_TOTAL);
1857
1858         // get secrets stats
1859         stat_secrets_found = STAT(SECRETS_FOUND);
1860         stat_secrets_total = STAT(SECRETS_TOTAL);
1861
1862         // get number of rows
1863         if(stat_secrets_total)
1864                 rows += 1;
1865         if(stat_monsters_total)
1866                 rows += 1;
1867
1868         // if no rows, return
1869         if (!rows)
1870                 return pos;
1871
1872         //  draw table header
1873         drawstring(pos + eX * panel_bg_padding, _("Map stats:"), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1874         pos.y += 1.25 * hud_fontsize.y;
1875         if(panel.current_panel_bg != "0")
1876                 pos.y += panel_bg_border;
1877
1878         panel_pos = pos;
1879         panel_size.y = hud_fontsize.y * rows;
1880         panel_size.y += panel_bg_padding * 2;
1881         HUD_Panel_DrawBg();
1882
1883         vector end_pos = panel_pos + eY * (panel_size.y + 0.5 * hud_fontsize.y);
1884         if(panel.current_panel_bg != "0")
1885                 end_pos.y += panel_bg_border * 2;
1886
1887         if(panel_bg_padding)
1888         {
1889                 panel_pos += '1 1 0' * panel_bg_padding;
1890                 panel_size -= '2 2 0' * panel_bg_padding;
1891         }
1892
1893         pos = panel_pos;
1894         vector tmp = panel_size;
1895
1896         if (sbt_bg_alpha)
1897                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1898
1899         // draw monsters
1900         if(stat_monsters_total)
1901         {
1902                 val = sprintf("%d/%d", stat_monsters_killed, stat_monsters_total);
1903                 pos = MapStats_DrawKeyValue(pos, _("Monsters killed:"), val);
1904         }
1905
1906         // draw secrets
1907         if(stat_secrets_total)
1908         {
1909                 val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total);
1910                 pos = MapStats_DrawKeyValue(pos, _("Secrets found:"), val);
1911         }
1912
1913         panel_size.x += panel_bg_padding * 2; // restore initial width
1914         return end_pos;
1915 }
1916
1917 vector Scoreboard_Rankings_Draw(vector pos, string ranktitle, entity pl, vector rgb, vector bg_size)
1918 {
1919         int i;
1920         RANKINGS_RECEIVED_CNT = 0;
1921         for (i=RANKINGS_CNT-1; i>=0; --i)
1922                 if (grecordtime[i])
1923                         ++RANKINGS_RECEIVED_CNT;
1924
1925         if (RANKINGS_RECEIVED_CNT == 0)
1926                 return pos;
1927
1928         vector hl_rgb = rgb + '0.5 0.5 0.5';
1929
1930         vector scoreboard_selected_hl_pos = pos;
1931
1932         drawstring(pos + eX * panel_bg_padding, ranktitle, hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1933         pos.y += 1.25 * hud_fontsize.y;
1934         if(panel.current_panel_bg != "0")
1935                 pos.y += panel_bg_border;
1936
1937         vector scoreboard_selected_hl_size = '0 0 0';
1938         scoreboard_selected_hl_size.x = scoreboard_right - scoreboard_left;
1939         scoreboard_selected_hl_size.y = pos.y - scoreboard_selected_hl_pos.y;
1940
1941         panel_pos = pos;
1942
1943         float namesize = 0;
1944         for(i = 0; i < RANKINGS_RECEIVED_CNT; ++i)
1945         {
1946                 float f = stringwidth(ColorTranslateRGB(grecordholder[i]), true, hud_fontsize);
1947                 if(f > namesize)
1948                         namesize = f;
1949         }
1950         bool cut = false;
1951         if(namesize > autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x)
1952         {
1953                 namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
1954                 cut = true;
1955         }
1956
1957         float ranksize = 3 * hud_fontsize.x;
1958         float timesize = 5 * hud_fontsize.x;
1959         vector columnsize = vec2(ranksize + timesize + namesize + hud_fontsize.x, 1.25 * hud_fontsize.y);
1960         rankings_columns = max(1, floor((panel_size.x - 2 * panel_bg_padding) / columnsize.x));
1961         rankings_columns = min(rankings_columns, RANKINGS_RECEIVED_CNT);
1962         if (!rankings_cnt)
1963         {
1964                 rankings_cnt = RANKINGS_RECEIVED_CNT;
1965                 rankings_rows = ceil(rankings_cnt / rankings_columns);
1966         }
1967
1968         // expand name column to fill the entire row
1969         float available_space = (panel_size.x - 2 * panel_bg_padding - columnsize.x * rankings_columns) / rankings_columns;
1970         namesize += available_space;
1971         columnsize.x += available_space;
1972
1973         panel_size.y = rankings_rows * 1.25 * hud_fontsize.y;
1974         panel_size.y += panel_bg_padding * 2;
1975         scoreboard_selected_hl_size.y += panel_size.y;
1976
1977         HUD_Panel_DrawBg();
1978
1979         vector end_pos = panel_pos + eY * (panel_size.y + 0.5 * hud_fontsize.y);
1980         if(panel.current_panel_bg != "0")
1981                 end_pos.y += panel_bg_border * 2;
1982
1983         if(panel_bg_padding)
1984         {
1985                 panel_pos += '1 1 0' * panel_bg_padding;
1986                 panel_size -= '2 2 0' * panel_bg_padding;
1987         }
1988
1989         pos = panel_pos;
1990
1991         if (sbt_bg_alpha)
1992                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, panel_size, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1993
1994         vector text_ofs = vec2(0.5 * hud_fontsize.x, (1.25 - 1) / 2 * hud_fontsize.y); // center text vertically
1995         string str = "";
1996         int column = 0, j = 0;
1997         string zoned_name_self = strzone(strdecolorize(entcs_GetName(player_localnum)));
1998         int start_item = rankings_start_column * rankings_rows;
1999         for(i = start_item; i < start_item + rankings_cnt; ++i)
2000         {
2001                 int t = grecordtime[i];
2002                 if (t == 0)
2003                         continue;
2004
2005                 if(strdecolorize(grecordholder[i]) == zoned_name_self)
2006                         drawfill(pos, columnsize, hl_rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);
2007                 else if(!((j + rankings_start_column + column) & 1) && sbt_highlight)
2008                         drawfill(pos, columnsize, hl_rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
2009
2010                 str = count_ordinal(i+1);
2011                 drawstring(pos + text_ofs, str, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
2012                 drawstring(pos + text_ofs + eX * ranksize, TIME_ENCODED_TOSTRING(t), hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
2013                 str = ColorTranslateRGB(grecordholder[i]);
2014                 if(cut)
2015                         str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
2016                 drawcolorcodedstring(pos + text_ofs + eX * (ranksize + timesize), str, hud_fontsize, sbt_fg_alpha, DRAWFLAG_NORMAL);
2017
2018                 pos.y += 1.25 * hud_fontsize.y;
2019                 j++;
2020                 if(j >= rankings_rows)
2021                 {
2022                         column++;
2023                         j = 0;
2024                         pos.x += panel_size.x / rankings_columns;
2025                         pos.y = panel_pos.y;
2026                 }
2027         }
2028         strfree(zoned_name_self);
2029
2030         if (scoreboard_selected_panel == SB_PANEL_RANKINGS)
2031         {
2032                 float fade = max(0, (1 - (time - scoreboard_selected_panel_time) * 2));
2033                 drawfill(scoreboard_selected_hl_pos, scoreboard_selected_hl_size, '1 1 1', fade * 0.44, DRAWFLAG_NORMAL);
2034         }
2035
2036         panel_size.x += panel_bg_padding * 2; // restore initial width
2037         return end_pos;
2038 }
2039
2040 bool have_weapon_stats;
2041 bool Scoreboard_AccuracyStats_WouldDraw(float ypos)
2042 {
2043         if (MUTATOR_CALLHOOK(DrawScoreboardAccuracy))
2044                 return false;
2045         if (!autocvar_hud_panel_scoreboard_accuracy || warmup_stage || ypos > 0.91 * vid_conheight)
2046                 return false;
2047
2048         if (time < scoreboard_time + autocvar_hud_panel_scoreboard_accuracy_showdelay
2049                 && ypos > autocvar_hud_panel_scoreboard_accuracy_showdelay_minpos * vid_conheight
2050                 && !intermission)
2051         {
2052                 return false;
2053         }
2054
2055         if (!have_weapon_stats)
2056         {
2057                 FOREACH(Weapons, it != WEP_Null, {
2058                         int weapon_stats = weapon_accuracy[i - WEP_FIRST];
2059                         if (weapon_stats >= 0)
2060                         {
2061                                 have_weapon_stats = true;
2062                                 break;
2063                         }
2064                 });
2065                 if (!have_weapon_stats)
2066                         return false;
2067         }
2068
2069         return true;
2070 }
2071
2072 bool have_item_stats;
2073 bool Scoreboard_ItemStats_WouldDraw(float ypos)
2074 {
2075         if (MUTATOR_CALLHOOK(DrawScoreboardItemStats))
2076                 return false;
2077         if (!autocvar_hud_panel_scoreboard_itemstats || !g_inventory || warmup_stage || ypos > 0.91 * vid_conheight)
2078                 return false;
2079
2080         if (time < scoreboard_time + autocvar_hud_panel_scoreboard_itemstats_showdelay
2081                 && ypos > autocvar_hud_panel_scoreboard_itemstats_showdelay_minpos * vid_conheight
2082                 && !intermission)
2083         {
2084                 return false;
2085         }
2086
2087         if (!have_item_stats)
2088         {
2089                 IL_EACH(default_order_items, true, {
2090                         if (!is_item_filtered(it))
2091                         {
2092                                 int q = g_inventory.inv_items[it.m_id];
2093                                 //q = 1; // debug: display all items
2094                                 if (q)
2095                                 {
2096                                         have_item_stats = true;
2097                                         break;
2098                                 }
2099                         }
2100                 });
2101                 if (!have_item_stats)
2102                         return false;
2103         }
2104
2105         return true;
2106 }
2107
2108 vector Scoreboard_Spectators_Draw(vector pos) {
2109
2110         entity pl, tm;
2111         string str = "";
2112
2113         for(pl = players.sort_next; pl; pl = pl.sort_next)
2114         {
2115                 if(pl.team == NUM_SPECTATOR)
2116                 {
2117                         for(tm = teams.sort_next; tm; tm = tm.sort_next)
2118                                 if(tm.team == NUM_SPECTATOR)
2119                                         break;
2120                         str = sprintf("%s (%d)", _("Spectators"), tm.team_size);
2121                         draw_beginBoldFont();
2122                         drawstring(pos, str, hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
2123                         draw_endBoldFont();
2124                         pos.y += 1.25 * hud_fontsize.y;
2125
2126                         pos = Scoreboard_DrawOthers(pos, '0 0 0', pl.team, NULL, pl, 0);
2127                         pos.y += 1.25 * hud_fontsize.y;
2128
2129                         break;
2130                 }
2131         }
2132         if (str != "") // if there's at least one spectator
2133                 pos.y += 0.5 * hud_fontsize.y;
2134
2135         return pos;
2136 }
2137
2138 void Scoreboard_Draw()
2139 {
2140         if(!autocvar__hud_configure)
2141         {
2142                 if(!hud_draw_maximized) return;
2143
2144                 // frametime checks allow to toggle the scoreboard even when the game is paused
2145                 if(scoreboard_active) {
2146                         if (scoreboard_fade_alpha == 0)
2147                                 scoreboard_time = time;
2148                         if(hud_configure_menu_open == 1)
2149                                 scoreboard_fade_alpha = 1;
2150                         float scoreboard_fadeinspeed = autocvar_hud_panel_scoreboard_fadeinspeed;
2151                         if (scoreboard_fadeinspeed && frametime)
2152                                 scoreboard_fade_alpha = min(1, scoreboard_fade_alpha + frametime * scoreboard_fadeinspeed);
2153                         else
2154                                 scoreboard_fade_alpha = 1;
2155                         if(hud_fontsize_str != autocvar_hud_fontsize)
2156                         {
2157                                 hud_fontsize = HUD_GetFontsize("hud_fontsize");
2158                                 Scoreboard_initFieldSizes();
2159                                 strcpy(hud_fontsize_str, autocvar_hud_fontsize);
2160                         }
2161                 }
2162                 else {
2163                         float scoreboard_fadeoutspeed = autocvar_hud_panel_scoreboard_fadeoutspeed;
2164                         if (scoreboard_fadeoutspeed && frametime)
2165                                 scoreboard_fade_alpha = max(0, scoreboard_fade_alpha - frametime * scoreboard_fadeoutspeed);
2166                         else
2167                                 scoreboard_fade_alpha = 0;
2168                 }
2169
2170                 if (!scoreboard_fade_alpha)
2171                 {
2172                         scoreboard_acc_fade_alpha = 0;
2173                         scoreboard_itemstats_fade_alpha = 0;
2174                         return;
2175                 }
2176         }
2177         else
2178                 scoreboard_fade_alpha = 0;
2179
2180         if (autocvar_hud_panel_scoreboard_dynamichud)
2181                 HUD_Scale_Enable();
2182         else
2183                 HUD_Scale_Disable();
2184
2185         if(scoreboard_fade_alpha <= 0)
2186                 return;
2187         panel_fade_alpha *= scoreboard_fade_alpha;
2188         HUD_Panel_LoadCvars();
2189
2190         sbt_bg_alpha = autocvar_hud_panel_scoreboard_table_bg_alpha * panel_fg_alpha;
2191         sbt_highlight = autocvar_hud_panel_scoreboard_table_highlight;
2192         sbt_highlight_alpha = autocvar_hud_panel_scoreboard_table_highlight_alpha * panel_fg_alpha;
2193         sbt_highlight_alpha_self = autocvar_hud_panel_scoreboard_table_highlight_alpha_self * panel_fg_alpha;
2194         sbt_highlight_alpha_eliminated = autocvar_hud_panel_scoreboard_table_highlight_alpha_eliminated * panel_fg_alpha;
2195         sbt_fg_alpha = autocvar_hud_panel_scoreboard_table_fg_alpha * panel_fg_alpha;
2196         sbt_fg_alpha_self = autocvar_hud_panel_scoreboard_table_fg_alpha_self * panel_fg_alpha;
2197
2198         // don't overlap with con_notify
2199         if(!autocvar__hud_configure)
2200                 panel_pos.y = max((autocvar_con_notify * autocvar_con_notifysize), panel_pos.y);
2201
2202         float excess = max(0, max_namesize - autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x);
2203         float fixed_scoreboard_width = bound(vid_conwidth * autocvar_hud_panel_scoreboard_minwidth, vid_conwidth - excess, vid_conwidth * 0.93);
2204         scoreboard_left = 0.5 * (vid_conwidth - fixed_scoreboard_width);
2205         scoreboard_right = scoreboard_left + fixed_scoreboard_width;
2206         panel_pos.x = scoreboard_left;
2207         panel_size.x = fixed_scoreboard_width;
2208
2209         Scoreboard_UpdatePlayerTeams();
2210
2211         scoreboard_top = panel_pos.y;
2212         vector pos = panel_pos;
2213         entity tm;
2214         string str;
2215         vector str_pos;
2216
2217         vector sb_gameinfo_type_fontsize, sb_gameinfo_detail_fontsize;
2218
2219         // Begin of Game Info Section
2220         sb_gameinfo_type_fontsize = hud_fontsize * 2.5;
2221         sb_gameinfo_detail_fontsize = hud_fontsize * 1.3;
2222
2223         // Game Info: Game Type
2224         if (scoreboard_ui_enabled == 2)
2225                 str = _("Team Selection");
2226         else
2227                 str = MapInfo_Type_ToText(gametype);
2228         draw_beginBoldFont();
2229         drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_type_fontsize)), str, sb_gameinfo_type_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
2230         draw_endBoldFont();
2231
2232         pos.y += sb_gameinfo_type_fontsize.y;
2233         // Game Info: Game Detail
2234         if (scoreboard_ui_enabled == 2)
2235         {
2236                 if (scoreboard_selected_team)
2237                         str = sprintf(_("^7Press ^3%s^7 to join the selected team"), getcommandkey(_("jump"), "+jump"));
2238                 else
2239                         str = sprintf(_("^7Press ^3%s^7 to auto-select a team and join"), getcommandkey(_("jump"), "+jump"));
2240                 drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
2241
2242                 pos.y += sb_gameinfo_detail_fontsize.y + hud_fontsize.y * 0.3;
2243                 str = sprintf(_("^7Press ^3%s ^7to select a specific team"), translate_key("TAB"));
2244                 drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
2245         }
2246         else
2247         {
2248                 float tl = STAT(TIMELIMIT);
2249                 float fl = STAT(FRAGLIMIT);
2250                 float ll = STAT(LEADLIMIT);
2251                 float ll_and_fl = STAT(LEADLIMIT_AND_FRAGLIMIT);
2252                 str = "";
2253                 if(tl > 0)
2254                         str = strcat(str, sprintf(_("^3%1.0f minutes"), tl));
2255                 if(!gametype.m_hidelimits)
2256                 {
2257                         if(fl > 0)
2258                         {
2259                                 if(tl > 0)
2260                                         str = strcat(str, "^7 / "); // delimiter
2261                                 if(teamplay)
2262                                 {
2263                                         str = strcat(str, sprintf(_("^5%s %s"), ScoreString(teamscores_flags(ts_primary), fl),
2264                                                 (teamscores_label(ts_primary) == "score")   ? CTX(_("SCO^points")) :
2265                                                 (teamscores_label(ts_primary) == "fastest") ? "" :
2266                                                 TranslateScoresLabel(teamscores_label(ts_primary))));
2267                                 }
2268                                 else
2269                                 {
2270                                         str = strcat(str, sprintf(_("^5%s %s"), ScoreString(scores_flags(ps_primary), fl),
2271                                                 (scores_label(ps_primary) == "score")   ? CTX(_("SCO^points")) :
2272                                                 (scores_label(ps_primary) == "fastest") ? "" :
2273                                                 TranslateScoresLabel(scores_label(ps_primary))));
2274                                 }
2275                         }
2276                         if(ll > 0)
2277                         {
2278                                 if(tl > 0 || fl > 0)
2279                                 {
2280                                         // delimiter
2281                                         if (ll_and_fl && fl > 0)
2282                                                 str = strcat(str, "^7 & ");
2283                                         else
2284                                                 str = strcat(str, "^7 / ");
2285                                 }
2286
2287                                 if(teamplay)
2288                                 {
2289                                         str = strcat(str, sprintf(_("^2+%s %s"), ScoreString(teamscores_flags(ts_primary), ll),
2290                                                 (teamscores_label(ts_primary) == "score")   ? CTX(_("SCO^points")) :
2291                                                 (teamscores_label(ts_primary) == "fastest") ? "" :
2292                                                 TranslateScoresLabel(teamscores_label(ts_primary))));
2293                                 }
2294                                 else
2295                                 {
2296                                         str = strcat(str, sprintf(_("^2+%s %s"), ScoreString(scores_flags(ps_primary), ll),
2297                                                 (scores_label(ps_primary) == "score")   ? CTX(_("SCO^points")) :
2298                                                 (scores_label(ps_primary) == "fastest") ? "" :
2299                                                 TranslateScoresLabel(scores_label(ps_primary))));
2300                                 }
2301                         }
2302                 }
2303                 drawcolorcodedstring(pos + '1 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align right
2304                 // map name
2305                 str = sprintf(_("^7Map: ^2%s"), shortmapname);
2306                 drawcolorcodedstring(pos, str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align left
2307         }
2308         // End of Game Info Section
2309
2310         pos.y += sb_gameinfo_detail_fontsize.y + hud_fontsize.y * 0.3; // space between Game Info Section and score table
2311         if(panel.current_panel_bg != "0")
2312                 pos.y += panel_bg_border;
2313
2314         // Draw the scoreboard
2315         float scale = autocvar_hud_panel_scoreboard_table_bg_scale;
2316         if(scale <= 0)
2317                 scale = 0.25;
2318         vector bg_size = draw_getimagesize("gfx/scoreboard/scoreboard_bg") * scale;
2319
2320         if(teamplay)
2321         {
2322                 vector panel_bg_color_save = panel_bg_color;
2323                 vector team_score_baseoffset;
2324                 vector team_size_baseoffset;
2325                 if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
2326                 {
2327                         // put team score to the left of scoreboard (and team size to the right)
2328                         team_score_baseoffset = eY * hud_fontsize.y - eX * hud_fontsize.x * 0.5;
2329                         team_size_baseoffset = eY * hud_fontsize.y + eX * hud_fontsize.x * 0.5;
2330                         if(panel.current_panel_bg != "0")
2331                         {
2332                                 team_score_baseoffset.x -= panel_bg_border;
2333                                 team_size_baseoffset.x += panel_bg_border;
2334                         }
2335                 }
2336                 else
2337                 {
2338                         // put team score to the right of scoreboard (and team size to the left)
2339                         team_score_baseoffset = eY * hud_fontsize.y + eX * hud_fontsize.x * 0.5;
2340                         team_size_baseoffset = eY * hud_fontsize.y - eX * hud_fontsize.x * 0.5;
2341                         if(panel.current_panel_bg != "0")
2342                         {
2343                                 team_score_baseoffset.x += panel_bg_border;
2344                                 team_size_baseoffset.x -= panel_bg_border;
2345                         }
2346                 }
2347
2348                 int team_size_total = 0;
2349                 if (autocvar_hud_panel_scoreboard_team_size_position != 0) // team size not off
2350                 {
2351                         // calculate team size total (sum of all team sizes)
2352                         for(tm = teams.sort_next; tm; tm = tm.sort_next)
2353                                 if(tm.team != NUM_SPECTATOR)
2354                                         team_size_total += tm.team_size;
2355                 }
2356
2357                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
2358                 {
2359                         if(tm.team == NUM_SPECTATOR)
2360                                 continue;
2361                         if(!tm.team)
2362                                 continue;
2363
2364                         draw_beginBoldFont();
2365                         vector rgb = Team_ColorRGB(tm.team);
2366                         str = ftos(tm.(teamscores(ts_primary)));
2367                         if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
2368                         {
2369                                 // team score on the left (default)
2370                                 str_pos = pos + team_score_baseoffset - eX * stringwidth(str, false, hud_fontsize * 1.5);
2371                         }
2372                         else
2373                         {
2374                                 // team score on the right
2375                                 str_pos = pos + team_score_baseoffset + eX * (panel_size.x + hud_fontsize.x * 1.5);
2376                         }
2377                         drawstring(str_pos, str, hud_fontsize * 1.5, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
2378
2379                         // team size (if set to show on the side)
2380                         if (autocvar_hud_panel_scoreboard_team_size_position != 0) // team size not off
2381                         {
2382                                 // calculate the starting position for the whole team size info string
2383                                 str = sprintf("%d/%d", tm.team_size, team_size_total);
2384                                 if (autocvar_hud_panel_scoreboard_team_size_position == 1)
2385                                 {
2386                                         // team size on the left
2387                                         str_pos = pos + team_size_baseoffset - eX * stringwidth(str, false, hud_fontsize * 1.5);
2388                                 }
2389                                 else
2390                                 {
2391                                         // team size on the right
2392                                         str_pos = pos + team_size_baseoffset + eX * (panel_size.x + hud_fontsize.x * 1.5);
2393                                 }
2394                                 str = sprintf("%d", tm.team_size);
2395                                 drawstring(str_pos, str, hud_fontsize * 1.5, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
2396                                 str_pos += eX * stringwidth(str, true, hud_fontsize * 1.5) + eY * hud_fontsize.y * .5;
2397                                 str = sprintf("/%d", team_size_total);
2398                                 drawstring(str_pos, str, hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
2399                         }
2400
2401
2402                         // secondary score, e.g. keyhunt
2403                         if(ts_primary != ts_secondary)
2404                         {
2405                                 str = ftos(tm.(teamscores(ts_secondary)));
2406                                 if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
2407                                 {
2408                                         // left
2409                                         str_pos = pos + team_score_baseoffset - vec2(stringwidth(str, false, hud_fontsize), hud_fontsize.y * -1.5);
2410                                 }
2411                                 else
2412                                 {
2413                                         // right
2414                                         str_pos = pos + team_score_baseoffset + vec2(panel_size.x + hud_fontsize.x * 1.5, hud_fontsize.y * 1.5);
2415                                 }
2416
2417                                 drawstring(str_pos, str, hud_fontsize, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
2418                         }
2419                         draw_endBoldFont();
2420                         if(autocvar_hud_panel_scoreboard_bg_teams_color_team > 0)
2421                                 panel_bg_color = rgb * autocvar_hud_panel_scoreboard_bg_teams_color_team;
2422                         else if(panel_bg_color_team > 0)
2423                                 panel_bg_color = rgb * panel_bg_color_team;
2424                         else
2425                                 panel_bg_color = rgb;
2426                         pos = Scoreboard_MakeTable(pos, tm, panel_bg_color, bg_size);
2427                 }
2428                 panel_bg_color = panel_bg_color_save;
2429         }
2430         else
2431         {
2432                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
2433                         if(tm.team != NUM_SPECTATOR)
2434                                 break;
2435
2436                 // display it anyway
2437                 pos = Scoreboard_MakeTable(pos, tm, panel_bg_color, bg_size);
2438         }
2439
2440         // draw scoreboard spectators before accuracy and item stats
2441         if (autocvar_hud_panel_scoreboard_spectators_position == 0) {
2442                 pos = Scoreboard_Spectators_Draw(pos);
2443         }
2444
2445         // draw accuracy and item stats
2446         if (Scoreboard_AccuracyStats_WouldDraw(pos.y))
2447                 pos = Scoreboard_AccuracyStats_Draw(pos, panel_bg_color, bg_size);
2448         if (Scoreboard_ItemStats_WouldDraw(pos.y))
2449                 pos = Scoreboard_ItemStats_Draw(pos, panel_bg_color, bg_size);
2450
2451         // draw scoreboard spectators after accuracy and item stats and before rankings
2452         if (autocvar_hud_panel_scoreboard_spectators_position == 1) {
2453                 pos = Scoreboard_Spectators_Draw(pos);
2454         }
2455
2456         if(MUTATOR_CALLHOOK(ShowRankings)) {
2457                 string ranktitle = M_ARGV(0, string);
2458                 string unit = GetSpeedUnit(autocvar_hud_panel_physics_speed_unit);
2459                 if(race_speedaward_alltimebest)
2460                 {
2461                         string name;
2462                         float namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
2463                         str = "";
2464                         if(race_speedaward)
2465                         {
2466                                 name = textShortenToWidth(ColorTranslateRGB(race_speedaward_holder), namesize, hud_fontsize, stringwidth_colors);
2467                                 str = sprintf(_("Speed award: %d%s ^7(%s^7)"), race_speedaward, unit, name);
2468                                 str = strcat(str, " / ");
2469                         }
2470                         name = textShortenToWidth(ColorTranslateRGB(race_speedaward_alltimebest_holder), namesize, hud_fontsize, stringwidth_colors);
2471                         str = strcat(str, sprintf(_("All-time fastest: %d%s ^7(%s^7)"), race_speedaward_alltimebest, unit, name));
2472                         drawcolorcodedstring(pos, str, hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
2473                         pos.y += 1.25 * hud_fontsize.y; // line height + line spacing
2474                 }
2475                 pos = Scoreboard_Rankings_Draw(pos, ranktitle, playerslots[player_localnum], panel_bg_color, bg_size);
2476         }
2477         else
2478                 rankings_cnt = 0;
2479
2480         // draw scoreboard spectators after rankings
2481         if (autocvar_hud_panel_scoreboard_spectators_position == 2) {
2482                 pos = Scoreboard_Spectators_Draw(pos);
2483         }
2484
2485         pos = Scoreboard_MapStats_Draw(pos, panel_bg_color, bg_size);
2486
2487         // draw scoreboard spectators after mapstats
2488         if (autocvar_hud_panel_scoreboard_spectators_position == 3) {
2489                 pos = Scoreboard_Spectators_Draw(pos);
2490         }
2491
2492
2493         // print information about respawn status
2494         float respawn_time = STAT(RESPAWN_TIME);
2495         if(!intermission)
2496         if(respawn_time)
2497         {
2498                 if(respawn_time < 0)
2499                 {
2500                         // a negative number means we are awaiting respawn, time value is still the same
2501                         respawn_time *= -1; // remove mark now that we checked it
2502
2503                         if(respawn_time < time) // it happens for a few frames when server is respawning the player
2504                                 str = ""; // draw an empty string to not change suddenly scoreboard_bottom
2505                         else
2506                                 str = sprintf(_("^1Respawning in ^3%s^1..."),
2507                                         (autocvar_hud_panel_scoreboard_respawntime_decimals ?
2508                                                 count_seconds_decs(respawn_time - time, autocvar_hud_panel_scoreboard_respawntime_decimals)
2509                                                 :
2510                                                 count_seconds(ceil(respawn_time - time))
2511                                         )
2512                                 );
2513                 }
2514                 else if(time < respawn_time)
2515                 {
2516                         str = sprintf(_("You are dead, wait ^3%s^7 before respawning"),
2517                                 (autocvar_hud_panel_scoreboard_respawntime_decimals ?
2518                                         count_seconds_decs(respawn_time - time, autocvar_hud_panel_scoreboard_respawntime_decimals)
2519                                         :
2520                                         count_seconds(ceil(respawn_time - time))
2521                                 )
2522                         );
2523                 }
2524                 else if(time >= respawn_time)
2525                         str = sprintf(_("You are dead, press ^2%s^7 to respawn"), getcommandkey("jump", "+jump"));
2526
2527                 pos.y += 1.2 * hud_fontsize.y;
2528                 drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, hud_fontsize)), str, hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
2529         }
2530
2531         pos.y += hud_fontsize.y;
2532         if (scoreboard_fade_alpha < 1)
2533                 scoreboard_bottom = scoreboard_top + (pos.y - scoreboard_top) * scoreboard_fade_alpha;
2534         else if (pos.y != scoreboard_bottom)
2535         {
2536                 if (pos.y > scoreboard_bottom)
2537                         scoreboard_bottom = min(pos.y, scoreboard_bottom + frametime * 10 * (pos.y - scoreboard_top));
2538                 else
2539                         scoreboard_bottom = max(pos.y, scoreboard_bottom - frametime * 10 * (pos.y - scoreboard_top));
2540         }
2541
2542         if (rankings_cnt)
2543         {
2544                 if (scoreboard_fade_alpha == 1)
2545                 {
2546                         if (scoreboard_bottom > 0.95 * vid_conheight)
2547                                 rankings_rows = max(1, rankings_rows - 1);
2548                         else if (scoreboard_bottom + 1.25 * hud_fontsize.y < 0.95 * vid_conheight)
2549                                 rankings_rows = min(ceil(RANKINGS_RECEIVED_CNT / rankings_columns), rankings_rows + 1);
2550                 }
2551                 rankings_cnt = rankings_rows * rankings_columns;
2552         }
2553 }