]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Merge branch 'terencehill/hud_cleanups' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_ctf.qc
1 #ifndef GAMEMODE_CTF_H
2 #define GAMEMODE_CTF_H
3
4 #ifndef CSQC
5 void ctf_Initialize();
6
7 REGISTER_MUTATOR(ctf, false)
8 {
9         ActivateTeamplay();
10         SetLimits(autocvar_capturelimit_override, -1, autocvar_captureleadlimit_override, -1);
11         have_team_spawns = -1; // request team spawns
12
13         MUTATOR_ONADD
14         {
15                 if (time > 1) // game loads at time 1
16                         error("This is a game type and it cannot be added at runtime.");
17                 ctf_Initialize();
18         }
19
20         MUTATOR_ONROLLBACK_OR_REMOVE
21         {
22                 // we actually cannot roll back ctf_Initialize here
23                 // BUT: we don't need to! If this gets called, adding always
24                 // succeeds.
25         }
26
27         MUTATOR_ONREMOVE
28         {
29                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
30                 return -1;
31         }
32
33         return 0;
34 }
35 #endif
36
37 #ifdef SVQC
38 // used in cheats.qc
39 void ctf_RespawnFlag(entity flag);
40
41 // score rule declarations
42 const int ST_CTF_CAPS = 1;
43 const int SP_CTF_CAPS = 4;
44 const int SP_CTF_CAPTIME = 5;
45 const int SP_CTF_PICKUPS = 6;
46 const int SP_CTF_DROPS = 7;
47 const int SP_CTF_FCKILLS = 8;
48 const int SP_CTF_RETURNS = 9;
49
50 // flag constants // for most of these, there is just one question to be asked: WHYYYYY?
51 #define FLAG_MIN (PL_MIN_CONST + '0 0 -13')
52 #define FLAG_MAX (PL_MAX_CONST + '0 0 -13')
53
54 const float FLAG_SCALE = 0.6;
55
56 const float FLAG_THINKRATE = 0.2;
57 const float FLAG_TOUCHRATE = 0.5;
58 const float WPFE_THINKRATE = 0.5;
59
60 const vector FLAG_DROP_OFFSET = ('0 0 32');
61 const vector FLAG_CARRY_OFFSET = ('-16 0 8');
62 #define FLAG_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13))
63 const vector FLAG_WAYPOINT_OFFSET = ('0 0 64');
64 const vector FLAG_FLOAT_OFFSET = ('0 0 32');
65 const vector FLAG_PASS_ARC_OFFSET = ('0 0 -10');
66
67 const vector VEHICLE_FLAG_OFFSET = ('0 0 96');
68 const float VEHICLE_FLAG_SCALE = 1.0;
69
70 // waypoint colors
71 #define WPCOLOR_ENEMYFC(t) ((t) ? colormapPaletteColor(t - 1, false) * 0.75 : '1 1 1')
72 #define WPCOLOR_FLAGCARRIER(t) (WP_FlagCarrier.m_color)
73 #define WPCOLOR_DROPPEDFLAG(t) ((t) ? ('0.25 0.25 0.25' + colormapPaletteColor(t - 1, false)) * 0.5 : '1 1 1')
74
75 // sounds
76 #define snd_flag_taken noise
77 #define snd_flag_returned noise1
78 #define snd_flag_capture noise2
79 #define snd_flag_respawn noise3
80 .string snd_flag_dropped;
81 .string snd_flag_touch;
82 .string snd_flag_pass;
83
84 // effects
85 .string toucheffect;
86 .string passeffect;
87 .string capeffect;
88
89 // list of flags on the map
90 entity ctf_worldflaglist;
91 .entity ctf_worldflagnext;
92 .entity ctf_staleflagnext;
93
94 // waypoint sprites
95 .entity bot_basewaypoint; // flag waypointsprite
96 .entity wps_helpme;
97 .entity wps_flagbase;
98 .entity wps_flagcarrier;
99 .entity wps_flagdropped;
100 .entity wps_enemyflagcarrier;
101 .float wps_helpme_time;
102 bool wpforenemy_announced;
103 float wpforenemy_nextthink;
104
105 // statuses
106 const int FLAG_BASE = 1;
107 const int FLAG_DROPPED = 2;
108 const int FLAG_CARRY = 3;
109 const int FLAG_PASSING = 4;
110
111 const int DROP_NORMAL = 1;
112 const int DROP_THROW = 2;
113 const int DROP_PASS = 3;
114 const int DROP_RESET = 4;
115
116 const int PICKUP_BASE = 1;
117 const int PICKUP_DROPPED = 2;
118
119 const int CAPTURE_NORMAL = 1;
120 const int CAPTURE_DROPPED = 2;
121
122 const int RETURN_TIMEOUT = 1;
123 const int RETURN_DROPPED = 2;
124 const int RETURN_DAMAGE = 3;
125 const int RETURN_SPEEDRUN = 4;
126 const int RETURN_NEEDKILL = 5;
127
128 // flag properties
129 #define ctf_spawnorigin dropped_origin
130 bool ctf_stalemate; // indicates that a stalemate is active
131 float ctf_captimerecord; // record time for capturing the flag
132 .float ctf_pickuptime;
133 .float ctf_droptime;
134 .int ctf_status; // status of the flag (FLAG_BASE, FLAG_DROPPED, FLAG_CARRY declared globally)
135 .entity ctf_dropper; // don't allow spam of dropping the flag
136 .int max_flag_health;
137 .float next_take_time;
138 .bool ctf_flagdamaged;
139 int ctf_teams;
140
141 // passing/throwing properties
142 .float pass_distance;
143 .entity pass_sender;
144 .entity pass_target;
145 .float throw_antispam;
146 .float throw_prevtime;
147 .int throw_count;
148
149 // CaptureShield: If the player is too bad to be allowed to capture, shield them from taking the flag.
150 .bool ctf_captureshielded; // set to 1 if the player is too bad to be allowed to capture
151 float ctf_captureshield_min_negscore; // punish at -20 points
152 float ctf_captureshield_max_ratio; // punish at most 30% of each team
153 float ctf_captureshield_force; // push force of the shield
154
155 // 1 flag ctf
156 bool ctf_oneflag; // indicates whether or not a neutral flag has been found
157
158 // bot player logic
159 const int HAVOCBOT_CTF_ROLE_NONE = 0;
160 const int HAVOCBOT_CTF_ROLE_DEFENSE = 2;
161 const int HAVOCBOT_CTF_ROLE_MIDDLE = 4;
162 const int HAVOCBOT_CTF_ROLE_OFFENSE = 8;
163 const int HAVOCBOT_CTF_ROLE_CARRIER = 16;
164 const int HAVOCBOT_CTF_ROLE_RETRIEVER = 32;
165 const int HAVOCBOT_CTF_ROLE_ESCORT = 64;
166
167 .bool havocbot_cantfindflag;
168
169 vector havocbot_ctf_middlepoint;
170 float havocbot_ctf_middlepoint_radius;
171
172 void havocbot_role_ctf_setrole(entity bot, int role);
173
174 // team checking
175 #define CTF_SAMETEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? DIFF_TEAM(a,b) : SAME_TEAM(a,b))
176 #define CTF_DIFFTEAM(a,b) ((autocvar_g_ctf_reverse || (ctf_oneflag && autocvar_g_ctf_oneflag_reverse)) ? SAME_TEAM(a,b) : DIFF_TEAM(a,b))
177
178 // networked flag statuses
179 .int ctf_flagstatus;
180 #endif
181
182 const int CTF_RED_FLAG_TAKEN                    = 1;
183 const int CTF_RED_FLAG_LOST                             = 2;
184 const int CTF_RED_FLAG_CARRYING                 = 3;
185 const int CTF_BLUE_FLAG_TAKEN                   = 4;
186 const int CTF_BLUE_FLAG_LOST                    = 8;
187 const int CTF_BLUE_FLAG_CARRYING                = 12;
188 const int CTF_YELLOW_FLAG_TAKEN                 = 16;
189 const int CTF_YELLOW_FLAG_LOST                  = 32;
190 const int CTF_YELLOW_FLAG_CARRYING              = 48;
191 const int CTF_PINK_FLAG_TAKEN                   = 64;
192 const int CTF_PINK_FLAG_LOST                    = 128;
193 const int CTF_PINK_FLAG_CARRYING                = 192;
194 const int CTF_NEUTRAL_FLAG_TAKEN                = 256;
195 const int CTF_NEUTRAL_FLAG_LOST                 = 512;
196 const int CTF_NEUTRAL_FLAG_CARRYING             = 768;
197 const int CTF_FLAG_NEUTRAL                              = 2048;
198 const int CTF_SHIELDED                                  = 4096;
199 #endif
200
201 #ifdef IMPLEMENTATION
202
203 #ifdef SVQC
204 #include "../../../common/vehicles/all.qh"
205 #include "../../teamplay.qh"
206 #endif
207
208 #include "../../../lib/warpzone/common.qh"
209
210 bool autocvar_g_ctf_allow_vehicle_carry;
211 bool autocvar_g_ctf_allow_vehicle_touch;
212 bool autocvar_g_ctf_allow_monster_touch;
213 bool autocvar_g_ctf_throw;
214 float autocvar_g_ctf_throw_angle_max;
215 float autocvar_g_ctf_throw_angle_min;
216 int autocvar_g_ctf_throw_punish_count;
217 float autocvar_g_ctf_throw_punish_delay;
218 float autocvar_g_ctf_throw_punish_time;
219 float autocvar_g_ctf_throw_strengthmultiplier;
220 float autocvar_g_ctf_throw_velocity_forward;
221 float autocvar_g_ctf_throw_velocity_up;
222 float autocvar_g_ctf_drop_velocity_up;
223 float autocvar_g_ctf_drop_velocity_side;
224 bool autocvar_g_ctf_oneflag_reverse;
225 bool autocvar_g_ctf_portalteleport;
226 bool autocvar_g_ctf_pass;
227 float autocvar_g_ctf_pass_arc;
228 float autocvar_g_ctf_pass_arc_max;
229 float autocvar_g_ctf_pass_directional_max;
230 float autocvar_g_ctf_pass_directional_min;
231 float autocvar_g_ctf_pass_radius;
232 float autocvar_g_ctf_pass_wait;
233 bool autocvar_g_ctf_pass_request;
234 float autocvar_g_ctf_pass_turnrate;
235 float autocvar_g_ctf_pass_timelimit;
236 float autocvar_g_ctf_pass_velocity;
237 bool autocvar_g_ctf_dynamiclights;
238 float autocvar_g_ctf_flag_collect_delay;
239 float autocvar_g_ctf_flag_damageforcescale;
240 bool autocvar_g_ctf_flag_dropped_waypoint;
241 bool autocvar_g_ctf_flag_dropped_floatinwater;
242 bool autocvar_g_ctf_flag_glowtrails;
243 int autocvar_g_ctf_flag_health;
244 bool autocvar_g_ctf_flag_return;
245 float autocvar_g_ctf_flag_return_carried_radius;
246 float autocvar_g_ctf_flag_return_time;
247 bool autocvar_g_ctf_flag_return_when_unreachable;
248 float autocvar_g_ctf_flag_return_damage;
249 float autocvar_g_ctf_flag_return_damage_delay;
250 float autocvar_g_ctf_flag_return_dropped;
251 float autocvar_g_ctf_flagcarrier_auto_helpme_damage;
252 float autocvar_g_ctf_flagcarrier_auto_helpme_time;
253 float autocvar_g_ctf_flagcarrier_selfdamagefactor;
254 float autocvar_g_ctf_flagcarrier_selfforcefactor;
255 float autocvar_g_ctf_flagcarrier_damagefactor;
256 float autocvar_g_ctf_flagcarrier_forcefactor;
257 //float autocvar_g_ctf_flagcarrier_waypointforenemy_spotting;
258 bool autocvar_g_ctf_fullbrightflags;
259 bool autocvar_g_ctf_ignore_frags;
260 int autocvar_g_ctf_score_capture;
261 int autocvar_g_ctf_score_capture_assist;
262 int autocvar_g_ctf_score_kill;
263 int autocvar_g_ctf_score_penalty_drop;
264 int autocvar_g_ctf_score_penalty_returned;
265 int autocvar_g_ctf_score_pickup_base;
266 int autocvar_g_ctf_score_pickup_dropped_early;
267 int autocvar_g_ctf_score_pickup_dropped_late;
268 int autocvar_g_ctf_score_return;
269 float autocvar_g_ctf_shield_force;
270 float autocvar_g_ctf_shield_max_ratio;
271 int autocvar_g_ctf_shield_min_negscore;
272 bool autocvar_g_ctf_stalemate;
273 int autocvar_g_ctf_stalemate_endcondition;
274 float autocvar_g_ctf_stalemate_time;
275 bool autocvar_g_ctf_reverse;
276 float autocvar_g_ctf_dropped_capture_delay;
277 float autocvar_g_ctf_dropped_capture_radius;
278
279 void ctf_FakeTimeLimit(entity e, float t)
280 {
281         msg_entity = e;
282         WriteByte(MSG_ONE, 3); // svc_updatestat
283         WriteByte(MSG_ONE, 236); // STAT_TIMELIMIT
284         if(t < 0)
285                 WriteCoord(MSG_ONE, autocvar_timelimit);
286         else
287                 WriteCoord(MSG_ONE, (t + 1) / 60);
288 }
289
290 void ctf_EventLog(string mode, int flagteam, entity actor) // use an alias for easy changing and quick editing later
291 {
292         if(autocvar_sv_eventlog)
293                 GameLogEcho(sprintf(":ctf:%s:%d:%d:%s", mode, flagteam, actor.team, ((actor != world) ? ftos(actor.playerid) : "")));
294                 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
295 }
296
297 void ctf_CaptureRecord(entity flag, entity player)
298 {
299         float cap_record = ctf_captimerecord;
300         float cap_time = (time - flag.ctf_pickuptime);
301         string refername = db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"));
302
303         // notify about shit
304         if(ctf_oneflag) { Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
305         else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_TIME_), player.netname, (cap_time * 100)); }
306         else if(cap_time < cap_record) { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_BROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
307         else { Send_Notification(NOTIF_ALL, world, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_CAPTURE_UNBROKEN_), player.netname, refername, (cap_time * 100), (cap_record * 100)); }
308
309         // write that shit in the database
310         if(!ctf_oneflag) // but not in 1-flag mode
311         if((!ctf_captimerecord) || (cap_time < cap_record))
312         {
313                 ctf_captimerecord = cap_time;
314                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
315                 db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
316                 write_recordmarker(player, (time - cap_time), cap_time);
317         }
318 }
319
320 void ctf_FlagcarrierWaypoints(entity player)
321 {
322         WaypointSprite_Spawn(WP_FlagCarrier, 0, 0, player, FLAG_WAYPOINT_OFFSET, world, player.team, player, wps_flagcarrier, true, RADARICON_FLAG);
323         WaypointSprite_UpdateMaxHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
324         WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
325         WaypointSprite_UpdateTeamRadar(player.wps_flagcarrier, RADARICON_FLAGCARRIER, WPCOLOR_FLAGCARRIER(player.team));
326 }
327
328 void ctf_CalculatePassVelocity(entity flag, vector to, vector from, float turnrate)
329 {
330         float current_distance = vlen((('1 0 0' * to.x) + ('0 1 0' * to.y)) - (('1 0 0' * from.x) + ('0 1 0' * from.y))); // for the sake of this check, exclude Z axis
331         float initial_height = min(autocvar_g_ctf_pass_arc_max, (flag.pass_distance * tanh(autocvar_g_ctf_pass_arc)));
332         float current_height = (initial_height * min(1, (current_distance / flag.pass_distance)));
333         //print("current_height = ", ftos(current_height), ", initial_height = ", ftos(initial_height), ".\n");
334
335         vector targpos;
336         if(current_height) // make sure we can actually do this arcing path
337         {
338                 targpos = (to + ('0 0 1' * current_height));
339                 WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
340                 if(trace_fraction < 1)
341                 {
342                         //print("normal arc line failed, trying to find new pos...");
343                         WarpZone_TraceLine(to, targpos, MOVE_NOMONSTERS, flag);
344                         targpos = (trace_endpos + FLAG_PASS_ARC_OFFSET);
345                         WarpZone_TraceLine(flag.origin, targpos, MOVE_NOMONSTERS, flag);
346                         if(trace_fraction < 1) { targpos = to; /* print(" ^1FAILURE^7, reverting to original direction.\n"); */ }
347                         /*else { print(" ^3SUCCESS^7, using new arc line.\n"); } */
348                 }
349         }
350         else { targpos = to; }
351
352         //flag.angles = normalize(('0 1 0' * to_y) - ('0 1 0' * from_y));
353
354         vector desired_direction = normalize(targpos - from);
355         if(turnrate) { flag.velocity = (normalize(normalize(flag.velocity) + (desired_direction * autocvar_g_ctf_pass_turnrate)) * autocvar_g_ctf_pass_velocity); }
356         else { flag.velocity = (desired_direction * autocvar_g_ctf_pass_velocity); }
357 }
358
359 bool ctf_CheckPassDirection(vector head_center, vector passer_center, vector passer_angle, vector nearest_to_passer)
360 {
361         if(autocvar_g_ctf_pass_directional_max || autocvar_g_ctf_pass_directional_min)
362         {
363                 // directional tracing only
364                 float spreadlimit;
365                 makevectors(passer_angle);
366
367                 // find the closest point on the enemy to the center of the attack
368                 float h; // hypotenuse, which is the distance between attacker to head
369                 float a; // adjacent side, which is the distance between attacker and the point on w_shotdir that is closest to head.origin
370
371                 h = vlen(head_center - passer_center);
372                 a = h * (normalize(head_center - passer_center) * v_forward);
373
374                 vector nearest_on_line = (passer_center + a * v_forward);
375                 float distance_from_line = vlen(nearest_to_passer - nearest_on_line);
376
377                 spreadlimit = (autocvar_g_ctf_pass_radius ? min(1, (vlen(passer_center - nearest_on_line) / autocvar_g_ctf_pass_radius)) : 1);
378                 spreadlimit = (autocvar_g_ctf_pass_directional_min * (1 - spreadlimit) + autocvar_g_ctf_pass_directional_max * spreadlimit);
379
380                 if(spreadlimit && (distance_from_line <= spreadlimit) && ((vlen(normalize(head_center - passer_center) - v_forward) * RAD2DEG) <= 90))
381                         { return true; }
382                 else
383                         { return false; }
384         }
385         else { return true; }
386 }
387
388
389 // =======================
390 // CaptureShield Functions
391 // =======================
392
393 bool ctf_CaptureShield_CheckStatus(entity p)
394 {
395         int s, s2, s3, s4, se, se2, se3, se4, sr, ser;
396         entity e;
397         int players_worseeq, players_total;
398
399         if(ctf_captureshield_max_ratio <= 0)
400                 return false;
401
402         s = PlayerScore_Add(p, SP_CTF_CAPS, 0);
403         s2 = PlayerScore_Add(p, SP_CTF_PICKUPS, 0);
404         s3 = PlayerScore_Add(p, SP_CTF_RETURNS, 0);
405         s4 = PlayerScore_Add(p, SP_CTF_FCKILLS, 0);
406
407         sr = ((s - s2) + (s3 + s4));
408
409         if(sr >= -ctf_captureshield_min_negscore)
410                 return false;
411
412         players_total = players_worseeq = 0;
413         FOR_EACH_PLAYER(e)
414         {
415                 if(DIFF_TEAM(e, p))
416                         continue;
417                 se = PlayerScore_Add(e, SP_CTF_CAPS, 0);
418                 se2 = PlayerScore_Add(e, SP_CTF_PICKUPS, 0);
419                 se3 = PlayerScore_Add(e, SP_CTF_RETURNS, 0);
420                 se4 = PlayerScore_Add(e, SP_CTF_FCKILLS, 0);
421
422                 ser = ((se - se2) + (se3 + se4));
423
424                 if(ser <= sr)
425                         ++players_worseeq;
426                 ++players_total;
427         }
428
429         // player is in the worse half, if >= half the players are better than him, or consequently, if < half of the players are worse
430         // use this rule here
431
432         if(players_worseeq >= players_total * ctf_captureshield_max_ratio)
433                 return false;
434
435         return true;
436 }
437
438 void ctf_CaptureShield_Update(entity player, bool wanted_status)
439 {
440         bool updated_status = ctf_CaptureShield_CheckStatus(player);
441         if((wanted_status == player.ctf_captureshielded) && (updated_status != wanted_status)) // 0: shield only, 1: unshield only
442         {
443                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((updated_status) ? CENTER_CTF_CAPTURESHIELD_SHIELDED : CENTER_CTF_CAPTURESHIELD_FREE));
444                 player.ctf_captureshielded = updated_status;
445         }
446 }
447
448 bool ctf_CaptureShield_Customize()
449 {SELFPARAM();
450         if(!other.ctf_captureshielded) { return false; }
451         if(CTF_SAMETEAM(self, other)) { return false; }
452
453         return true;
454 }
455
456 void ctf_CaptureShield_Touch()
457 {SELFPARAM();
458         if(!other.ctf_captureshielded) { return; }
459         if(CTF_SAMETEAM(self, other)) { return; }
460
461         vector mymid = (self.absmin + self.absmax) * 0.5;
462         vector othermid = (other.absmin + other.absmax) * 0.5;
463
464         Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ctf_captureshield_force);
465         if(IS_REAL_CLIENT(other)) { Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
466 }
467
468 void ctf_CaptureShield_Spawn(entity flag)
469 {SELFPARAM();
470         entity shield = spawn();
471
472         shield.enemy = self;
473         shield.team = self.team;
474         shield.touch = ctf_CaptureShield_Touch;
475         shield.customizeentityforclient = ctf_CaptureShield_Customize;
476         shield.classname = "ctf_captureshield";
477         shield.effects = EF_ADDITIVE;
478         shield.movetype = MOVETYPE_NOCLIP;
479         shield.solid = SOLID_TRIGGER;
480         shield.avelocity = '7 0 11';
481         shield.scale = 0.5;
482
483         setorigin(shield, self.origin);
484         setmodel(shield, MDL_CTF_SHIELD);
485         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
486 }
487
488
489 // ====================
490 // Drop/Pass/Throw Code
491 // ====================
492
493 void ctf_Handle_Drop(entity flag, entity player, int droptype)
494 {
495         // declarations
496         player = (player ? player : flag.pass_sender);
497
498         // main
499         flag.movetype = MOVETYPE_TOSS;
500         flag.takedamage = DAMAGE_YES;
501         flag.angles = '0 0 0';
502         flag.health = flag.max_flag_health;
503         flag.ctf_droptime = time;
504         flag.ctf_dropper = player;
505         flag.ctf_status = FLAG_DROPPED;
506
507         // messages and sounds
508         Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_LOST_) : INFO_CTF_LOST_NEUTRAL), player.netname);
509         _sound(flag, CH_TRIGGER, flag.snd_flag_dropped, VOL_BASE, ATTEN_NONE);
510         ctf_EventLog("dropped", player.team, player);
511
512         // scoring
513         PlayerTeamScore_AddScore(player, -autocvar_g_ctf_score_penalty_drop);
514         PlayerScore_Add(player, SP_CTF_DROPS, 1);
515
516         // waypoints
517         if(autocvar_g_ctf_flag_dropped_waypoint) {
518                 entity wp = WaypointSprite_Spawn(WP_FlagDropped, 0, 0, flag, FLAG_WAYPOINT_OFFSET, world, ((autocvar_g_ctf_flag_dropped_waypoint == 2) ? 0 : player.team), flag, wps_flagdropped, true, RADARICON_FLAG);
519                 wp.colormod = WPCOLOR_DROPPEDFLAG(flag.team);
520         }
521
522         if(autocvar_g_ctf_flag_return_time || (autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health))
523         {
524                 WaypointSprite_UpdateMaxHealth(flag.wps_flagdropped, flag.max_flag_health);
525                 WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health);
526         }
527
528         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
529
530         if(droptype == DROP_PASS)
531         {
532                 flag.pass_distance = 0;
533                 flag.pass_sender = world;
534                 flag.pass_target = world;
535         }
536 }
537
538 void ctf_Handle_Retrieve(entity flag, entity player)
539 {
540         entity tmp_player; // temporary entity which the FOR_EACH_PLAYER loop uses to scan players
541         entity sender = flag.pass_sender;
542
543         // transfer flag to player
544         flag.owner = player;
545         flag.owner.flagcarried = flag;
546
547         // reset flag
548         if(player.vehicle)
549         {
550                 setattachment(flag, player.vehicle, "");
551                 setorigin(flag, VEHICLE_FLAG_OFFSET);
552                 flag.scale = VEHICLE_FLAG_SCALE;
553         }
554         else
555         {
556                 setattachment(flag, player, "");
557                 setorigin(flag, FLAG_CARRY_OFFSET);
558         }
559         flag.movetype = MOVETYPE_NONE;
560         flag.takedamage = DAMAGE_NO;
561         flag.solid = SOLID_NOT;
562         flag.angles = '0 0 0';
563         flag.ctf_status = FLAG_CARRY;
564
565         // messages and sounds
566         _sound(player, CH_TRIGGER, flag.snd_flag_pass, VOL_BASE, ATTEN_NORM);
567         ctf_EventLog("receive", flag.team, player);
568
569         FOR_EACH_REALPLAYER(tmp_player)
570         {
571                 if(tmp_player == sender)
572                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_SENT_) : CENTER_CTF_PASS_SENT_NEUTRAL), player.netname);
573                 else if(tmp_player == player)
574                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_RECEIVED_) : CENTER_CTF_PASS_RECEIVED_NEUTRAL), sender.netname);
575                 else if(SAME_TEAM(tmp_player, sender))
576                         Send_Notification(NOTIF_ONE, tmp_player, MSG_CENTER, ((flag.team) ? APP_TEAM_ENT_4(flag, CENTER_CTF_PASS_OTHER_) : CENTER_CTF_PASS_OTHER_NEUTRAL), sender.netname, player.netname);
577         }
578
579         // create new waypoint
580         ctf_FlagcarrierWaypoints(player);
581
582         sender.throw_antispam = time + autocvar_g_ctf_pass_wait;
583         player.throw_antispam = sender.throw_antispam;
584
585         flag.pass_distance = 0;
586         flag.pass_sender = world;
587         flag.pass_target = world;
588 }
589
590 void ctf_Handle_Throw(entity player, entity receiver, int droptype)
591 {
592         entity flag = player.flagcarried;
593         vector targ_origin, flag_velocity;
594
595         if(!flag) { return; }
596         if((droptype == DROP_PASS) && !receiver) { return; }
597
598         if(flag.speedrunning) { ctf_RespawnFlag(flag); return; }
599
600         // reset the flag
601         setattachment(flag, world, "");
602         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
603         flag.owner.flagcarried = world;
604         flag.owner = world;
605         flag.solid = SOLID_TRIGGER;
606         flag.ctf_dropper = player;
607         flag.ctf_droptime = time;
608
609         flag.flags = FL_ITEM | FL_NOTARGET; // clear FL_ONGROUND for MOVETYPE_TOSS
610
611         switch(droptype)
612         {
613                 case DROP_PASS:
614                 {
615                         // warpzone support:
616                         // for the examples, we assume player -> wz1 -> ... -> wzn -> receiver
617                         // findradius has already put wzn ... wz1 into receiver's warpzone parameters!
618                         WarpZone_RefSys_Copy(flag, receiver);
619                         WarpZone_RefSys_AddInverse(flag, receiver); // wz1^-1 ... wzn^-1 receiver
620                         targ_origin = WarpZone_RefSys_TransformOrigin(receiver, flag, (0.5 * (receiver.absmin + receiver.absmax))); // this is target origin as seen by the flag
621
622                         flag.pass_distance = vlen((('1 0 0' * targ_origin.x) + ('0 1 0' * targ_origin.y)) - (('1 0 0' *  player.origin.x) + ('0 1 0' *  player.origin.y))); // for the sake of this check, exclude Z axis
623                         ctf_CalculatePassVelocity(flag, targ_origin, player.origin, false);
624
625                         // main
626                         flag.movetype = MOVETYPE_FLY;
627                         flag.takedamage = DAMAGE_NO;
628                         flag.pass_sender = player;
629                         flag.pass_target = receiver;
630                         flag.ctf_status = FLAG_PASSING;
631
632                         // other
633                         _sound(player, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
634                         WarpZone_TrailParticles(world, _particleeffectnum(flag.passeffect), player.origin, targ_origin);
635                         ctf_EventLog("pass", flag.team, player);
636                         break;
637                 }
638
639                 case DROP_THROW:
640                 {
641                         makevectors((player.v_angle.y * '0 1 0') + (bound(autocvar_g_ctf_throw_angle_min, player.v_angle.x, autocvar_g_ctf_throw_angle_max) * '1 0 0'));
642
643                         flag_velocity = (('0 0 1' * autocvar_g_ctf_throw_velocity_up) + ((v_forward * autocvar_g_ctf_throw_velocity_forward) * ((player.items & ITEM_Strength.m_itemid) ? autocvar_g_ctf_throw_strengthmultiplier : 1)));
644                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, flag_velocity, false);
645                         ctf_Handle_Drop(flag, player, droptype);
646                         break;
647                 }
648
649                 case DROP_RESET:
650                 {
651                         flag.velocity = '0 0 0'; // do nothing
652                         break;
653                 }
654
655                 default:
656                 case DROP_NORMAL:
657                 {
658                         flag.velocity = W_CalculateProjectileVelocity(player.velocity, (('0 0 1' * autocvar_g_ctf_drop_velocity_up) + ((('0 1 0' * crandom()) + ('1 0 0' * crandom())) * autocvar_g_ctf_drop_velocity_side)), false);
659                         ctf_Handle_Drop(flag, player, droptype);
660                         break;
661                 }
662         }
663
664         // kill old waypointsprite
665         WaypointSprite_Ping(player.wps_flagcarrier);
666         WaypointSprite_Kill(player.wps_flagcarrier);
667
668         if(player.wps_enemyflagcarrier)
669                 WaypointSprite_Kill(player.wps_enemyflagcarrier);
670
671         // captureshield
672         ctf_CaptureShield_Update(player, 0); // shield player from picking up flag
673 }
674
675
676 // ==============
677 // Event Handlers
678 // ==============
679
680 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
681 {
682         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
683         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
684         entity player_team_flag = world, tmp_entity;
685         float old_time, new_time;
686
687         if(!player) { return; } // without someone to give the reward to, we can't possibly cap
688         if(CTF_DIFFTEAM(player, flag)) { return; }
689
690         if(ctf_oneflag)
691         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
692         if(SAME_TEAM(tmp_entity, player))
693         {
694                 player_team_flag = tmp_entity;
695                 break;
696         }
697
698         nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
699
700         player.throw_prevtime = time;
701         player.throw_count = 0;
702
703         // messages and sounds
704         Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
705         ctf_CaptureRecord(enemy_flag, player);
706         _sound(player, CH_TRIGGER, ((ctf_oneflag) ? player_team_flag.snd_flag_capture : ((DIFF_TEAM(player, flag)) ? enemy_flag.snd_flag_capture : flag.snd_flag_capture)), VOL_BASE, ATTEN_NONE);
707
708         switch(capturetype)
709         {
710                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
711                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
712                 default: break;
713         }
714
715         // scoring
716         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
717         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
718
719         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
720         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
721         if(!old_time || new_time < old_time)
722                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
723
724         // effects
725         Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
726         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
727
728         // other
729         if(capturetype == CAPTURE_NORMAL)
730         {
731                 WaypointSprite_Kill(player.wps_flagcarrier);
732                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
733
734                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
735                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
736         }
737
738         // reset the flag
739         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
740         ctf_RespawnFlag(enemy_flag);
741 }
742
743 void ctf_Handle_Return(entity flag, entity player)
744 {
745         // messages and sounds
746         if(IS_MONSTER(player))
747         {
748                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
749         }
750         else if(flag.team)
751         {
752                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
753                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
754         }
755         _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
756         ctf_EventLog("return", flag.team, player);
757
758         // scoring
759         if(IS_PLAYER(player))
760         {
761                 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
762                 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
763
764                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
765         }
766
767         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
768
769         if(flag.ctf_dropper)
770         {
771                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
772                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
773                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
774         }
775
776         // other
777         if(player.flagcarried == flag)
778                 WaypointSprite_Kill(player.wps_flagcarrier);
779
780         // reset the flag
781         ctf_RespawnFlag(flag);
782 }
783
784 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
785 {
786         // declarations
787         float pickup_dropped_score; // used to calculate dropped pickup score
788         entity tmp_entity; // temporary entity
789
790         // attach the flag to the player
791         flag.owner = player;
792         player.flagcarried = flag;
793         if(player.vehicle)
794         {
795                 setattachment(flag, player.vehicle, "");
796                 setorigin(flag, VEHICLE_FLAG_OFFSET);
797                 flag.scale = VEHICLE_FLAG_SCALE;
798         }
799         else
800         {
801                 setattachment(flag, player, "");
802                 setorigin(flag, FLAG_CARRY_OFFSET);
803         }
804
805         // flag setup
806         flag.movetype = MOVETYPE_NONE;
807         flag.takedamage = DAMAGE_NO;
808         flag.solid = SOLID_NOT;
809         flag.angles = '0 0 0';
810         flag.ctf_status = FLAG_CARRY;
811
812         switch(pickuptype)
813         {
814                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
815                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
816                 default: break;
817         }
818
819         // messages and sounds
820         Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
821         if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
822         if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
823         else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
824         else { Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((SAME_TEAM(player, flag)) ? CENTER_CTF_PICKUP_TEAM : CENTER_CTF_PICKUP_TEAM_ENEMY), Team_ColorCode(flag.team)); }
825
826         Send_Notification(NOTIF_TEAM_EXCEPT, player, MSG_CHOICE, ((flag.team) ? APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_) : CHOICE_CTF_PICKUP_TEAM_NEUTRAL), Team_ColorCode(player.team), player.netname);
827
828         if(!flag.team)
829         FOR_EACH_PLAYER(tmp_entity)
830         if(tmp_entity != player)
831         if(DIFF_TEAM(player, tmp_entity))
832                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
833
834         if(flag.team)
835         FOR_EACH_PLAYER(tmp_entity)
836         if(tmp_entity != player)
837         if(CTF_SAMETEAM(flag, tmp_entity))
838         if(SAME_TEAM(player, tmp_entity))
839                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
840         else
841                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, ((SAME_TEAM(flag, player)) ? CHOICE_CTF_PICKUP_ENEMY_TEAM : CHOICE_CTF_PICKUP_ENEMY), Team_ColorCode(player.team), player.netname);
842
843         _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
844
845         // scoring
846         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
847         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
848         switch(pickuptype)
849         {
850                 case PICKUP_BASE:
851                 {
852                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
853                         ctf_EventLog("steal", flag.team, player);
854                         break;
855                 }
856
857                 case PICKUP_DROPPED:
858                 {
859                         pickup_dropped_score = (autocvar_g_ctf_flag_return_time ? bound(0, ((flag.ctf_droptime + autocvar_g_ctf_flag_return_time) - time) / autocvar_g_ctf_flag_return_time, 1) : 1);
860                         pickup_dropped_score = floor((autocvar_g_ctf_score_pickup_dropped_late * (1 - pickup_dropped_score) + autocvar_g_ctf_score_pickup_dropped_early * pickup_dropped_score) + 0.5);
861                         LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
862                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
863                         ctf_EventLog("pickup", flag.team, player);
864                         break;
865                 }
866
867                 default: break;
868         }
869
870         // speedrunning
871         if(pickuptype == PICKUP_BASE)
872         {
873                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
874                 if((player.speedrunning) && (ctf_captimerecord))
875                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
876         }
877
878         // effects
879         Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
880
881         // waypoints
882         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
883         ctf_FlagcarrierWaypoints(player);
884         WaypointSprite_Ping(player.wps_flagcarrier);
885 }
886
887
888 // ===================
889 // Main Flag Functions
890 // ===================
891
892 void ctf_CheckFlagReturn(entity flag, int returntype)
893 {
894         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
895         {
896                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
897
898                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
899                 {
900                         switch(returntype)
901                         {
902                                 case RETURN_DROPPED: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DROPPED_) : INFO_CTF_FLAGRETURN_DROPPED_NEUTRAL)); break;
903                                 case RETURN_DAMAGE: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_DAMAGED_) : INFO_CTF_FLAGRETURN_DAMAGED_NEUTRAL)); break;
904                                 case RETURN_SPEEDRUN: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_SPEEDRUN_) : INFO_CTF_FLAGRETURN_SPEEDRUN_NEUTRAL), ctf_captimerecord); break;
905                                 case RETURN_NEEDKILL: Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_NEEDKILL_) : INFO_CTF_FLAGRETURN_NEEDKILL_NEUTRAL)); break;
906
907                                 default:
908                                 case RETURN_TIMEOUT:
909                                         { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
910                         }
911                         _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
912                         ctf_EventLog("returned", flag.team, world);
913                         ctf_RespawnFlag(flag);
914                 }
915         }
916 }
917
918 bool ctf_Stalemate_Customize()
919 {SELFPARAM();
920         // make spectators see what the player would see
921         entity e, wp_owner;
922         e = WaypointSprite_getviewentity(other);
923         wp_owner = self.owner;
924
925         // team waypoints
926         if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
927         if(SAME_TEAM(wp_owner, e)) { return false; }
928         if(!IS_PLAYER(e)) { return false; }
929
930         return true;
931 }
932
933 void ctf_CheckStalemate(void)
934 {
935         // declarations
936         int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
937         entity tmp_entity;
938
939         entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
940
941         // build list of stale flags
942         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
943         {
944                 if(autocvar_g_ctf_stalemate)
945                 if(tmp_entity.ctf_status != FLAG_BASE)
946                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
947                 {
948                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
949                         ctf_staleflaglist = tmp_entity;
950
951                         switch(tmp_entity.team)
952                         {
953                                 case NUM_TEAM_1: ++stale_red_flags; break;
954                                 case NUM_TEAM_2: ++stale_blue_flags; break;
955                                 case NUM_TEAM_3: ++stale_yellow_flags; break;
956                                 case NUM_TEAM_4: ++stale_pink_flags; break;
957                                 default: ++stale_neutral_flags; break;
958                         }
959                 }
960         }
961
962         if(ctf_oneflag)
963                 stale_flags = (stale_neutral_flags >= 1);
964         else
965                 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
966
967         if(ctf_oneflag && stale_flags == 1)
968                 ctf_stalemate = true;
969         else if(stale_flags >= 2)
970                 ctf_stalemate = true;
971         else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
972                 { ctf_stalemate = false; wpforenemy_announced = false; }
973         else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
974                 { ctf_stalemate = false; wpforenemy_announced = false; }
975
976         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
977         if(ctf_stalemate)
978         {
979                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
980                 {
981                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
982                         {
983                                 entity wp = WaypointSprite_Spawn(((ctf_oneflag) ? WP_FlagCarrier : WP_FlagCarrierEnemy), 0, 0, tmp_entity.owner, FLAG_WAYPOINT_OFFSET, world, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
984                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
985                                 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
986                         }
987                 }
988
989                 if (!wpforenemy_announced)
990                 {
991                         FOR_EACH_REALPLAYER(tmp_entity)
992                                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
993
994                         wpforenemy_announced = true;
995                 }
996         }
997 }
998
999 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
1000 {SELFPARAM();
1001         if(ITEM_DAMAGE_NEEDKILL(deathtype))
1002         {
1003                 if(autocvar_g_ctf_flag_return_damage_delay)
1004                 {
1005                         self.ctf_flagdamaged = true;
1006                 }
1007                 else
1008                 {
1009                         self.health = 0;
1010                         ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1011                 }
1012                 return;
1013         }
1014         if(autocvar_g_ctf_flag_return_damage)
1015         {
1016                 // reduce health and check if it should be returned
1017                 self.health = self.health - damage;
1018                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
1019                 return;
1020         }
1021 }
1022
1023 void ctf_FlagThink()
1024 {SELFPARAM();
1025         // declarations
1026         entity tmp_entity;
1027
1028         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
1029
1030         // captureshield
1031         if(self == ctf_worldflaglist) // only for the first flag
1032                 FOR_EACH_CLIENT(tmp_entity)
1033                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
1034
1035         // sanity checks
1036         if(self.mins != FLAG_MIN || self.maxs != FLAG_MAX) { // reset the flag boundaries in case it got squished
1037                 LOG_TRACE("wtf the flag got squashed?\n");
1038                 tracebox(self.origin, FLAG_MIN, FLAG_MAX, self.origin, MOVE_NOMONSTERS, self);
1039                 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
1040                         setsize(self, FLAG_MIN, FLAG_MAX); }
1041
1042         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
1043         {
1044                 case FLAG_DROPPED:
1045                 {
1046                         self.angles = '0 0 0';
1047                         break;
1048                 }
1049
1050                 default: break;
1051         }
1052
1053         // main think method
1054         switch(self.ctf_status)
1055         {
1056                 case FLAG_BASE:
1057                 {
1058                         if(autocvar_g_ctf_dropped_capture_radius)
1059                         {
1060                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
1061                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
1062                                         if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
1063                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
1064                                                 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
1065                         }
1066                         return;
1067                 }
1068
1069                 case FLAG_DROPPED:
1070                 {
1071                         if(autocvar_g_ctf_flag_dropped_floatinwater)
1072                         {
1073                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
1074                                 if(pointcontents(midpoint) == CONTENT_WATER)
1075                                 {
1076                                         self.velocity = self.velocity * 0.5;
1077
1078                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
1079                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
1080                                         else
1081                                                 { self.movetype = MOVETYPE_FLY; }
1082                                 }
1083                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
1084                         }
1085                         if(autocvar_g_ctf_flag_return_dropped)
1086                         {
1087                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
1088                                 {
1089                                         self.health = 0;
1090                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
1091                                         return;
1092                                 }
1093                         }
1094                         if(self.ctf_flagdamaged)
1095                         {
1096                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1097                                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1098                                 return;
1099                         }
1100                         else if(autocvar_g_ctf_flag_return_time)
1101                         {
1102                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1103                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
1104                                 return;
1105                         }
1106                         return;
1107                 }
1108
1109                 case FLAG_CARRY:
1110                 {
1111                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
1112                         {
1113                                 self.health = 0;
1114                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
1115
1116                                 setself(self.owner);
1117                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
1118                                 ImpulseCommands();
1119                                 setself(this);
1120                         }
1121                         if(autocvar_g_ctf_stalemate)
1122                         {
1123                                 if(time >= wpforenemy_nextthink)
1124                                 {
1125                                         ctf_CheckStalemate();
1126                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1127                                 }
1128                         }
1129                         if(CTF_SAMETEAM(self, self.owner) && self.team)
1130                         {
1131                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1132                                         ctf_Handle_Throw(self.owner, world, DROP_THROW);
1133                                 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
1134                                         ctf_Handle_Return(self, self.owner);
1135                         }
1136                         return;
1137                 }
1138
1139                 case FLAG_PASSING:
1140                 {
1141                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
1142                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
1143                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
1144
1145                         if((self.pass_target == world)
1146                                 || (self.pass_target.deadflag != DEAD_NO)
1147                                 || (self.pass_target.flagcarried)
1148                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
1149                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
1150                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1151                         {
1152                                 // give up, pass failed
1153                                 ctf_Handle_Drop(self, world, DROP_PASS);
1154                         }
1155                         else
1156                         {
1157                                 // still a viable target, go for it
1158                                 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
1159                         }
1160                         return;
1161                 }
1162
1163                 default: // this should never happen
1164                 {
1165                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1166                         return;
1167                 }
1168         }
1169 }
1170
1171 void ctf_FlagTouch()
1172 {SELFPARAM();
1173         if(gameover) { return; }
1174         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1175
1176         entity toucher = other, tmp_entity;
1177         bool is_not_monster = (!IS_MONSTER(toucher)), num_perteam = 0;
1178
1179         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1180         if(ITEM_TOUCH_NEEDKILL())
1181         {
1182                 if(!autocvar_g_ctf_flag_return_damage_delay)
1183                 {
1184                         self.health = 0;
1185                         ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1186                 }
1187                 if(!self.ctf_flagdamaged) { return; }
1188         }
1189
1190         FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
1191
1192         // special touch behaviors
1193         if(toucher.frozen) { return; }
1194         else if(IS_VEHICLE(toucher))
1195         {
1196                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1197                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1198                 else
1199                         return; // do nothing
1200         }
1201         else if(IS_MONSTER(toucher))
1202         {
1203                 if(!autocvar_g_ctf_allow_monster_touch)
1204                         return; // do nothing
1205         }
1206         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1207         {
1208                 if(time > self.wait) // if we haven't in a while, play a sound/effect
1209                 {
1210                         Send_Effect_(self.toucheffect, self.origin, '0 0 0', 1);
1211                         _sound(self, CH_TRIGGER, self.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1212                         self.wait = time + FLAG_TOUCHRATE;
1213                 }
1214                 return;
1215         }
1216         else if(toucher.deadflag != DEAD_NO) { return; }
1217
1218         switch(self.ctf_status)
1219         {
1220                 case FLAG_BASE:
1221                 {
1222                         if(ctf_oneflag)
1223                         {
1224                                 if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1225                                         ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1226                                 else if(!self.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1227                                         ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1228                         }
1229                         else if(CTF_SAMETEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && is_not_monster)
1230                                 ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1231                         else if(CTF_DIFFTEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1232                                 ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1233                         break;
1234                 }
1235
1236                 case FLAG_DROPPED:
1237                 {
1238                         if(CTF_SAMETEAM(toucher, self) && (autocvar_g_ctf_flag_return || num_perteam <= 1) && self.team) // automatically return if there's only 1 player on the team
1239                                 ctf_Handle_Return(self, toucher); // toucher just returned his own flag
1240                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != self.ctf_dropper) || (time > self.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1241                                 ctf_Handle_Pickup(self, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1242                         break;
1243                 }
1244
1245                 case FLAG_CARRY:
1246                 {
1247                         LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1248                         break;
1249                 }
1250
1251                 case FLAG_PASSING:
1252                 {
1253                         if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != self.pass_sender))
1254                         {
1255                                 if(DIFF_TEAM(toucher, self.pass_sender))
1256                                         ctf_Handle_Return(self, toucher);
1257                                 else
1258                                         ctf_Handle_Retrieve(self, toucher);
1259                         }
1260                         break;
1261                 }
1262         }
1263 }
1264
1265 .float last_respawn;
1266 void ctf_RespawnFlag(entity flag)
1267 {
1268         // check for flag respawn being called twice in a row
1269         if(flag.last_respawn > time - 0.5)
1270                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1271
1272         flag.last_respawn = time;
1273
1274         // reset the player (if there is one)
1275         if((flag.owner) && (flag.owner.flagcarried == flag))
1276         {
1277                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1278                 WaypointSprite_Kill(flag.wps_flagcarrier);
1279
1280                 flag.owner.flagcarried = world;
1281
1282                 if(flag.speedrunning)
1283                         ctf_FakeTimeLimit(flag.owner, -1);
1284         }
1285
1286         if((flag.owner) && (flag.owner.vehicle))
1287                 flag.scale = FLAG_SCALE;
1288
1289         if(flag.ctf_status == FLAG_DROPPED)
1290                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1291
1292         // reset the flag
1293         setattachment(flag, world, "");
1294         setorigin(flag, flag.ctf_spawnorigin);
1295
1296         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1297         flag.takedamage = DAMAGE_NO;
1298         flag.health = flag.max_flag_health;
1299         flag.solid = SOLID_TRIGGER;
1300         flag.velocity = '0 0 0';
1301         flag.angles = flag.mangle;
1302         flag.flags = FL_ITEM | FL_NOTARGET;
1303
1304         flag.ctf_status = FLAG_BASE;
1305         flag.owner = world;
1306         flag.pass_distance = 0;
1307         flag.pass_sender = world;
1308         flag.pass_target = world;
1309         flag.ctf_dropper = world;
1310         flag.ctf_pickuptime = 0;
1311         flag.ctf_droptime = 0;
1312         flag.ctf_flagdamaged = 0;
1313
1314         ctf_CheckStalemate();
1315 }
1316
1317 void ctf_Reset()
1318 {SELFPARAM();
1319         if(self.owner)
1320                 if(IS_PLAYER(self.owner))
1321                         ctf_Handle_Throw(self.owner, world, DROP_RESET);
1322
1323         ctf_RespawnFlag(self);
1324 }
1325
1326 void ctf_DelayedFlagSetup(void) // called after a flag is placed on a map by ctf_FlagSetup()
1327 {SELFPARAM();
1328         // bot waypoints
1329         waypoint_spawnforitem_force(self, self.origin);
1330         self.nearestwaypointtimeout = 0; // activate waypointing again
1331         self.bot_basewaypoint = self.nearestwaypoint;
1332
1333         // waypointsprites
1334         entity basename;
1335         switch (self.team)
1336         {
1337                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1338                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1339                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1340                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1341                 default: basename = WP_FlagBaseNeutral; break;
1342         }
1343
1344         entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1345         wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1346         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1347
1348         // captureshield setup
1349         ctf_CaptureShield_Spawn(self);
1350 }
1351
1352 void set_flag_string(entity flag, .string field, string value, string teamname)
1353 {
1354         if(flag.(field) == "")
1355                 flag.(field) = strzone(sprintf(value,teamname));
1356 }
1357
1358 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1359 {SELFPARAM();
1360         // declarations
1361         setself(flag); // for later usage with droptofloor()
1362
1363         // main setup
1364         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1365         ctf_worldflaglist = flag;
1366
1367         setattachment(flag, world, "");
1368
1369         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1370         flag.team = teamnumber;
1371         flag.classname = "item_flag_team";
1372         flag.target = "###item###"; // wut?
1373         flag.flags = FL_ITEM | FL_NOTARGET;
1374         flag.solid = SOLID_TRIGGER;
1375         flag.takedamage = DAMAGE_NO;
1376         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1377         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1378         flag.health = flag.max_flag_health;
1379         flag.event_damage = ctf_FlagDamage;
1380         flag.pushable = true;
1381         flag.teleportable = TELEPORT_NORMAL;
1382         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1383         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1384         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1385         flag.velocity = '0 0 0';
1386         flag.mangle = flag.angles;
1387         flag.reset = ctf_Reset;
1388         flag.touch = ctf_FlagTouch;
1389         flag.think = ctf_FlagThink;
1390         flag.nextthink = time + FLAG_THINKRATE;
1391         flag.ctf_status = FLAG_BASE;
1392
1393         string teamname = Static_Team_ColorName_Lower(teamnumber);
1394         // appearence
1395         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1396         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1397         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1398         set_flag_string(flag, toucheffect,      "%sflag_touch", teamname);
1399         set_flag_string(flag, passeffect,       "%s_pass",              teamname);
1400         set_flag_string(flag, capeffect,        "%s_cap",               teamname);
1401
1402         // sounds
1403         flag.snd_flag_taken = SND(CTF_TAKEN(teamnumber));
1404         flag.snd_flag_returned = SND(CTF_RETURNED(teamnumber));
1405         flag.snd_flag_capture = SND(CTF_CAPTURE(teamnumber));
1406         flag.snd_flag_dropped = SND(CTF_DROPPED(teamnumber));
1407         if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = SND(CTF_RESPAWN); // if there is ever a team-based sound for this, update the code to match.
1408         precache_sound(flag.snd_flag_respawn);
1409         if (flag.snd_flag_touch == "") flag.snd_flag_touch = SND(CTF_TOUCH); // again has no team-based sound
1410         precache_sound(flag.snd_flag_touch);
1411         if (flag.snd_flag_pass == "") flag.snd_flag_pass = SND(CTF_PASS); // same story here
1412         precache_sound(flag.snd_flag_pass);
1413
1414         // precache
1415         precache_model(flag.model);
1416
1417         // appearence
1418         _setmodel(flag, flag.model); // precision set below
1419         setsize(flag, FLAG_MIN, FLAG_MAX);
1420         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1421
1422         if(autocvar_g_ctf_flag_glowtrails)
1423         {
1424                 switch(teamnumber)
1425                 {
1426                         case NUM_TEAM_1: flag.glow_color = 251; break;
1427                         case NUM_TEAM_2: flag.glow_color = 210; break;
1428                         case NUM_TEAM_3: flag.glow_color = 110; break;
1429                         case NUM_TEAM_4: flag.glow_color = 145; break;
1430                         default:                 flag.glow_color = 254; break;
1431                 }
1432                 flag.glow_size = 25;
1433                 flag.glow_trail = 1;
1434         }
1435
1436         flag.effects |= EF_LOWPRECISION;
1437         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1438         if(autocvar_g_ctf_dynamiclights)
1439         {
1440                 switch(teamnumber)
1441                 {
1442                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1443                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1444                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1445                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1446                         default:                 flag.effects |= EF_DIMLIGHT; break;
1447                 }
1448         }
1449
1450         // flag placement
1451         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1452         {
1453                 flag.dropped_origin = flag.origin;
1454                 flag.noalign = true;
1455                 flag.movetype = MOVETYPE_NONE;
1456         }
1457         else // drop to floor, automatically find a platform and set that as spawn origin
1458         {
1459                 flag.noalign = false;
1460                 setself(flag);
1461                 droptofloor();
1462                 flag.movetype = MOVETYPE_TOSS;
1463         }
1464
1465         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1466 }
1467
1468
1469 // ================
1470 // Bot player logic
1471 // ================
1472
1473 // NOTE: LEGACY CODE, needs to be re-written!
1474
1475 void havocbot_calculate_middlepoint()
1476 {
1477         entity f;
1478         vector s = '0 0 0';
1479         vector fo = '0 0 0';
1480         float n = 0;
1481
1482         f = ctf_worldflaglist;
1483         while (f)
1484         {
1485                 fo = f.origin;
1486                 s = s + fo;
1487                 f = f.ctf_worldflagnext;
1488         }
1489         if(!n)
1490                 return;
1491         havocbot_ctf_middlepoint = s * (1.0 / n);
1492         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1493 }
1494
1495
1496 entity havocbot_ctf_find_flag(entity bot)
1497 {
1498         entity f;
1499         f = ctf_worldflaglist;
1500         while (f)
1501         {
1502                 if (CTF_SAMETEAM(bot, f))
1503                         return f;
1504                 f = f.ctf_worldflagnext;
1505         }
1506         return world;
1507 }
1508
1509 entity havocbot_ctf_find_enemy_flag(entity bot)
1510 {
1511         entity f;
1512         f = ctf_worldflaglist;
1513         while (f)
1514         {
1515                 if(ctf_oneflag)
1516                 {
1517                         if(CTF_DIFFTEAM(bot, f))
1518                         {
1519                                 if(f.team)
1520                                 {
1521                                         if(bot.flagcarried)
1522                                                 return f;
1523                                 }
1524                                 else if(!bot.flagcarried)
1525                                         return f;
1526                         }
1527                 }
1528                 else if (CTF_DIFFTEAM(bot, f))
1529                         return f;
1530                 f = f.ctf_worldflagnext;
1531         }
1532         return world;
1533 }
1534
1535 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1536 {
1537         if (!teamplay)
1538                 return 0;
1539
1540         int c = 0;
1541         entity head;
1542
1543         FOR_EACH_PLAYER(head)
1544         {
1545                 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1546                         continue;
1547
1548                 if(vlen(head.origin - org) < tc_radius)
1549                         ++c;
1550         }
1551
1552         return c;
1553 }
1554
1555 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1556 {SELFPARAM();
1557         entity head;
1558         head = ctf_worldflaglist;
1559         while (head)
1560         {
1561                 if (CTF_SAMETEAM(self, head))
1562                         break;
1563                 head = head.ctf_worldflagnext;
1564         }
1565         if (head)
1566                 navigation_routerating(head, ratingscale, 10000);
1567 }
1568
1569 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1570 {SELFPARAM();
1571         entity head;
1572         head = ctf_worldflaglist;
1573         while (head)
1574         {
1575                 if (CTF_SAMETEAM(self, head))
1576                         break;
1577                 head = head.ctf_worldflagnext;
1578         }
1579         if (!head)
1580                 return;
1581
1582         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1583 }
1584
1585 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1586 {SELFPARAM();
1587         entity head;
1588         head = ctf_worldflaglist;
1589         while (head)
1590         {
1591                 if(ctf_oneflag)
1592                 {
1593                         if(CTF_DIFFTEAM(self, head))
1594                         {
1595                                 if(head.team)
1596                                 {
1597                                         if(self.flagcarried)
1598                                                 break;
1599                                 }
1600                                 else if(!self.flagcarried)
1601                                         break;
1602                         }
1603                 }
1604                 else if(CTF_DIFFTEAM(self, head))
1605                         break;
1606                 head = head.ctf_worldflagnext;
1607         }
1608         if (head)
1609                 navigation_routerating(head, ratingscale, 10000);
1610 }
1611
1612 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1613 {SELFPARAM();
1614         if (!bot_waypoints_for_items)
1615         {
1616                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1617                 return;
1618         }
1619
1620         entity head;
1621
1622         head = havocbot_ctf_find_enemy_flag(self);
1623
1624         if (!head)
1625                 return;
1626
1627         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1628 }
1629
1630 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1631 {SELFPARAM();
1632         entity mf;
1633
1634         mf = havocbot_ctf_find_flag(self);
1635
1636         if(mf.ctf_status == FLAG_BASE)
1637                 return;
1638
1639         if(mf.tag_entity)
1640                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1641 }
1642
1643 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1644 {
1645         entity head;
1646         head = ctf_worldflaglist;
1647         while (head)
1648         {
1649                 // flag is out in the field
1650                 if(head.ctf_status != FLAG_BASE)
1651                 if(head.tag_entity==world)      // dropped
1652                 {
1653                         if(df_radius)
1654                         {
1655                                 if(vlen(org-head.origin)<df_radius)
1656                                         navigation_routerating(head, ratingscale, 10000);
1657                         }
1658                         else
1659                                 navigation_routerating(head, ratingscale, 10000);
1660                 }
1661
1662                 head = head.ctf_worldflagnext;
1663         }
1664 }
1665
1666 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1667 {SELFPARAM();
1668         entity head;
1669         float t;
1670         head = findchainfloat(bot_pickup, true);
1671         while (head)
1672         {
1673                 // gather health and armor only
1674                 if (head.solid)
1675                 if (head.health || head.armorvalue)
1676                 if (vlen(head.origin - org) < sradius)
1677                 {
1678                         // get the value of the item
1679                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1680                         if (t > 0)
1681                                 navigation_routerating(head, t * ratingscale, 500);
1682                 }
1683                 head = head.chain;
1684         }
1685 }
1686
1687 void havocbot_ctf_reset_role(entity bot)
1688 {
1689         float cdefense, cmiddle, coffense;
1690         entity mf, ef, head;
1691         float c;
1692
1693         if(bot.deadflag != DEAD_NO)
1694                 return;
1695
1696         if(vlen(havocbot_ctf_middlepoint)==0)
1697                 havocbot_calculate_middlepoint();
1698
1699         // Check ctf flags
1700         if (bot.flagcarried)
1701         {
1702                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1703                 return;
1704         }
1705
1706         mf = havocbot_ctf_find_flag(bot);
1707         ef = havocbot_ctf_find_enemy_flag(bot);
1708
1709         // Retrieve stolen flag
1710         if(mf.ctf_status!=FLAG_BASE)
1711         {
1712                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1713                 return;
1714         }
1715
1716         // If enemy flag is taken go to the middle to intercept pursuers
1717         if(ef.ctf_status!=FLAG_BASE)
1718         {
1719                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1720                 return;
1721         }
1722
1723         // if there is only me on the team switch to offense
1724         c = 0;
1725         FOR_EACH_PLAYER(head)
1726         if(SAME_TEAM(head, bot))
1727                 ++c;
1728
1729         if(c==1)
1730         {
1731                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1732                 return;
1733         }
1734
1735         // Evaluate best position to take
1736         // Count mates on middle position
1737         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1738
1739         // Count mates on defense position
1740         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1741
1742         // Count mates on offense position
1743         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1744
1745         if(cdefense<=coffense)
1746                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1747         else if(coffense<=cmiddle)
1748                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1749         else
1750                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1751 }
1752
1753 void havocbot_role_ctf_carrier()
1754 {SELFPARAM();
1755         if(self.deadflag != DEAD_NO)
1756         {
1757                 havocbot_ctf_reset_role(self);
1758                 return;
1759         }
1760
1761         if (self.flagcarried == world)
1762         {
1763                 havocbot_ctf_reset_role(self);
1764                 return;
1765         }
1766
1767         if (self.bot_strategytime < time)
1768         {
1769                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1770
1771                 navigation_goalrating_start();
1772                 if(ctf_oneflag)
1773                         havocbot_goalrating_ctf_enemybase(50000);
1774                 else
1775                         havocbot_goalrating_ctf_ourbase(50000);
1776
1777                 if(self.health<100)
1778                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1779
1780                 navigation_goalrating_end();
1781
1782                 if (self.navigation_hasgoals)
1783                         self.havocbot_cantfindflag = time + 10;
1784                 else if (time > self.havocbot_cantfindflag)
1785                 {
1786                         // Can't navigate to my own base, suicide!
1787                         // TODO: drop it and wander around
1788                         Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1789                         return;
1790                 }
1791         }
1792 }
1793
1794 void havocbot_role_ctf_escort()
1795 {SELFPARAM();
1796         entity mf, ef;
1797
1798         if(self.deadflag != DEAD_NO)
1799         {
1800                 havocbot_ctf_reset_role(self);
1801                 return;
1802         }
1803
1804         if (self.flagcarried)
1805         {
1806                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1807                 return;
1808         }
1809
1810         // If enemy flag is back on the base switch to previous role
1811         ef = havocbot_ctf_find_enemy_flag(self);
1812         if(ef.ctf_status==FLAG_BASE)
1813         {
1814                 self.havocbot_role = self.havocbot_previous_role;
1815                 self.havocbot_role_timeout = 0;
1816                 return;
1817         }
1818
1819         // If the flag carrier reached the base switch to defense
1820         mf = havocbot_ctf_find_flag(self);
1821         if(mf.ctf_status!=FLAG_BASE)
1822         if(vlen(ef.origin - mf.dropped_origin) < 300)
1823         {
1824                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1825                 return;
1826         }
1827
1828         // Set the role timeout if necessary
1829         if (!self.havocbot_role_timeout)
1830         {
1831                 self.havocbot_role_timeout = time + random() * 30 + 60;
1832         }
1833
1834         // If nothing happened just switch to previous role
1835         if (time > self.havocbot_role_timeout)
1836         {
1837                 self.havocbot_role = self.havocbot_previous_role;
1838                 self.havocbot_role_timeout = 0;
1839                 return;
1840         }
1841
1842         // Chase the flag carrier
1843         if (self.bot_strategytime < time)
1844         {
1845                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1846                 navigation_goalrating_start();
1847                 havocbot_goalrating_ctf_enemyflag(30000);
1848                 havocbot_goalrating_ctf_ourstolenflag(40000);
1849                 havocbot_goalrating_items(10000, self.origin, 10000);
1850                 navigation_goalrating_end();
1851         }
1852 }
1853
1854 void havocbot_role_ctf_offense()
1855 {SELFPARAM();
1856         entity mf, ef;
1857         vector pos;
1858
1859         if(self.deadflag != DEAD_NO)
1860         {
1861                 havocbot_ctf_reset_role(self);
1862                 return;
1863         }
1864
1865         if (self.flagcarried)
1866         {
1867                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1868                 return;
1869         }
1870
1871         // Check flags
1872         mf = havocbot_ctf_find_flag(self);
1873         ef = havocbot_ctf_find_enemy_flag(self);
1874
1875         // Own flag stolen
1876         if(mf.ctf_status!=FLAG_BASE)
1877         {
1878                 if(mf.tag_entity)
1879                         pos = mf.tag_entity.origin;
1880                 else
1881                         pos = mf.origin;
1882
1883                 // Try to get it if closer than the enemy base
1884                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1885                 {
1886                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1887                         return;
1888                 }
1889         }
1890
1891         // Escort flag carrier
1892         if(ef.ctf_status!=FLAG_BASE)
1893         {
1894                 if(ef.tag_entity)
1895                         pos = ef.tag_entity.origin;
1896                 else
1897                         pos = ef.origin;
1898
1899                 if(vlen(pos-mf.dropped_origin)>700)
1900                 {
1901                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1902                         return;
1903                 }
1904         }
1905
1906         // About to fail, switch to middlefield
1907         if(self.health<50)
1908         {
1909                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1910                 return;
1911         }
1912
1913         // Set the role timeout if necessary
1914         if (!self.havocbot_role_timeout)
1915                 self.havocbot_role_timeout = time + 120;
1916
1917         if (time > self.havocbot_role_timeout)
1918         {
1919                 havocbot_ctf_reset_role(self);
1920                 return;
1921         }
1922
1923         if (self.bot_strategytime < time)
1924         {
1925                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1926                 navigation_goalrating_start();
1927                 havocbot_goalrating_ctf_ourstolenflag(50000);
1928                 havocbot_goalrating_ctf_enemybase(20000);
1929                 havocbot_goalrating_items(5000, self.origin, 1000);
1930                 havocbot_goalrating_items(1000, self.origin, 10000);
1931                 navigation_goalrating_end();
1932         }
1933 }
1934
1935 // Retriever (temporary role):
1936 void havocbot_role_ctf_retriever()
1937 {SELFPARAM();
1938         entity mf;
1939
1940         if(self.deadflag != DEAD_NO)
1941         {
1942                 havocbot_ctf_reset_role(self);
1943                 return;
1944         }
1945
1946         if (self.flagcarried)
1947         {
1948                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1949                 return;
1950         }
1951
1952         // If flag is back on the base switch to previous role
1953         mf = havocbot_ctf_find_flag(self);
1954         if(mf.ctf_status==FLAG_BASE)
1955         {
1956                 havocbot_ctf_reset_role(self);
1957                 return;
1958         }
1959
1960         if (!self.havocbot_role_timeout)
1961                 self.havocbot_role_timeout = time + 20;
1962
1963         if (time > self.havocbot_role_timeout)
1964         {
1965                 havocbot_ctf_reset_role(self);
1966                 return;
1967         }
1968
1969         if (self.bot_strategytime < time)
1970         {
1971                 float rt_radius;
1972                 rt_radius = 10000;
1973
1974                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1975                 navigation_goalrating_start();
1976                 havocbot_goalrating_ctf_ourstolenflag(50000);
1977                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1978                 havocbot_goalrating_ctf_enemybase(30000);
1979                 havocbot_goalrating_items(500, self.origin, rt_radius);
1980                 navigation_goalrating_end();
1981         }
1982 }
1983
1984 void havocbot_role_ctf_middle()
1985 {SELFPARAM();
1986         entity mf;
1987
1988         if(self.deadflag != DEAD_NO)
1989         {
1990                 havocbot_ctf_reset_role(self);
1991                 return;
1992         }
1993
1994         if (self.flagcarried)
1995         {
1996                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1997                 return;
1998         }
1999
2000         mf = havocbot_ctf_find_flag(self);
2001         if(mf.ctf_status!=FLAG_BASE)
2002         {
2003                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2004                 return;
2005         }
2006
2007         if (!self.havocbot_role_timeout)
2008                 self.havocbot_role_timeout = time + 10;
2009
2010         if (time > self.havocbot_role_timeout)
2011         {
2012                 havocbot_ctf_reset_role(self);
2013                 return;
2014         }
2015
2016         if (self.bot_strategytime < time)
2017         {
2018                 vector org;
2019
2020                 org = havocbot_ctf_middlepoint;
2021                 org.z = self.origin.z;
2022
2023                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2024                 navigation_goalrating_start();
2025                 havocbot_goalrating_ctf_ourstolenflag(50000);
2026                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
2027                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
2028                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
2029                 havocbot_goalrating_items(2500, self.origin, 10000);
2030                 havocbot_goalrating_ctf_enemybase(2500);
2031                 navigation_goalrating_end();
2032         }
2033 }
2034
2035 void havocbot_role_ctf_defense()
2036 {SELFPARAM();
2037         entity mf;
2038
2039         if(self.deadflag != DEAD_NO)
2040         {
2041                 havocbot_ctf_reset_role(self);
2042                 return;
2043         }
2044
2045         if (self.flagcarried)
2046         {
2047                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2048                 return;
2049         }
2050
2051         // If own flag was captured
2052         mf = havocbot_ctf_find_flag(self);
2053         if(mf.ctf_status!=FLAG_BASE)
2054         {
2055                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2056                 return;
2057         }
2058
2059         if (!self.havocbot_role_timeout)
2060                 self.havocbot_role_timeout = time + 30;
2061
2062         if (time > self.havocbot_role_timeout)
2063         {
2064                 havocbot_ctf_reset_role(self);
2065                 return;
2066         }
2067         if (self.bot_strategytime < time)
2068         {
2069                 float mp_radius;
2070                 vector org;
2071
2072                 org = mf.dropped_origin;
2073                 mp_radius = havocbot_ctf_middlepoint_radius;
2074
2075                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2076                 navigation_goalrating_start();
2077
2078                 // if enemies are closer to our base, go there
2079                 entity head, closestplayer = world;
2080                 float distance, bestdistance = 10000;
2081                 FOR_EACH_PLAYER(head)
2082                 {
2083                         if(head.deadflag!=DEAD_NO)
2084                                 continue;
2085
2086                         distance = vlen(org - head.origin);
2087                         if(distance<bestdistance)
2088                         {
2089                                 closestplayer = head;
2090                                 bestdistance = distance;
2091                         }
2092                 }
2093
2094                 if(closestplayer)
2095                 if(DIFF_TEAM(closestplayer, self))
2096                 if(vlen(org - self.origin)>1000)
2097                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
2098                         havocbot_goalrating_ctf_ourbase(30000);
2099
2100                 havocbot_goalrating_ctf_ourstolenflag(20000);
2101                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
2102                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
2103                 havocbot_goalrating_items(10000, org, mp_radius);
2104                 havocbot_goalrating_items(5000, self.origin, 10000);
2105                 navigation_goalrating_end();
2106         }
2107 }
2108
2109 void havocbot_role_ctf_setrole(entity bot, int role)
2110 {
2111         LOG_TRACE(strcat(bot.netname," switched to "));
2112         switch(role)
2113         {
2114                 case HAVOCBOT_CTF_ROLE_CARRIER:
2115                         LOG_TRACE("carrier");
2116                         bot.havocbot_role = havocbot_role_ctf_carrier;
2117                         bot.havocbot_role_timeout = 0;
2118                         bot.havocbot_cantfindflag = time + 10;
2119                         bot.bot_strategytime = 0;
2120                         break;
2121                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2122                         LOG_TRACE("defense");
2123                         bot.havocbot_role = havocbot_role_ctf_defense;
2124                         bot.havocbot_role_timeout = 0;
2125                         break;
2126                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2127                         LOG_TRACE("middle");
2128                         bot.havocbot_role = havocbot_role_ctf_middle;
2129                         bot.havocbot_role_timeout = 0;
2130                         break;
2131                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2132                         LOG_TRACE("offense");
2133                         bot.havocbot_role = havocbot_role_ctf_offense;
2134                         bot.havocbot_role_timeout = 0;
2135                         break;
2136                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2137                         LOG_TRACE("retriever");
2138                         bot.havocbot_previous_role = bot.havocbot_role;
2139                         bot.havocbot_role = havocbot_role_ctf_retriever;
2140                         bot.havocbot_role_timeout = time + 10;
2141                         bot.bot_strategytime = 0;
2142                         break;
2143                 case HAVOCBOT_CTF_ROLE_ESCORT:
2144                         LOG_TRACE("escort");
2145                         bot.havocbot_previous_role = bot.havocbot_role;
2146                         bot.havocbot_role = havocbot_role_ctf_escort;
2147                         bot.havocbot_role_timeout = time + 30;
2148                         bot.bot_strategytime = 0;
2149                         break;
2150         }
2151         LOG_TRACE("\n");
2152 }
2153
2154
2155 // ==============
2156 // Hook Functions
2157 // ==============
2158
2159 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2160 {SELFPARAM();
2161         entity flag;
2162         int t = 0, t2 = 0, t3 = 0;
2163
2164         // initially clear items so they can be set as necessary later.
2165         self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING          | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2166                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2167                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2168                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2169                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2170                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
2171
2172         // scan through all the flags and notify the client about them
2173         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2174         {
2175                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2176                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2177                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2178                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2179                 if(flag.team == 0)                      { t = CTF_NEUTRAL_FLAG_CARRYING;        t2 = CTF_NEUTRAL_FLAG_TAKEN;    t3 = CTF_NEUTRAL_FLAG_LOST; self.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
2180
2181                 switch(flag.ctf_status)
2182                 {
2183                         case FLAG_PASSING:
2184                         case FLAG_CARRY:
2185                         {
2186                                 if((flag.owner == self) || (flag.pass_sender == self))
2187                                         self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2188                                 else
2189                                         self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2190                                 break;
2191                         }
2192                         case FLAG_DROPPED:
2193                         {
2194                                 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2195                                 break;
2196                         }
2197                 }
2198         }
2199
2200         // item for stopping players from capturing the flag too often
2201         if(self.ctf_captureshielded)
2202                 self.ctf_flagstatus |= CTF_SHIELDED;
2203
2204         // update the health of the flag carrier waypointsprite
2205         if(self.wps_flagcarrier)
2206                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2207
2208         return false;
2209 }
2210
2211 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2212 {
2213         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2214         {
2215                 if(frag_target == frag_attacker) // damage done to yourself
2216                 {
2217                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2218                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2219                 }
2220                 else // damage done to everyone else
2221                 {
2222                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2223                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2224                 }
2225         }
2226         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2227         {
2228                 if(autocvar_g_ctf_flagcarrier_auto_helpme_damage > ('1 0 0' * healtharmor_maxdamage(frag_target.health, frag_target.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id)))
2229                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2230                 {
2231                         frag_target.wps_helpme_time = time;
2232                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2233                 }
2234                 // todo: add notification for when flag carrier needs help?
2235         }
2236         return false;
2237 }
2238
2239 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2240 {
2241         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2242         {
2243                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
2244                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2245         }
2246
2247         if(frag_target.flagcarried)
2248         {
2249                 entity tmp_entity = frag_target.flagcarried;
2250                 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2251                 tmp_entity.ctf_dropper = world;
2252         }
2253
2254         return false;
2255 }
2256
2257 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2258 {
2259         frag_score = 0;
2260         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2261 }
2262
2263 void ctf_RemovePlayer(entity player)
2264 {
2265         if(player.flagcarried)
2266                 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2267
2268         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2269         {
2270                 if(flag.pass_sender == player) { flag.pass_sender = world; }
2271                 if(flag.pass_target == player) { flag.pass_target = world; }
2272                 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2273         }
2274 }
2275
2276 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2277 {SELFPARAM();
2278         ctf_RemovePlayer(self);
2279         return false;
2280 }
2281
2282 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2283 {SELFPARAM();
2284         ctf_RemovePlayer(self);
2285         return false;
2286 }
2287
2288 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2289 {SELFPARAM();
2290         if(self.flagcarried)
2291         if(!autocvar_g_ctf_portalteleport)
2292                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2293
2294         return false;
2295 }
2296
2297 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2298 {SELFPARAM();
2299         if(MUTATOR_RETURNVALUE || gameover) { return false; }
2300
2301         entity player = self;
2302
2303         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2304         {
2305                 // pass the flag to a team mate
2306                 if(autocvar_g_ctf_pass)
2307                 {
2308                         entity head, closest_target = world;
2309                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2310
2311                         while(head) // find the closest acceptable target to pass to
2312                         {
2313                                 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2314                                 if(head != player && SAME_TEAM(head, player))
2315                                 if(!head.speedrunning && !head.vehicle)
2316                                 {
2317                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2318                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2319                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2320
2321                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2322                                         {
2323                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2324                                                 {
2325                                                         if(IS_BOT_CLIENT(head))
2326                                                         {
2327                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2328                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2329                                                         }
2330                                                         else
2331                                                         {
2332                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2333                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2334                                                         }
2335                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2336                                                         return true;
2337                                                 }
2338                                                 else if(player.flagcarried)
2339                                                 {
2340                                                         if(closest_target)
2341                                                         {
2342                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2343                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2344                                                                         { closest_target = head; }
2345                                                         }
2346                                                         else { closest_target = head; }
2347                                                 }
2348                                         }
2349                                 }
2350                                 head = head.chain;
2351                         }
2352
2353                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2354                 }
2355
2356                 // throw the flag in front of you
2357                 if(autocvar_g_ctf_throw && player.flagcarried)
2358                 {
2359                         if(player.throw_count == -1)
2360                         {
2361                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2362                                 {
2363                                         player.throw_prevtime = time;
2364                                         player.throw_count = 1;
2365                                         ctf_Handle_Throw(player, world, DROP_THROW);
2366                                         return true;
2367                                 }
2368                                 else
2369                                 {
2370                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2371                                         return false;
2372                                 }
2373                         }
2374                         else
2375                         {
2376                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2377                                 else { player.throw_count += 1; }
2378                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2379
2380                                 player.throw_prevtime = time;
2381                                 ctf_Handle_Throw(player, world, DROP_THROW);
2382                                 return true;
2383                         }
2384                 }
2385         }
2386
2387         return false;
2388 }
2389
2390 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2391 {SELFPARAM();
2392         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2393         {
2394                 self.wps_helpme_time = time;
2395                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2396         }
2397         else // create a normal help me waypointsprite
2398         {
2399                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2400                 WaypointSprite_Ping(self.wps_helpme);
2401         }
2402
2403         return true;
2404 }
2405
2406 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2407 {
2408         if(vh_player.flagcarried)
2409         {
2410                 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2411
2412                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2413                 {
2414                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2415                 }
2416                 else
2417                 {
2418                         setattachment(vh_player.flagcarried, vh_vehicle, "");
2419                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2420                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2421                         //vh_player.flagcarried.angles = '0 0 0';
2422                 }
2423                 return true;
2424         }
2425
2426         return false;
2427 }
2428
2429 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2430 {
2431         if(vh_player.flagcarried)
2432         {
2433                 setattachment(vh_player.flagcarried, vh_player, "");
2434                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2435                 vh_player.flagcarried.scale = FLAG_SCALE;
2436                 vh_player.flagcarried.angles = '0 0 0';
2437                 vh_player.flagcarried.nodrawtoclient = world;
2438                 return true;
2439         }
2440
2441         return false;
2442 }
2443
2444 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2445 {SELFPARAM();
2446         if(self.flagcarried)
2447         {
2448                 Send_Notification(NOTIF_ALL, world, MSG_INFO, ((self.flagcarried.team) ? APP_TEAM_ENT_4(self.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN_) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2449                 ctf_RespawnFlag(self.flagcarried);
2450                 return true;
2451         }
2452
2453         return false;
2454 }
2455
2456 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2457 {
2458         entity flag; // temporary entity for the search method
2459
2460         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2461         {
2462                 switch(flag.ctf_status)
2463                 {
2464                         case FLAG_DROPPED:
2465                         case FLAG_PASSING:
2466                         {
2467                                 // lock the flag, game is over
2468                                 flag.movetype = MOVETYPE_NONE;
2469                                 flag.takedamage = DAMAGE_NO;
2470                                 flag.solid = SOLID_NOT;
2471                                 flag.nextthink = false; // stop thinking
2472
2473                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2474                                 break;
2475                         }
2476
2477                         default:
2478                         case FLAG_BASE:
2479                         case FLAG_CARRY:
2480                         {
2481                                 // do nothing for these flags
2482                                 break;
2483                         }
2484                 }
2485         }
2486
2487         return false;
2488 }
2489
2490 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2491 {SELFPARAM();
2492         havocbot_ctf_reset_role(self);
2493         return true;
2494 }
2495
2496 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2497 {
2498         //ret_float = ctf_teams;
2499         ret_string = "ctf_team";
2500         return true;
2501 }
2502
2503 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2504 {SELFPARAM();
2505         self.ctf_flagstatus = other.ctf_flagstatus;
2506         return false;
2507 }
2508
2509 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2510 {
2511         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2512         {
2513                 if (MapInfo_Get_ByID(i))
2514                 {
2515                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2516
2517                         if(!r)
2518                                 continue;
2519
2520                         // TODO: uid2name
2521                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2522                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2523                 }
2524         }
2525
2526         return false;
2527 }
2528
2529 bool superspec_Spectate(entity _player); // TODO
2530 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2531 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2532 {
2533         if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2534
2535         if(cmd_name == "followfc")
2536         {
2537                 if(!g_ctf)
2538                         return true;
2539
2540                 entity _player;
2541                 int _team = 0;
2542                 bool found = false;
2543
2544                 if(cmd_argc == 2)
2545                 {
2546                         switch(argv(1))
2547                         {
2548                                 case "red": _team = NUM_TEAM_1; break;
2549                                 case "blue": _team = NUM_TEAM_2; break;
2550                                 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2551                                 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2552                         }
2553                 }
2554
2555                 FOR_EACH_PLAYER(_player)
2556                 {
2557                         if(_player.flagcarried && (_player.team == _team || _team == 0))
2558                         {
2559                                 found = true;
2560                                 if(_team == 0 && IS_SPEC(self) && self.enemy == _player)
2561                                         continue; // already spectating a fc, try to find the other fc
2562                                 return superspec_Spectate(_player);
2563                         }
2564                 }
2565
2566                 if(!found)
2567                         superspec_msg("", "", self, "No active flag carrier\n", 1);
2568                 return true;
2569         }
2570
2571         return false;
2572 }
2573
2574 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2575 {
2576         if(frag_target.flagcarried)
2577                 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2578
2579         return false;
2580 }
2581
2582
2583 // ==========
2584 // Spawnfuncs
2585 // ==========
2586
2587 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2588 CTF flag for team one (Red).
2589 Keys:
2590 "angle" Angle the flag will point (minus 90 degrees)...
2591 "model" model to use, note this needs red and blue as skins 0 and 1...
2592 "noise" sound played when flag is picked up...
2593 "noise1" sound played when flag is returned by a teammate...
2594 "noise2" sound played when flag is captured...
2595 "noise3" sound played when flag is lost in the field and respawns itself...
2596 "noise4" sound played when flag is dropped by a player...
2597 "noise5" sound played when flag touches the ground... */
2598 spawnfunc(item_flag_team1)
2599 {
2600         if(!g_ctf) { remove(self); return; }
2601
2602         ctf_FlagSetup(NUM_TEAM_1, self);
2603 }
2604
2605 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2606 CTF flag for team two (Blue).
2607 Keys:
2608 "angle" Angle the flag will point (minus 90 degrees)...
2609 "model" model to use, note this needs red and blue as skins 0 and 1...
2610 "noise" sound played when flag is picked up...
2611 "noise1" sound played when flag is returned by a teammate...
2612 "noise2" sound played when flag is captured...
2613 "noise3" sound played when flag is lost in the field and respawns itself...
2614 "noise4" sound played when flag is dropped by a player...
2615 "noise5" sound played when flag touches the ground... */
2616 spawnfunc(item_flag_team2)
2617 {
2618         if(!g_ctf) { remove(self); return; }
2619
2620         ctf_FlagSetup(NUM_TEAM_2, self);
2621 }
2622
2623 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2624 CTF flag for team three (Yellow).
2625 Keys:
2626 "angle" Angle the flag will point (minus 90 degrees)...
2627 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2628 "noise" sound played when flag is picked up...
2629 "noise1" sound played when flag is returned by a teammate...
2630 "noise2" sound played when flag is captured...
2631 "noise3" sound played when flag is lost in the field and respawns itself...
2632 "noise4" sound played when flag is dropped by a player...
2633 "noise5" sound played when flag touches the ground... */
2634 spawnfunc(item_flag_team3)
2635 {
2636         if(!g_ctf) { remove(self); return; }
2637
2638         ctf_FlagSetup(NUM_TEAM_3, self);
2639 }
2640
2641 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2642 CTF flag for team four (Pink).
2643 Keys:
2644 "angle" Angle the flag will point (minus 90 degrees)...
2645 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2646 "noise" sound played when flag is picked up...
2647 "noise1" sound played when flag is returned by a teammate...
2648 "noise2" sound played when flag is captured...
2649 "noise3" sound played when flag is lost in the field and respawns itself...
2650 "noise4" sound played when flag is dropped by a player...
2651 "noise5" sound played when flag touches the ground... */
2652 spawnfunc(item_flag_team4)
2653 {
2654         if(!g_ctf) { remove(self); return; }
2655
2656         ctf_FlagSetup(NUM_TEAM_4, self);
2657 }
2658
2659 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2660 CTF flag (Neutral).
2661 Keys:
2662 "angle" Angle the flag will point (minus 90 degrees)...
2663 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2664 "noise" sound played when flag is picked up...
2665 "noise1" sound played when flag is returned by a teammate...
2666 "noise2" sound played when flag is captured...
2667 "noise3" sound played when flag is lost in the field and respawns itself...
2668 "noise4" sound played when flag is dropped by a player...
2669 "noise5" sound played when flag touches the ground... */
2670 spawnfunc(item_flag_neutral)
2671 {
2672         if(!g_ctf) { remove(self); return; }
2673         if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2674
2675         ctf_FlagSetup(0, self);
2676 }
2677
2678 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2679 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2680 Note: If you use spawnfunc_ctf_team entities you must define at least 2!  However, unlike domination, you don't need to make a blank one too.
2681 Keys:
2682 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2683 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2684 spawnfunc(ctf_team)
2685 {
2686         if(!g_ctf) { remove(self); return; }
2687
2688         self.classname = "ctf_team";
2689         self.team = self.cnt + 1;
2690 }
2691
2692 // compatibility for quake maps
2693 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2694 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2695 spawnfunc(info_player_team1);
2696 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2697 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2698 spawnfunc(info_player_team2);
2699 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2700 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2701
2702 void team_CTF_neutralflag()                      { SELFPARAM(); spawnfunc_item_flag_neutral(self);  }
2703 void team_neutralobelisk()                       { SELFPARAM(); spawnfunc_item_flag_neutral(self);  }
2704
2705
2706 // ==============
2707 // Initialization
2708 // ==============
2709
2710 // scoreboard setup
2711 void ctf_ScoreRules(int teams)
2712 {
2713         CheckAllowedTeams(world);
2714         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2715         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2716         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2717         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2718         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2719         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2720         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2721         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2722         ScoreRules_basics_end();
2723 }
2724
2725 // code from here on is just to support maps that don't have flag and team entities
2726 void ctf_SpawnTeam (string teamname, int teamcolor)
2727 {
2728         entity this = new(ctf_team);
2729         this.netname = teamname;
2730         this.cnt = teamcolor;
2731         this.spawnfunc_checked = true;
2732         WITH(entity, self, this, spawnfunc_ctf_team(this));
2733 }
2734
2735 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2736 {
2737         ctf_teams = 2;
2738
2739         entity tmp_entity;
2740         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2741         {
2742                 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2743                 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2744                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2745         }
2746
2747         ctf_teams = bound(2, ctf_teams, 4);
2748
2749         // if no teams are found, spawn defaults
2750         if(find(world, classname, "ctf_team") == world)
2751         {
2752                 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2753                 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2754                 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2755                 if(ctf_teams >= 3)
2756                         ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2757                 if(ctf_teams >= 4)
2758                         ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2759         }
2760
2761         ctf_ScoreRules(ctf_teams);
2762 }
2763
2764 void ctf_Initialize()
2765 {
2766         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2767
2768         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2769         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2770         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2771
2772         addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2773
2774         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2775 }
2776
2777 #endif