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