]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into TimePath/gametypes/infection
authorTimePath <andrew.hardaker1995@gmail.com>
Wed, 2 Dec 2015 04:29:15 +0000 (15:29 +1100)
committerTimePath <andrew.hardaker1995@gmail.com>
Wed, 2 Dec 2015 04:29:41 +0000 (15:29 +1100)
# Conflicts:
# qcsrc/client/hud.qc
# qcsrc/server/cl_player.qc
# qcsrc/server/mutators/mutators.qh
# qcsrc/server/mutators/mutators_include.qc
# qcsrc/server/teamplay.qc

1  2 
gamemodes.cfg
qcsrc/client/hud/panel/modicons.qc
qcsrc/common/mapinfo.qc
qcsrc/common/mapinfo.qh
qcsrc/menu/xonotic/util.qc
qcsrc/server/mutators/all.inc
qcsrc/server/mutators/mutator/gamemode_infection.qc

diff --cc gamemodes.cfg
Simple merge
index 0000000000000000000000000000000000000000,8fab80ed6170b0dc93e19877d994f64be9be6f56..92969ce6fb8badecf4b27d982d04974e10d753b1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,776 +1,785 @@@
+ // Mod icons panel (#10)
+ bool mod_active; // is there any active mod icon?
+ void DrawCAItem(vector myPos, vector mySize, float aspect_ratio, int layout, int i)
+ {
+       int stat = -1;
+       string pic = "";
+       vector color = '0 0 0';
+       switch(i)
+       {
+               case 0:
+                       stat = STAT(REDALIVE);
+                       pic = "player_red.tga";
+                       color = '1 0 0';
+                       break;
+               case 1:
+                       stat = STAT(BLUEALIVE);
+                       pic = "player_blue.tga";
+                       color = '0 0 1';
+                       break;
+               case 2:
+                       stat = STAT(YELLOWALIVE);
+                       pic = "player_yellow.tga";
+                       color = '1 1 0';
+                       break;
+               default:
+               case 3:
+                       stat = STAT(PINKALIVE);
+                       pic = "player_pink.tga";
+                       color = '1 0 1';
+                       break;
+       }
+       if(mySize.x/mySize.y > aspect_ratio)
+       {
+               i = aspect_ratio * mySize.y;
+               myPos.x = myPos.x + (mySize.x - i) / 2;
+               mySize.x = i;
+       }
+       else
+       {
+               i = 1/aspect_ratio * mySize.x;
+               myPos.y = myPos.y + (mySize.y - i) / 2;
+               mySize.y = i;
+       }
+       if(layout)
+       {
+               drawpic_aspect_skin(myPos, pic, eX * 0.7 * mySize.x + eY * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect(myPos + eX * 0.7 * mySize.x, ftos(stat), eX * 0.3 * mySize.x + eY * mySize.y, color, panel_fg_alpha, DRAWFLAG_NORMAL);
+       }
+       else
+               drawstring_aspect(myPos, ftos(stat), mySize, color, panel_fg_alpha, DRAWFLAG_NORMAL);
+ }
+ // Clan Arena and Freeze Tag HUD modicons
+ void HUD_Mod_CA(vector myPos, vector mySize)
+ {
+       mod_active = 1; // required in each mod function that always shows something
+       int layout;
+       if(gametype == MAPINFO_TYPE_CA)
+               layout = autocvar_hud_panel_modicons_ca_layout;
+       else //if(gametype == MAPINFO_TYPE_FREEZETAG)
+               layout = autocvar_hud_panel_modicons_freezetag_layout;
+       int rows, columns;
+       float aspect_ratio;
+       aspect_ratio = (layout) ? 2 : 1;
+       rows = HUD_GetRowCount(team_count, mySize, aspect_ratio);
+       columns = ceil(team_count/rows);
+       int i;
+       float row = 0, column = 0;
+       vector pos, itemSize;
+       itemSize = eX * mySize.x*(1/columns) + eY * mySize.y*(1/rows);
+       for(i=0; i<team_count; ++i)
+       {
+               pos = myPos + eX * column * itemSize.x + eY * row * itemSize.y;
+               DrawCAItem(pos, itemSize, aspect_ratio, layout, i);
+               ++row;
+               if(row >= rows)
+               {
+                       row = 0;
+                       ++column;
+               }
+       }
+ }
+ // CTF HUD modicon section
+ int redflag_prevframe, blueflag_prevframe, yellowflag_prevframe, pinkflag_prevframe, neutralflag_prevframe; // status during previous frame
+ int redflag_prevstatus, blueflag_prevstatus, yellowflag_prevstatus, pinkflag_prevstatus, neutralflag_prevstatus; // last remembered status
+ float redflag_statuschange_time, blueflag_statuschange_time, yellowflag_statuschange_time, pinkflag_statuschange_time, neutralflag_statuschange_time; // time when the status changed
+ void HUD_Mod_CTF_Reset()
+ {
+       redflag_prevstatus = blueflag_prevstatus = yellowflag_prevstatus = pinkflag_prevstatus = neutralflag_prevstatus = 0;
+       redflag_prevframe = blueflag_prevframe = yellowflag_prevframe = pinkflag_prevframe = neutralflag_prevframe = 0;
+       redflag_statuschange_time = blueflag_statuschange_time = yellowflag_statuschange_time = pinkflag_statuschange_time = neutralflag_statuschange_time = 0;
+ }
+ void HUD_Mod_CTF(vector pos, vector mySize)
+ {
+       vector redflag_pos, blueflag_pos, yellowflag_pos, pinkflag_pos, neutralflag_pos;
+       vector flag_size;
+       float f; // every function should have that
+       int redflag, blueflag, yellowflag, pinkflag, neutralflag; // current status
+       float redflag_statuschange_elapsedtime, blueflag_statuschange_elapsedtime, yellowflag_statuschange_elapsedtime, pinkflag_statuschange_elapsedtime, neutralflag_statuschange_elapsedtime; // time since the status changed
+       bool ctf_oneflag; // one-flag CTF mode enabled/disabled
+       int stat_items = STAT(CTF_FLAGSTATUS);
+       float fs, fs2, fs3, size1, size2;
+       vector e1, e2;
+       redflag = (stat_items/CTF_RED_FLAG_TAKEN) & 3;
+       blueflag = (stat_items/CTF_BLUE_FLAG_TAKEN) & 3;
+       yellowflag = (stat_items/CTF_YELLOW_FLAG_TAKEN) & 3;
+       pinkflag = (stat_items/CTF_PINK_FLAG_TAKEN) & 3;
+       neutralflag = (stat_items/CTF_NEUTRAL_FLAG_TAKEN) & 3;
+       ctf_oneflag = (stat_items & CTF_FLAG_NEUTRAL);
+       mod_active = (redflag || blueflag || yellowflag || pinkflag || neutralflag);
+       if (autocvar__hud_configure) {
+               redflag = 1;
+               blueflag = 2;
+               if (team_count >= 3)
+                       yellowflag = 2;
+               if (team_count >= 4)
+                       pinkflag = 3;
+               ctf_oneflag = neutralflag = 0; // disable neutral flag in hud editor?
+       }
+       // when status CHANGES, set old status into prevstatus and current status into status
+       #define X(team) do {                                                                                                                    \
+               if (team##flag != team##flag_prevframe) {                                                                       \
+               team##flag_statuschange_time = time;                                                                    \
+               team##flag_prevstatus = team##flag_prevframe;                                                   \
+               team##flag_prevframe = team##flag;                                                                              \
+       }                                                                                                                                                       \
+       team##flag_statuschange_elapsedtime = time - team##flag_statuschange_time;      \
+     } while (0)
+       X(red);
+       X(blue);
+       X(yellow);
+       X(pink);
+       X(neutral);
+       #undef X
+       const float BLINK_FACTOR = 0.15;
+       const float BLINK_BASE = 0.85;
+       // note:
+       //   RMS = sqrt(BLINK_BASE^2 + 0.5 * BLINK_FACTOR^2)
+       // thus
+       //   BLINK_BASE = sqrt(RMS^2 - 0.5 * BLINK_FACTOR^2)
+       // ensure RMS == 1
+       const float BLINK_FREQ = 5; // circle frequency, = 2*pi*frequency in hertz
+       #define X(team, cond) \
+       string team##_icon, team##_icon_prevstatus; \
+       int team##_alpha, team##_alpha_prevstatus; \
+       team##_alpha = team##_alpha_prevstatus = 1; \
+       do { \
+               switch (team##flag) { \
+                       case 1: team##_icon = "flag_" #team "_taken"; break; \
+                       case 2: team##_icon = "flag_" #team "_lost"; break; \
+                       case 3: team##_icon = "flag_" #team "_carrying"; team##_alpha = BLINK_BASE + BLINK_FACTOR * cos(time * BLINK_FREQ); break; \
+                       default: \
+                               if ((stat_items & CTF_SHIELDED) && (cond)) { \
+                                       team##_icon = "flag_" #team "_shielded"; \
+                               } else { \
+                                       team##_icon = string_null; \
+                               } \
+                               break; \
+               } \
+               switch (team##flag_prevstatus) { \
+                       case 1: team##_icon_prevstatus = "flag_" #team "_taken"; break; \
+                       case 2: team##_icon_prevstatus = "flag_" #team "_lost"; break; \
+                       case 3: team##_icon_prevstatus = "flag_" #team "_carrying"; team##_alpha_prevstatus = BLINK_BASE + BLINK_FACTOR * cos(time * BLINK_FREQ); break; \
+                       default: \
+                               if (team##flag == 3) { \
+                                       team##_icon_prevstatus = "flag_" #team "_carrying"; /* make it more visible */\
+                               } else if((stat_items & CTF_SHIELDED) && (cond)) { \
+                                       team##_icon_prevstatus = "flag_" #team "_shielded"; \
+                               } else { \
+                                       team##_icon_prevstatus = string_null; \
+                               } \
+                               break; \
+               } \
+       } while (0)
+       X(red, myteam != NUM_TEAM_1);
+       X(blue, myteam != NUM_TEAM_2);
+       X(yellow, myteam != NUM_TEAM_3);
+       X(pink, myteam != NUM_TEAM_4);
+       X(neutral, true);
+       #undef X
+       if (ctf_oneflag) {
+               // hacky, but these aren't needed
+               red_icon = red_icon_prevstatus = blue_icon = blue_icon_prevstatus = yellow_icon = yellow_icon_prevstatus = pink_icon = pink_icon_prevstatus = string_null;
+               fs = fs2 = fs3 = 1;
+       } else switch (team_count) {
+               default:
+               case 2: fs = 0.5; fs2 = 0.5; fs3 = 0.5; break;
+               case 3: fs = 1; fs2 = 0.35; fs3 = 0.35; break;
+               case 4: fs = 0.75; fs2 = 0.25; fs3 = 0.5; break;
+       }
+       if (mySize_x > mySize_y) {
+               size1 = mySize_x;
+               size2 = mySize_y;
+               e1 = eX;
+               e2 = eY;
+       } else {
+               size1 = mySize_y;
+               size2 = mySize_x;
+               e1 = eY;
+               e2 = eX;
+       }
+       switch (myteam) {
+               default:
+               case NUM_TEAM_1: {
+                       redflag_pos = pos;
+                       blueflag_pos = pos + eX * fs2 * size1;
+                       yellowflag_pos = pos - eX * fs2 * size1;
+                       pinkflag_pos = pos + eX * fs3 * size1;
+                       break;
+               }
+               case NUM_TEAM_2: {
+                       redflag_pos = pos + eX * fs2 * size1;
+                       blueflag_pos = pos;
+                       yellowflag_pos = pos - eX * fs2 * size1;
+                       pinkflag_pos = pos + eX * fs3 * size1;
+                       break;
+               }
+               case NUM_TEAM_3: {
+                       redflag_pos = pos + eX * fs3 * size1;
+                       blueflag_pos = pos - eX * fs2 * size1;
+                       yellowflag_pos = pos;
+                       pinkflag_pos = pos + eX * fs2 * size1;
+                       break;
+               }
+               case NUM_TEAM_4: {
+                       redflag_pos = pos - eX * fs2 * size1;
+                       blueflag_pos = pos + eX * fs3 * size1;
+                       yellowflag_pos = pos + eX * fs2 * size1;
+                       pinkflag_pos = pos;
+                       break;
+               }
+       }
+       neutralflag_pos = pos;
+       flag_size = e1 * fs * size1 + e2 * size2;
+       #define X(team) do { \
+               f = bound(0, team##flag_statuschange_elapsedtime * 2, 1); \
+               if (team##_icon_prevstatus && f < 1) \
+                       drawpic_aspect_skin_expanding(team##flag_pos, team##_icon_prevstatus, flag_size, '1 1 1', panel_fg_alpha * team##_alpha_prevstatus, DRAWFLAG_NORMAL, f); \
+               if (team##_icon) \
+                       drawpic_aspect_skin(team##flag_pos, team##_icon, flag_size, '1 1 1', panel_fg_alpha * team##_alpha * f, DRAWFLAG_NORMAL); \
+       } while (0)
+       X(red);
+       X(blue);
+       X(yellow);
+       X(pink);
+       X(neutral);
+       #undef X
+ }
+ // Keyhunt HUD modicon section
+ vector KH_SLOTS[4];
+ void HUD_Mod_KH(vector pos, vector mySize)
+ {
+       mod_active = 1; // keyhunt should never hide the mod icons panel
+       // Read current state
+       int state = STAT(KH_KEYS);
+       int i, key_state;
+       int all_keys, team1_keys, team2_keys, team3_keys, team4_keys, dropped_keys, carrying_keys;
+       all_keys = team1_keys = team2_keys = team3_keys = team4_keys = dropped_keys = carrying_keys = 0;
+       for(i = 0; i < 4; ++i)
+       {
+               key_state = (bitshift(state, i * -5) & 31) - 1;
+               if(key_state == -1)
+                       continue;
+               if(key_state == 30)
+               {
+                       ++carrying_keys;
+                       key_state = myteam;
+               }
+               switch(key_state)
+               {
+                       case NUM_TEAM_1: ++team1_keys; break;
+                       case NUM_TEAM_2: ++team2_keys; break;
+                       case NUM_TEAM_3: ++team3_keys; break;
+                       case NUM_TEAM_4: ++team4_keys; break;
+                       case 29: ++dropped_keys; break;
+               }
+               ++all_keys;
+       }
+       // Calculate slot measurements
+       vector slot_size;
+       if(all_keys == 4 && mySize.x * 0.5 < mySize.y && mySize.y * 0.5 < mySize.x)
+       {
+               // Quadratic arrangement
+               slot_size = eX * mySize.x * 0.5 + eY * mySize.y * 0.5;
+               KH_SLOTS[0] = pos;
+               KH_SLOTS[1] = pos + eX * slot_size.x;
+               KH_SLOTS[2] = pos + eY * slot_size.y;
+               KH_SLOTS[3] = pos + eX * slot_size.x + eY * slot_size.y;
+       }
+       else
+       {
+               if(mySize.x > mySize.y)
+               {
+                       // Horizontal arrangement
+                       slot_size = eX * mySize.x / all_keys + eY * mySize.y;
+                       for(i = 0; i < all_keys; ++i)
+                               KH_SLOTS[i] = pos + eX * slot_size.x * i;
+               }
+               else
+               {
+                       // Vertical arrangement
+                       slot_size = eX * mySize.x + eY * mySize.y / all_keys;
+                       for(i = 0; i < all_keys; ++i)
+                               KH_SLOTS[i] = pos + eY * slot_size.y * i;
+               }
+       }
+       // Make icons blink in case of RUN HERE
+       float blink = 0.6 + sin(2*M_PI*time) / 2.5; // Oscillate between 0.2 and 1
+       float alpha;
+       alpha = 1;
+       if(carrying_keys)
+               switch(myteam)
+               {
+                       case NUM_TEAM_1: if(team1_keys == all_keys) alpha = blink; break;
+                       case NUM_TEAM_2: if(team2_keys == all_keys) alpha = blink; break;
+                       case NUM_TEAM_3: if(team3_keys == all_keys) alpha = blink; break;
+                       case NUM_TEAM_4: if(team4_keys == all_keys) alpha = blink; break;
+               }
+       // Draw icons
+       i = 0;
+       while(team1_keys--)
+               if(myteam == NUM_TEAM_1 && carrying_keys)
+               {
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_red_carrying", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+                       --carrying_keys;
+               }
+               else
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_red_taken", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+       while(team2_keys--)
+               if(myteam == NUM_TEAM_2 && carrying_keys)
+               {
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_blue_carrying", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+                       --carrying_keys;
+               }
+               else
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_blue_taken", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+       while(team3_keys--)
+               if(myteam == NUM_TEAM_3 && carrying_keys)
+               {
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_yellow_carrying", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+                       --carrying_keys;
+               }
+               else
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_yellow_taken", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+       while(team4_keys--)
+               if(myteam == NUM_TEAM_4 && carrying_keys)
+               {
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_pink_carrying", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+                       --carrying_keys;
+               }
+               else
+                       drawpic_aspect_skin(KH_SLOTS[i++], "kh_pink_taken", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+       while(dropped_keys--)
+               drawpic_aspect_skin(KH_SLOTS[i++], "kh_dropped", slot_size, '1 1 1', alpha, DRAWFLAG_NORMAL);
+ }
++void HUD_Mod_Infection(vector pos, vector mySize)
++{
++      mod_active = 1; // Infection should always show the mod HUD
++      int f = stof(getplayerkeyvalue(player_localentnum - 1, "colors"));
++      vector color = colormapPaletteColor(floor(f / 16), 0);
++      drawpic_aspect_skin(pos, "player_neutral", mySize, color, panel_fg_alpha, DRAWFLAG_NORMAL);
++}
++
+ // Keepaway HUD mod icon
+ int kaball_prevstatus; // last remembered status
+ float kaball_statuschange_time; // time when the status changed
+ // we don't need to reset for keepaway since it immediately
+ // autocorrects prevstatus as to if the player has the ball or not
+ void HUD_Mod_Keepaway(vector pos, vector mySize)
+ {
+       mod_active = 1; // keepaway should always show the mod HUD
+       float BLINK_FACTOR = 0.15;
+       float BLINK_BASE = 0.85;
+       float BLINK_FREQ = 5;
+       float kaball_alpha = BLINK_BASE + BLINK_FACTOR * cos(time * BLINK_FREQ);
+       int stat_items = getstati(STAT_ITEMS, 0, 24);
+       int kaball = (stat_items/IT_KEY1) & 1;
+       if(kaball != kaball_prevstatus)
+       {
+               kaball_statuschange_time = time;
+               kaball_prevstatus = kaball;
+       }
+       vector kaball_pos, kaball_size;
+       if(mySize.x > mySize.y) {
+               kaball_pos = pos + eX * 0.25 * mySize.x;
+               kaball_size = eX * 0.5 * mySize.x + eY * mySize.y;
+       } else {
+               kaball_pos = pos + eY * 0.25 * mySize.y;
+               kaball_size = eY * 0.5 * mySize.y + eX * mySize.x;
+       }
+       float kaball_statuschange_elapsedtime = time - kaball_statuschange_time;
+       float f = bound(0, kaball_statuschange_elapsedtime*2, 1);
+       if(kaball_prevstatus && f < 1)
+               drawpic_aspect_skin_expanding(kaball_pos, "keepawayball_carrying", kaball_size, '1 1 1', panel_fg_alpha * kaball_alpha, DRAWFLAG_NORMAL, f);
+       if(kaball)
+               drawpic_aspect_skin(pos, "keepawayball_carrying", eX * mySize.x + eY * mySize.y, '1 1 1', panel_fg_alpha * kaball_alpha * f, DRAWFLAG_NORMAL);
+ }
+ // Nexball HUD mod icon
+ void HUD_Mod_NexBall(vector pos, vector mySize)
+ {
+       float nb_pb_starttime, dt, p;
+       int stat_items;
+       stat_items = getstati(STAT_ITEMS, 0, 24);
+       nb_pb_starttime = STAT(NB_METERSTART);
+       if (stat_items & IT_KEY1)
+               mod_active = 1;
+       else
+               mod_active = 0;
+       //Manage the progress bar if any
+       if (nb_pb_starttime > 0)
+       {
+               dt = (time - nb_pb_starttime) % nb_pb_period;
+               // one period of positive triangle
+               p = 2 * dt / nb_pb_period;
+               if (p > 1)
+                       p = 2 - p;
+               HUD_Panel_DrawProgressBar(pos, mySize, "progressbar", p, (mySize.x <= mySize.y), 0, autocvar_hud_progressbar_nexball_color, autocvar_hud_progressbar_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+       }
+       if (stat_items & IT_KEY1)
+               drawpic_aspect_skin(pos, "nexball_carrying", eX * mySize.x + eY * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+ }
+ // Race/CTS HUD mod icons
+ float crecordtime_prev; // last remembered crecordtime
+ float crecordtime_change_time; // time when crecordtime last changed
+ float srecordtime_prev; // last remembered srecordtime
+ float srecordtime_change_time; // time when srecordtime last changed
+ float race_status_time;
+ int race_status_prev;
+ string race_status_name_prev;
+ void HUD_Mod_Race(vector pos, vector mySize)
+ {
+       mod_active = 1; // race should never hide the mod icons panel
+       entity me;
+       me = playerslots[player_localnum];
+       float t, score;
+       float f; // yet another function has this
+       score = me.(scores[ps_primary]);
+       if(!(scores_flags[ps_primary] & SFL_TIME) || teamplay) // race/cts record display on HUD
+               return; // no records in the actual race
+       // clientside personal record
+       string rr;
+       if(gametype == MAPINFO_TYPE_CTS)
+               rr = CTS_RECORD;
+       else
+               rr = RACE_RECORD;
+       t = stof(db_get(ClientProgsDB, strcat(shortmapname, rr, "time")));
+       if(score && (score < t || !t)) {
+               db_put(ClientProgsDB, strcat(shortmapname, rr, "time"), ftos(score));
+               if(autocvar_cl_autodemo_delete_keeprecords)
+               {
+                       f = autocvar_cl_autodemo_delete;
+                       f &= ~1;
+                       cvar_set("cl_autodemo_delete", ftos(f)); // don't delete demo with new record!
+               }
+       }
+       if(t != crecordtime_prev) {
+               crecordtime_prev = t;
+               crecordtime_change_time = time;
+       }
+       vector textPos, medalPos;
+       float squareSize;
+       if(mySize.x > mySize.y) {
+               // text on left side
+               squareSize = min(mySize.y, mySize.x/2);
+               textPos = pos + eX * 0.5 * max(0, mySize.x/2 - squareSize) + eY * 0.5 * (mySize.y - squareSize);
+               medalPos = pos + eX * 0.5 * max(0, mySize.x/2 - squareSize) + eX * 0.5 * mySize.x + eY * 0.5 * (mySize.y - squareSize);
+       } else {
+               // text on top
+               squareSize = min(mySize.x, mySize.y/2);
+               textPos = pos + eY * 0.5 * max(0, mySize.y/2 - squareSize) + eX * 0.5 * (mySize.x - squareSize);
+               medalPos = pos + eY * 0.5 * max(0, mySize.y/2 - squareSize) + eY * 0.5 * mySize.y + eX * 0.5 * (mySize.x - squareSize);
+       }
+       f = time - crecordtime_change_time;
+       if (f > 1) {
+               drawstring_aspect(textPos, _("Personal best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect(textPos + eY * 0.25 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+       } else {
+               drawstring_aspect(textPos, _("Personal best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect(textPos + eY * 0.25 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect_expanding(pos, _("Personal best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL, f);
+               drawstring_aspect_expanding(pos + eY * 0.25 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL, f);
+       }
+       // server record
+       t = race_server_record;
+       if(t != srecordtime_prev) {
+               srecordtime_prev = t;
+               srecordtime_change_time = time;
+       }
+       f = time - srecordtime_change_time;
+       if (f > 1) {
+               drawstring_aspect(textPos + eY * 0.5 * squareSize, _("Server best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect(textPos + eY * 0.75 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+       } else {
+               drawstring_aspect(textPos + eY * 0.5 * squareSize, _("Server best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect(textPos + eY * 0.75 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawstring_aspect_expanding(textPos + eY * 0.5 * squareSize, _("Server best"), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL, f);
+               drawstring_aspect_expanding(textPos + eY * 0.75 * squareSize, TIME_ENCODED_TOSTRING(t), eX * squareSize + eY * 0.25 * squareSize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL, f);
+       }
+       if (race_status != race_status_prev || race_status_name != race_status_name_prev) {
+               race_status_time = time + 5;
+               race_status_prev = race_status;
+               if (race_status_name_prev)
+                       strunzone(race_status_name_prev);
+               race_status_name_prev = strzone(race_status_name);
+       }
+       // race "awards"
+       float a;
+       a = bound(0, race_status_time - time, 1);
+       string s;
+       s = textShortenToWidth(race_status_name, squareSize, '1 1 0' * 0.1 * squareSize, stringwidth_colors);
+       float rank;
+       if(race_status > 0)
+               rank = race_CheckName(race_status_name);
+       else
+               rank = 0;
+       string rankname;
+       rankname = count_ordinal(rank);
+       vector namepos;
+       namepos = medalPos + '0 0.8 0' * squareSize;
+       vector rankpos;
+       rankpos = medalPos + '0 0.15 0' * squareSize;
+       if(race_status == 0)
+               drawpic_aspect_skin(medalPos, "race_newfail", '1 1 0' * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+       else if(race_status == 1) {
+               drawpic_aspect_skin(medalPos + '0.1 0 0' * squareSize, "race_newtime", '1 1 0' * 0.8 * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawcolorcodedstring_aspect(namepos, s, '1 0.2 0' * squareSize, panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawstring_aspect(rankpos, rankname, '1 0.15 0' * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+       } else if(race_status == 2) {
+               if(race_status_name == entcs_GetName(player_localnum) || !race_myrank || race_myrank < rank)
+                       drawpic_aspect_skin(medalPos + '0.1 0 0' * squareSize, "race_newrankgreen", '1 1 0' * 0.8 * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               else
+                       drawpic_aspect_skin(medalPos + '0.1 0 0' * squareSize, "race_newrankyellow", '1 1 0' * 0.8 * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawcolorcodedstring_aspect(namepos, s, '1 0.2 0' * squareSize, panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawstring_aspect(rankpos, rankname, '1 0.15 0' * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+       } else if(race_status == 3) {
+               drawpic_aspect_skin(medalPos + '0.1 0 0' * squareSize, "race_newrecordserver", '1 1 0' * 0.8 * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawcolorcodedstring_aspect(namepos, s, '1 0.2 0' * squareSize, panel_fg_alpha * a, DRAWFLAG_NORMAL);
+               drawstring_aspect(rankpos, rankname, '1 0.15 0' * squareSize, '1 1 1', panel_fg_alpha * a, DRAWFLAG_NORMAL);
+       }
+       if (race_status_time - time <= 0) {
+               race_status_prev = -1;
+               race_status = -1;
+               if(race_status_name)
+                       strunzone(race_status_name);
+               race_status_name = string_null;
+               if(race_status_name_prev)
+                       strunzone(race_status_name_prev);
+               race_status_name_prev = string_null;
+       }
+ }
+ void DrawDomItem(vector myPos, vector mySize, float aspect_ratio, int layout, int i)
+ {
+       float stat = -1;
+       string pic = "";
+       vector color = '0 0 0';
+       switch(i)
+       {
+               case 0:
+                       stat = STAT(DOM_PPS_RED);
+                       pic = "dom_icon_red";
+                       color = '1 0 0';
+                       break;
+               case 1:
+                       stat = STAT(DOM_PPS_BLUE);
+                       pic = "dom_icon_blue";
+                       color = '0 0 1';
+                       break;
+               case 2:
+                       stat = STAT(DOM_PPS_YELLOW);
+                       pic = "dom_icon_yellow";
+                       color = '1 1 0';
+                       break;
+               default:
+               case 3:
+                       stat = STAT(DOM_PPS_PINK);
+                       pic = "dom_icon_pink";
+                       color = '1 0 1';
+                       break;
+       }
+       float pps_ratio = stat / STAT(DOM_TOTAL_PPS);
+       if(mySize.x/mySize.y > aspect_ratio)
+       {
+               i = aspect_ratio * mySize.y;
+               myPos.x = myPos.x + (mySize.x - i) / 2;
+               mySize.x = i;
+       }
+       else
+       {
+               i = 1/aspect_ratio * mySize.x;
+               myPos.y = myPos.y + (mySize.y - i) / 2;
+               mySize.y = i;
+       }
+       if (layout) // show text too
+       {
+               //draw the text
+               color *= 0.5 + pps_ratio * (1 - 0.5); // half saturated color at min, full saturated at max
+               if (layout == 2) // average pps
+                       drawstring_aspect(myPos + eX * mySize.y, ftos_decimals(stat, 2), eX * (2/3) * mySize.x + eY * mySize.y, color, panel_fg_alpha, DRAWFLAG_NORMAL);
+               else // percentage of average pps
+                       drawstring_aspect(myPos + eX * mySize.y, strcat( ftos(floor(pps_ratio*100 + 0.5)), "%" ), eX * (2/3) * mySize.x + eY * mySize.y, color, panel_fg_alpha, DRAWFLAG_NORMAL);
+       }
+       //draw the icon
+       drawpic_aspect_skin(myPos, pic, '1 1 0' * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+       if (stat > 0)
+       {
+               drawsetcliparea(myPos.x, myPos.y + mySize.y * (1 - pps_ratio), mySize.y, mySize.y * pps_ratio);
+               drawpic_aspect_skin(myPos, strcat(pic, "-highlighted"), '1 1 0' * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
+               drawresetcliparea();
+       }
+ }
+ void HUD_Mod_Dom(vector myPos, vector mySize)
+ {
+       mod_active = 1; // required in each mod function that always shows something
+       int layout = autocvar_hud_panel_modicons_dom_layout;
+       int rows, columns;
+       float aspect_ratio;
+       aspect_ratio = (layout) ? 3 : 1;
+       rows = HUD_GetRowCount(team_count, mySize, aspect_ratio);
+       columns = ceil(team_count/rows);
+       int i;
+       float row = 0, column = 0;
+       vector pos, itemSize;
+       itemSize = eX * mySize.x*(1/columns) + eY * mySize.y*(1/rows);
+       for(i=0; i<team_count; ++i)
+       {
+               pos = myPos + eX * column * itemSize.x + eY * row * itemSize.y;
+               DrawDomItem(pos, itemSize, aspect_ratio, layout, i);
+               ++row;
+               if(row >= rows)
+               {
+                       row = 0;
+                       ++column;
+               }
+       }
+ }
+ void HUD_ModIcons_SetFunc()
+ {
+       switch(gametype)
+       {
+               case MAPINFO_TYPE_KEYHUNT:              HUD_ModIcons_GameType = HUD_Mod_KH; break;
+               case MAPINFO_TYPE_CTF:                  HUD_ModIcons_GameType = HUD_Mod_CTF; break;
+               case MAPINFO_TYPE_NEXBALL:              HUD_ModIcons_GameType = HUD_Mod_NexBall; break;
+               case MAPINFO_TYPE_CTS:
+               case MAPINFO_TYPE_RACE:         HUD_ModIcons_GameType = HUD_Mod_Race; break;
+               case MAPINFO_TYPE_CA:
+               case MAPINFO_TYPE_FREEZETAG:    HUD_ModIcons_GameType = HUD_Mod_CA; break;
+               case MAPINFO_TYPE_DOMINATION:   HUD_ModIcons_GameType = HUD_Mod_Dom; break;
+               case MAPINFO_TYPE_KEEPAWAY:     HUD_ModIcons_GameType = HUD_Mod_Keepaway; break;
++              case MAPINFO_TYPE_INFECTION: HUD_ModIcons_GameType = HUD_Mod_Infection; break;
+       }
+ }
+ int mod_prev; // previous state of mod_active to check for a change
+ float mod_alpha;
+ float mod_change; // "time" when mod_active changed
+ void HUD_ModIcons()
+ {
+       if(!autocvar__hud_configure)
+       {
+               if(!autocvar_hud_panel_modicons) return;
+               if(!HUD_ModIcons_GameType) return;
+       }
+       HUD_Panel_UpdateCvars();
+       draw_beginBoldFont();
+       if(mod_active != mod_prev) {
+               mod_change = time;
+               mod_prev = mod_active;
+       }
+       if(mod_active || autocvar__hud_configure)
+               mod_alpha = bound(0, (time - mod_change) * 2, 1);
+       else
+               mod_alpha = bound(0, 1 - (time - mod_change) * 2, 1);
+       if(mod_alpha)
+               HUD_Panel_DrawBg(mod_alpha);
+       if(panel_bg_padding)
+       {
+               panel_pos += '1 1 0' * panel_bg_padding;
+               panel_size -= '2 2 0' * panel_bg_padding;
+       }
+       if(autocvar__hud_configure)
+               HUD_Mod_CTF(panel_pos, panel_size);
+       else
+               HUD_ModIcons_GameType(panel_pos, panel_size);
+       draw_endBoldFont();
+ }
index ebcd8bf3c27393496c0083c235dcb73c43b6afcd,5ca1c3803da30ea19467f6daaa21074ec4ac2d74..73bf7b7c652dc82f02b2b4b128640e0df35dd541
@@@ -518,14 -538,6 +538,13 @@@ void _MapInfo_Map_ApplyGametype(string 
                        cvar_set("fraglimit", sa);
                s = cdr(s);
        }
-       if(pWantedType == MAPINFO_TYPE_INFECTION)
 +      
-               if(sa != "")
-                       cvar_set("fraglimit", sa);
++      if (pWantedType == MAPINFO_TYPE_INFECTION)
 +      {
 +              sa = car(s);
++              if (sa != "") cvar_set("fraglimit", sa);
 +              s = cdr(s);
 +      }
  
        /* keepaway wuz here
        if(pWantedType == MAPINFO_TYPE_KEEPAWAY)
@@@ -899,7 -857,7 +864,7 @@@ float MapInfo_isRedundant(string fn, st
  }
  
  // load info about a map by name into the MapInfo_Map_* globals
--float MapInfo_Get_ByName_NoFallbacks(string pFilename, int pAllowGenerate, int pGametypeToSet)
++float MapInfo_Get_ByName_NoFallbacks(string pFilename, bool pAllowGenerate, int pGametypeToSet)
  {
        string fn;
        string s, t;
        MapInfo_Cache_Store();
        if(MapInfo_Map_supportedGametypes != 0)
                return r;
-       dprint("Map ", pFilename, " supports no game types, ignored\n");
+       LOG_TRACE("Map ", pFilename, " supports no game types, ignored\n");
        return 0;
  }
--float MapInfo_Get_ByName(string pFilename, float pAllowGenerate, int pGametypeToSet)
++float MapInfo_Get_ByName(string pFilename, bool pAllowGenerate, int pGametypeToSet)
  {
        float r = MapInfo_Get_ByName_NoFallbacks(pFilename, pAllowGenerate, pGametypeToSet);
  
                        if(MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DEATHMATCH)
                                _MapInfo_Map_ApplyGametypeEx ("", pGametypeToSet, MAPINFO_TYPE_TEAM_DEATHMATCH);
        }
++      _MapInfo_Map_ApplyGametypeEx("", pGametypeToSet, MAPINFO_TYPE_INFECTION);  // infection always works
  
        if(pGametypeToSet)
        {
@@@ -1277,15 -1241,15 +1249,15 @@@ int MapInfo_CurrentGametype(
        return MAPINFO_TYPE_DEATHMATCH;
  }
  
--float _MapInfo_CheckMap(string s) // returns 0 if the map can't be played with the current settings, 1 otherwise
++bool _MapInfo_CheckMap(string s) // returns false if the map can't be played with the current settings, true otherwise
  {
--      if(!MapInfo_Get_ByName(s, 1, 0))
--              return 0;
++      if(!MapInfo_Get_ByName(s, true, 0))
++              return false;
        if((MapInfo_Map_supportedGametypes & MapInfo_CurrentGametype()) == 0)
--              return 0;
++              return false;
        if((MapInfo_Map_supportedFeatures & MapInfo_CurrentFeatures()) != MapInfo_CurrentFeatures())
--              return 0;
--      return 1;
++              return false;
++      return true;
  }
  
  float MapInfo_CheckMap(string s) // returns 0 if the map can't be played with the current settings, 1 otherwise
@@@ -1362,9 -1326,9 +1334,7 @@@ void MapInfo_LoadMapSettings_SaveGameTy
  
  void MapInfo_LoadMapSettings(string s) // to be called from worldspawn
  {
--      float t;
--
--      t = MapInfo_CurrentGametype();
++      int t = MapInfo_CurrentGametype();
        MapInfo_LoadMapSettings_SaveGameType(t);
  
        if(!_MapInfo_CheckMap(s)) // with underscore, it keeps temps
index 87cc3827811b7f97611c66d0f8d92807f662d907,702798bbf861d3bfcb912ceac3b67a6afcd5052b..6e2949914b88de00518ed873d1e920cfd1b8e024
  
  #include "util.qh"
  
 -REGISTRY(Gametypes, BITS(4))
+ CLASS(Gametype, Object)
+     ATTRIB(Gametype, m_id, int, 0)
+     /** game type ID */
+     ATTRIB(Gametype, items, int, 0)
+     /** game type name as in cvar (with g_ prefix) */
+     ATTRIB(Gametype, netname, string, string_null)
+     /** game type short name */
+     ATTRIB(Gametype, mdl, string, string_null)
+     /** human readable name */
+     ATTRIB(Gametype, message, string, string_null)
+     /** does this gametype support teamplay? */
+     ATTRIB(Gametype, team, bool, false)
+     /** game type defaults */
+     ATTRIB(Gametype, model2, string, string_null)
+     /** game type description */
+     ATTRIB(Gametype, gametype_description, string, string_null)
+     ATTRIB(Gametype, m_mutators, string, string_null)
+     ATTRIB(Gametype, m_parse_mapinfo, bool(string k, string v), func_null)
+     METHOD(Gametype, describe, string(entity this)) { return this.gametype_description; }
+     METHOD(Gametype, display, void(entity this, void(string name, string icon) returns)) {
+         returns(this.message, strcat("gametype_", this.mdl));
+     }
+     CONSTRUCTOR(Gametype, string hname, string sname, string g_name, bool gteamplay, string mutators, string defaults, string gdescription)
+     {
+         CONSTRUCT(Gametype);
+         this.netname = g_name;
+         this.mdl = sname;
+         this.message = hname;
+         this.team = gteamplay;
+         this.m_mutators = mutators;
+         this.model2 = defaults;
+         this.gametype_description = gdescription;
+     }
+ ENDCLASS(Gametype)
++REGISTRY(Gametypes, BITS(5))
+ #define Gametypes_from(i) _Gametypes_from(i, NULL)
+ REGISTER_REGISTRY(Gametypes)
+ REGISTRY_CHECK(Gametypes)
  int MAPINFO_TYPE_ALL;
- entity MapInfo_Type_first;
- entity MapInfo_Type_last;
- .entity enemy; // internal next pointer
- .int items; // game type ID
- .string netname; // game type name as in cvar (with g_ prefix)
- .string mdl; // game type short name
- .string message; // human readable name
- .int team; // does this gametype support teamplay?
- .string model2; // game type defaults
- .string gametype_description; // game type description
- #define REGISTER_GAMETYPE(hname,sname,g_name,NAME,gteamplay,defaults,gdescription) \
-       int MAPINFO_TYPE_##NAME; \
-       entity MapInfo_Type##g_name; \
-       void RegisterGametypes_##g_name() \
-       { \
-               MAPINFO_TYPE_##NAME = MAPINFO_TYPE_ALL + 1; \
-               MAPINFO_TYPE_ALL |= MAPINFO_TYPE_##NAME; \
-               MapInfo_Type##g_name = spawn(); \
-               MapInfo_Type##g_name.items = MAPINFO_TYPE_##NAME; \
-               MapInfo_Type##g_name.netname = #g_name; \
-               MapInfo_Type##g_name.mdl = #sname; \
-               MapInfo_Type##g_name.message = hname; \
-               MapInfo_Type##g_name.team = gteamplay; \
-               MapInfo_Type##g_name.model2 = defaults; \
-               MapInfo_Type##g_name.gametype_description = gdescription; \
-               if(!MapInfo_Type_first) \
-                       MapInfo_Type_first = MapInfo_Type##g_name; \
-               if(MapInfo_Type_last) \
-                       MapInfo_Type_last.enemy = MapInfo_Type##g_name; \
-               MapInfo_Type_last = MapInfo_Type##g_name; \
-       } \
-       ACCUMULATE_FUNCTION(RegisterGametypes, RegisterGametypes_##g_name)
+ #define REGISTER_GAMETYPE(hname, sname, g_name, NAME, gteamplay, mutators, defaults, gdescription)          \
+     int MAPINFO_TYPE_##NAME;                                                                                \
+     bool NAME##_mapinfo(string k, string v) { return = false; }                                             \
+     REGISTER(Gametypes, MAPINFO_TYPE, g_name, m_id,                                      \
+         NEW(Gametype, hname, #sname, #g_name, gteamplay, #sname " " mutators, defaults, gdescription)       \
+     ) {                                                                                                     \
+         /* same as `1 << m_id` */                                                                           \
+         MAPINFO_TYPE_##NAME = MAPINFO_TYPE_ALL + 1; MAPINFO_TYPE_ALL |= MAPINFO_TYPE_##NAME;                \
+         this.items = MAPINFO_TYPE_##NAME;                                                                   \
+         this.m_parse_mapinfo = NAME##_mapinfo;                                                              \
+     }                                                                                                       \
+     [[accumulate]] bool NAME##_mapinfo(string k, string v)
  
  #define IS_GAMETYPE(NAME) \
-       (MapInfo_LoadedGametype == MAPINFO_TYPE_##NAME)
- REGISTER_GAMETYPE(_("Deathmatch"),dm,g_dm,DEATHMATCH,false,"timelimit=20 pointlimit=30 leadlimit=0",_("Kill all enemies"));
- #define g_dm IS_GAMETYPE(DEATHMATCH)
- REGISTER_GAMETYPE(_("Last Man Standing"),lms,g_lms,LMS,false,"timelimit=20 lives=9 leadlimit=0",_("Survive and kill until the enemies have no lives left"));
- #define g_lms IS_GAMETYPE(LMS)
- REGISTER_GAMETYPE(_("Race"),rc,g_race,RACE,false,"timelimit=20 qualifying_timelimit=5 laplimit=7 teamlaplimit=15 leadlimit=0",_("Race against other players to the finish line"));
+     (MapInfo_LoadedGametype == MAPINFO_TYPE_##NAME)
+ REGISTER_GAMETYPE(_("Deathmatch"),dm,g_dm,DEATHMATCH,false,"","timelimit=20 pointlimit=30 leadlimit=0",_("Score as many frags as you can"));
+ REGISTER_GAMETYPE(_("Last Man Standing"),lms,g_lms,LMS,false,"","timelimit=20 lives=9 leadlimit=0",_("Survive and kill until the enemies have no lives left"));
+ REGISTER_GAMETYPE(_("Race"),rc,g_race,RACE,false,"","timelimit=20 qualifying_timelimit=5 laplimit=7 teamlaplimit=15 leadlimit=0",_("Race against other players to the finish line"))
+ {
+     if (!k) {
+       cvar_set("g_race_qualifying_timelimit", cvar_defstring("g_race_qualifying_timelimit"));
+         return true;
+     }
+     switch (k) {
+         case "qualifying_timelimit":
+             cvar_set("g_race_qualifying_timelimit", v);
+             return true;
+     }
+ }
  #define g_race IS_GAMETYPE(RACE)
  
- REGISTER_GAMETYPE(_("Race CTS"),cts,g_cts,CTS,false,"timelimit=20 skill=-1",_("Race for fastest time"));
+ REGISTER_GAMETYPE(_("Race CTS"),cts,g_cts,CTS,false,"cloaked","timelimit=20",_("Race for fastest time."));
  #define g_cts IS_GAMETYPE(CTS)
  
- REGISTER_GAMETYPE(_("Team Deathmatch"),tdm,g_tdm,TEAM_DEATHMATCH,true,"timelimit=20 pointlimit=50 teams=2 leadlimit=0",_("Kill all enemy teammates"));
+ REGISTER_GAMETYPE(_("Team Deathmatch"),tdm,g_tdm,TEAM_DEATHMATCH,true,"","timelimit=20 pointlimit=50 teams=2 leadlimit=0",_("Help your team score the most frags against the enemy team"))
+ {
+     if (!k) {
+         cvar_set("g_tdm_teams", cvar_defstring("g_tdm_teams"));
+         return true;
+     }
+     switch (k) {
+         case "teams":
+             cvar_set("g_tdm_teams", v);
+             return true;
+     }
+ }
  #define g_tdm IS_GAMETYPE(TEAM_DEATHMATCH)
  
- REGISTER_GAMETYPE(_("Capture the Flag"),ctf,g_ctf,CTF,true,"timelimit=20 caplimit=10 leadlimit=6",_("Find and bring the enemy flag to your base to capture it"));
+ REGISTER_GAMETYPE(_("Capture the Flag"),ctf,g_ctf,CTF,true,"","timelimit=20 caplimit=10 leadlimit=6",_("Find and bring the enemy flag to your base to capture it, defend your base from the other team"));
  #define g_ctf IS_GAMETYPE(CTF)
  
- REGISTER_GAMETYPE(_("Clan Arena"),ca,g_ca,CA,true,"timelimit=20 pointlimit=10 teams=2 leadlimit=0",_("Kill all enemy teammates to win the round"));
+ REGISTER_GAMETYPE(_("Clan Arena"),ca,g_ca,CA,true,"","timelimit=20 pointlimit=10 teams=2 leadlimit=0",_("Kill all enemy teammates to win the round"))
+ {
+     if (!k) {
+         cvar_set("g_ca_teams", cvar_defstring("g_ca_teams"));
+         return true;
+     }
+     switch (k) {
+         case "teams":
+             cvar_set("g_ca_teams", v);
+             return true;
+     }
+ }
  #define g_ca IS_GAMETYPE(CA)
  
- REGISTER_GAMETYPE(_("Domination"),dom,g_domination,DOMINATION,true,"timelimit=20 pointlimit=200 teams=2 leadlimit=0",_("Capture all the control points to win"));
- #define g_domination IS_GAMETYPE(DOMINATION)
- REGISTER_GAMETYPE(_("Key Hunt"),kh,g_keyhunt,KEYHUNT,true,"timelimit=20 pointlimit=1000 teams=3 leadlimit=0",_("Gather all the keys to win the round"));
- #define g_keyhunt IS_GAMETYPE(KEYHUNT)
- REGISTER_GAMETYPE(_("Assault"),as,g_assault,ASSAULT,true,"timelimit=20",_("Destroy obstacles to find and destroy the enemy power core before time runs out"));
+ REGISTER_GAMETYPE(_("Domination"),dom,g_domination,DOMINATION,true,"","timelimit=20 pointlimit=200 teams=2 leadlimit=0",_("Capture and defend all the control points to win"))
+ {
+     if (!k) {
+         cvar_set("g_domination_default_teams", cvar_defstring("g_domination_default_teams"));
+         return true;
+     }
+     switch (k) {
+         case "teams":
+             cvar_set("g_domination_default_teams", v);
+             return true;
+     }
+ }
+ REGISTER_GAMETYPE(_("Key Hunt"),kh,g_keyhunt,KEYHUNT,true,"","timelimit=20 pointlimit=1000 teams=3 leadlimit=0",_("Gather all the keys to win the round"))
+ {
+     if (!k) {
+       cvar_set("g_keyhunt_teams", cvar_defstring("g_keyhunt_teams"));
+       return true;
+     }
+     switch (k) {
+         case "teams":
+             cvar_set("g_keyhunt_teams", v);
+             return true;
+     }
+ }
+ REGISTER_GAMETYPE(_("Assault"),as,g_assault,ASSAULT,true,"","timelimit=20",_("Destroy obstacles to find and destroy the enemy power core before time runs out"));
  #define g_assault IS_GAMETYPE(ASSAULT)
  
- REGISTER_GAMETYPE(_("Onslaught"),ons,g_onslaught,ONSLAUGHT,true,"timelimit=20",_("Capture control points to reach and destroy the enemy generator"));
- #define g_onslaught IS_GAMETYPE(ONSLAUGHT)
+ REGISTER_GAMETYPE(_("Onslaught"),ons,g_onslaught,ONSLAUGHT,true,"","pointlimit=1 timelimit=20",_("Capture control points to reach and destroy the enemy generator"));
  
- REGISTER_GAMETYPE(_("Nexball"),nb,g_nexball,NEXBALL,true,"timelimit=20 pointlimit=5 leadlimit=0",_("XonSports"));
+ REGISTER_GAMETYPE(_("Nexball"),nb,g_nexball,NEXBALL,true,"","timelimit=20 pointlimit=5 leadlimit=0",_("Shoot and kick the ball into the enemies goal, keep your goal clean"));
  #define g_nexball IS_GAMETYPE(NEXBALL)
  
- REGISTER_GAMETYPE(_("Freeze Tag"),ft,g_freezetag,FREEZETAG,true,"timelimit=20 pointlimit=10 teams=2 leadlimit=0",_("Kill enemies to freeze them, stand next to teammates to revive them"));
+ REGISTER_GAMETYPE(_("Freeze Tag"),ft,g_freezetag,FREEZETAG,true,"","timelimit=20 pointlimit=10 teams=2 leadlimit=0",_("Kill enemies to freeze them, stand next to teammates to revive them, freeze the most enemies to win"))
+ {
+     if (!k) {
+         cvar_set("g_freezetag_teams", cvar_defstring("g_freezetag_teams"));
+         return true;
+     }
+     switch (k) {
+         case "teams":
+             cvar_set("g_freezetag_teams", v);
+             return true;
+     }
+ }
  #define g_freezetag IS_GAMETYPE(FREEZETAG)
  
- REGISTER_GAMETYPE(_("Keepaway"),ka,g_keepaway,KEEPAWAY,true,"timelimit=20 pointlimit=30",_("Hold the ball to get points for kills"));
- #define g_keepaway IS_GAMETYPE(KEEPAWAY)
+ REGISTER_GAMETYPE(_("Keepaway"),ka,g_keepaway,KEEPAWAY,true,"","timelimit=20 pointlimit=30",_("Hold the ball to get points for kills"));
  
- REGISTER_GAMETYPE(_("Invasion"),inv,g_invasion,INVASION,false,"pointlimit=50 teams=0",_("Survive against waves of monsters"));
- #define g_invasion IS_GAMETYPE(INVASION)
+ REGISTER_GAMETYPE(_("Invasion"),inv,g_invasion,INVASION,false,"","pointlimit=50 teams=0",_("Survive against waves of monsters"))
+ {
+     switch (k) {
+         case "teams":
+             cvar_set("g_invasion_teams", v);
+             return true;
+     }
+ }
  
- REGISTER_GAMETYPE(_("Infection"),inf,g_infection,INFECTION,true,"timelimit=20",_("Survive against the infection"))
- #define g_infection IS_GAMETYPE(INFECTION)
++REGISTER_GAMETYPE(_("Infection"),inf,g_infection,INFECTION,true,"","timelimit=20",_("Survive against the infection"));
 +
  const int MAPINFO_FEATURE_WEAPONS       = 1; // not defined for instagib-only maps
  const int MAPINFO_FEATURE_VEHICLES      = 2;
  const int MAPINFO_FEATURE_TURRETS       = 4;
index 10bf7468a8c8cc2e9746db65022c66e0f65e4cb4,b879257b7f7be8712694155a04ab66cff2c3509c..34bc2264b5beb10f57910e2019018b51180cfb06
@@@ -665,13 -667,11 +667,12 @@@ float updateCompression(
  
  // note: include only those that should be in the menu!
  #define GAMETYPES \
-       GAMETYPE(MAPINFO_TYPE_ASSAULT) \
+       GAMETYPE(MAPINFO_TYPE_DEATHMATCH) \
+       GAMETYPE(MAPINFO_TYPE_TEAM_DEATHMATCH) \
        GAMETYPE(MAPINFO_TYPE_CTF) \
        GAMETYPE(MAPINFO_TYPE_CA) \
-       GAMETYPE(MAPINFO_TYPE_DEATHMATCH) \
-       GAMETYPE(MAPINFO_TYPE_DOMINATION) \
        GAMETYPE(MAPINFO_TYPE_FREEZETAG) \
 +      GAMETYPE(MAPINFO_TYPE_INFECTION) \
        GAMETYPE(MAPINFO_TYPE_KEEPAWAY) \
        GAMETYPE(MAPINFO_TYPE_KEYHUNT) \
        GAMETYPE(MAPINFO_TYPE_LMS) \
index 0000000000000000000000000000000000000000,1a80b440947a48ca55ec7d839ba1ec3853167ca5..cb709f3baafb4de87dd40be263e47d11042ef2f1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,13 +1,14 @@@
+ #include "mutator/gamemode_assault.qc"
+ #include "mutator/gamemode_ca.qc"
+ #include "mutator/gamemode_ctf.qc"
+ #include "mutator/gamemode_cts.qc"
+ #include "mutator/gamemode_deathmatch.qc"
+ #include "mutator/gamemode_domination.qc"
+ #include "mutator/gamemode_freezetag.qc"
++#include "mutator/gamemode_infection.qc"
+ #include "mutator/gamemode_invasion.qc"
+ #include "mutator/gamemode_keepaway.qc"
+ #include "mutator/gamemode_keyhunt.qc"
+ #include "mutator/gamemode_lms.qc"
+ #include "mutator/gamemode_race.qc"
+ #include "mutator/gamemode_tdm.qc"
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..4af6e488b7a340512884265347193d0f2b60362b
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,261 @@@
++#ifdef IMPLEMENTATION
++
++float autocvar_g_infection_round_timelimit;
++float autocvar_g_infection_warmup;
++int autocvar_g_infection_teams;
++bool autocvar_g_infection_conversions;
++
++int infection_players_count;
++
++.int infectioncolor;
++.int infectioncolor_original;
++
++const int INFECTIONTEAM_NONE = -1;
++const int INFECTIONTEAM_UNDEFINED = -2;
++
++// safe team comparisons
++#define INF_SAMETEAM(a,b) (a.infectioncolor == b.infectioncolor)
++#define INF_DIFFTEAM(a,b) (a.infectioncolor != b.infectioncolor)
++
++void infection_SetColor(entity e, int _color)
++{
++      e.infectioncolor = _color;
++      setcolor(e, (_color << 4) | _color);
++}
++
++string color_owner_green, color_owner_red;
++// find whose color a player is carrying, true if it's his own, otherwise set color_owner_* to the other owner
++void infection_GetColorOwner(entity me)
++{
++      if (me.infectioncolor == me.infectioncolor_original) {
++              color_owner_green = "^2your own";
++              color_owner_red = "^1their own";
++              return;
++      }
++      entity e;
++      FOR_EACH_PLAYER(e) {
++              if (me.infectioncolor == e.infectioncolor_original) {
++                      color_owner_green = sprintf("%s^2's", e.netname);
++                      color_owner_red = sprintf("%s^1's", e.netname);
++                      break;
++              }
++      }
++}
++
++void infection_Assign(bool late)
++{
++      if (!late) {
++              int infection_coloridx = 0;
++              entity e;
++              FOR_EACH_PLAYER(e) {
++                      if (infection_coloridx < autocvar_g_infection_teams) { // Limit alphas
++                              e.infectioncolor_original = infection_coloridx;
++                      }
++                      infection_SetColor(e, infection_coloridx++ % bound(0, autocvar_g_infection_teams, 15));
++              }
++      } else {
++              // Spawn too late, give player a random color
++              int color = 15;
++              int skip = rint(random() * (infection_players_count - 1)); // Ignore self
++              entity e;
++              FOR_EACH_PLAYER(e) {
++                      if (e == self || IS_OBSERVER(e)) continue;
++                      if (!skip --> 0) break;
++              }
++              dprintf("[INFECTION] copying %s's color\n", e.netname);
++              color = e.infectioncolor;
++              self.infectioncolor_original = INFECTIONTEAM_NONE; // Can't win if player didn't spawn during the round delay
++              infection_SetColor(self, color);
++    }
++}
++
++bool infection_CheckTeams()
++{
++      return infection_players_count > 1;
++}
++
++bool infection_CheckWinner()
++{
++      if (infection_players_count <= 1) return false; // There can be no winner
++
++      if (0 < round_handler_GetEndTime() && round_handler_GetEndTime() <= time) { // Round over
++              Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
++              Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
++              round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit);
++              return true;
++      }
++
++      // Check if only one color remains
++      int previnfectioncolor = -1;
++      bool we_have_a_winner = true; // until loop below proves us wrong
++      entity e;
++      FOR_EACH_PLAYER(e) {
++              // All infection colors are the same if we have a winner
++              if (previnfectioncolor != -1 && previnfectioncolor != e.infectioncolor) {
++                      // In this case we still have more than one color alive
++                      we_have_a_winner = false;
++                      break;
++              }
++              previnfectioncolor = e.infectioncolor;
++      }
++
++      if (!we_have_a_winner) return false;
++
++      // Who is it?
++      FOR_EACH_PLAYER(e) {
++              if (e.infectioncolor == e.infectioncolor_original) {
++                      UpdateFrags(e, 10); // Bonus points
++                      Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_PLAYER_WIN, e.netname);
++                      Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_PLAYER_WIN, e.netname);
++              }
++      }
++
++      round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit);
++      return true;
++}
++
++void infection_RoundStart()
++{
++      infection_Assign(false);
++      infection_CheckWinner();
++}
++
++MUTATOR_HOOKFUNCTION(inf, PlayerDies)
++{
++      infection_CheckWinner();
++      return true;
++}
++
++bool inf_RemovePlayer();
++MUTATOR_HOOKFUNCTION(inf, MakePlayerObserver) { return inf_RemovePlayer(); }
++MUTATOR_HOOKFUNCTION(inf, ClientDisconnect) { return inf_RemovePlayer(); }
++bool inf_RemovePlayer()
++{
++      if (!IS_PLAYER(self)) return true; // Wasn't playing
++
++      infection_players_count--;
++
++      self.infectioncolor_original = INFECTIONTEAM_UNDEFINED;
++
++      // Grant alpha status to next of kin
++      entity e;
++      FOR_EACH_PLAYER(e) {
++              if (e.infectioncolor == self.infectioncolor_original) {
++                      e.infectioncolor_original = self.infectioncolor;
++                      centerprint(e, "^2You are now an alpha.\n");
++                      break;
++              }
++      }
++
++      infection_CheckWinner();
++      return true;
++}
++
++MUTATOR_HOOKFUNCTION(inf, PlayerSpawn)
++{
++      if (self.infectioncolor_original != INFECTIONTEAM_UNDEFINED) return true; // Wasn't observing
++      infection_players_count++;
++
++      infection_Assign(true);
++
++      return true;
++}
++
++MUTATOR_HOOKFUNCTION(inf, GiveFragsForKill, CBC_ORDER_FIRST)
++{
++      frag_score = 0;
++      infection_GetColorOwner(frag_attacker);
++      // If this is the first time we die... (our infectioncolor remained unchanged)
++      if (autocvar_g_infection_conversions && frag_target.infectioncolor == frag_target.infectioncolor_original) {
++              entity e;
++              FOR_EACH_PLAYER(e) { // check other players...
++                      if (INF_SAMETEAM(e, frag_target)) { // And see if they have our original infection color
++                              // If so, remove it, our infection color has now "died out" from this round and we can not win anymore.
++                              // The attacker will "summon" all of our previously fragged targets, and also us.
++                              centerprint(e, sprintf("^1Your alpha ^7%s^1 was infected by ^7%s^1 with ^7%s^1 color.\n",
++                                      frag_target.netname, frag_attacker.netname, color_owner_red));
++                              infection_SetColor(e, frag_attacker.infectioncolor);
++                              frag_score++;
++                      }
++              }
++      } else {
++              infection_SetColor(frag_target, frag_attacker.infectioncolor);
++              frag_score++;
++      }
++
++      string target = frag_target.netname, attacker = frag_attacker.netname;
++
++      centerprint(frag_attacker, sprintf("^2You infected ^7%s^2 with ^7%s^2 color.\n", target, color_owner_green));
++      centerprint(frag_target, sprintf("^1You were infected by ^7%s^1 with ^7%s^1 color.\n", attacker, color_owner_red));
++
++      bprint(sprintf("^7%s^1 was infected by ^7%s^1 with ^7%s^1 color.\n", frag_target.netname, attacker, color_owner_red));
++
++      return true;
++}
++
++MUTATOR_HOOKFUNCTION(inf, PlayerPreThink, CBC_ORDER_FIRST)
++{
++      if (IS_PLAYER(self)) {
++              // Prevent cheating by changing player colors
++              infection_SetColor(self, round_handler_IsRoundStarted()
++                      ? self.infectioncolor : 15
++              );
++      }
++      return true;
++}
++
++MUTATOR_HOOKFUNCTION(inf, PlayerDamage_Calculate)
++{
++      if (IS_PLAYER(frag_attacker) // Allow environment damage
++              && frag_attacker != frag_target  // Allow self damage
++              && INF_SAMETEAM(frag_attacker, frag_target) // Block friendly fire
++      ) {
++              frag_damage = 0;
++              frag_force = '0 0 0';
++      }
++      return false;
++}
++
++MUTATOR_HOOKFUNCTION(inf, BotShouldAttack)
++{
++      return INF_SAMETEAM(checkentity, self);
++}
++
++MUTATOR_HOOKFUNCTION(inf, ClientConnect)
++{
++      self.infectioncolor_original = INFECTIONTEAM_UNDEFINED;
++      stuffcmd(self, "settemp cl_forceplayercolors 0\n");
++
++      return false;
++}
++
++REGISTER_MUTATOR(inf, false)
++{
++      MUTATOR_ONADD
++      {
++              if (time > 1)  // game loads at time 1
++              {
++                      error("This is a game type and it cannot be added at runtime.");
++              }
++              infection_players_count = 0;
++              round_handler_Spawn(infection_CheckTeams, infection_CheckWinner, infection_RoundStart);
++              round_handler_Init(5, autocvar_g_infection_warmup, autocvar_g_infection_round_timelimit);
++      }
++
++      MUTATOR_ONROLLBACK_OR_REMOVE
++      {
++              // we actually cannot roll back inf_Initialize here
++              // BUT: we don't need to! If this gets called, adding always
++              // succeeds.
++      }
++
++      MUTATOR_ONREMOVE
++      {
++              print("This is a game type and it cannot be removed at runtime.");
++              return -1;
++      }
++
++      return 0;
++}
++
++#endif