]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Reset: `this` param
[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         MUTATOR_ONADD
10         {
11                 if (time > 1) // game loads at time 1
12                         error("This is a game type and it cannot be added at runtime.");
13                 ctf_Initialize();
14
15                 ActivateTeamplay();
16                 SetLimits(autocvar_capturelimit_override, autocvar_captureleadlimit_override, -1, -1);
17                 have_team_spawns = -1; // request team spawns
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 void shockwave_spawn(string m, vector org, float sz, float t1, float t2)
682 {
683         return modeleffect_spawn(m, 0, 0, org, '0 0 0', '0 0 0', '0 0 0', 0, sz, 1, t1, t2);
684 }
685
686 // ==============
687 // Event Handlers
688 // ==============
689
690 void ctf_Handle_Capture(entity flag, entity toucher, int capturetype)
691 {
692         entity enemy_flag = ((capturetype == CAPTURE_NORMAL) ? toucher.flagcarried : toucher);
693         entity player = ((capturetype == CAPTURE_NORMAL) ? toucher : enemy_flag.ctf_dropper);
694         entity player_team_flag = world, tmp_entity;
695         float old_time, new_time;
696
697         if(!player) { return; } // without someone to give the reward to, we can't possibly cap
698         if(CTF_DIFFTEAM(player, flag)) { return; }
699
700         if(ctf_oneflag)
701         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
702         if(SAME_TEAM(tmp_entity, player))
703         {
704                 player_team_flag = tmp_entity;
705                 break;
706         }
707
708         nades_GiveBonus(player, autocvar_g_nades_bonus_score_high );
709
710         player.throw_prevtime = time;
711         player.throw_count = 0;
712
713         // messages and sounds
714         Send_Notification(NOTIF_ONE, player, MSG_CENTER, ((enemy_flag.team) ? APP_TEAM_ENT_4(enemy_flag, CENTER_CTF_CAPTURE_) : CENTER_CTF_CAPTURE_NEUTRAL));
715         ctf_CaptureRecord(enemy_flag, player);
716         _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);
717
718         switch(capturetype)
719         {
720                 case CAPTURE_NORMAL: ctf_EventLog("capture", enemy_flag.team, player); break;
721                 case CAPTURE_DROPPED: ctf_EventLog("droppedcapture", enemy_flag.team, player); break;
722                 default: break;
723         }
724
725         // scoring
726         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_capture);
727         PlayerTeamScore_Add(player, SP_CTF_CAPS, ST_CTF_CAPS, 1);
728
729         old_time = PlayerScore_Add(player, SP_CTF_CAPTIME, 0);
730         new_time = TIME_ENCODE(time - enemy_flag.ctf_pickuptime);
731         if(!old_time || new_time < old_time)
732                 PlayerScore_Add(player, SP_CTF_CAPTIME, new_time - old_time);
733
734         // effects
735         Send_Effect_(flag.capeffect, flag.origin, '0 0 0', 1);
736         //shockwave_spawn("models/ctf/shockwavetransring.md3", flag.origin - '0 0 15', -0.8, 0, 1);
737
738         // other
739         if(capturetype == CAPTURE_NORMAL)
740         {
741                 WaypointSprite_Kill(player.wps_flagcarrier);
742                 if(flag.speedrunning) { ctf_FakeTimeLimit(player, -1); }
743
744                 if((enemy_flag.ctf_dropper) && (player != enemy_flag.ctf_dropper))
745                         { PlayerTeamScore_AddScore(enemy_flag.ctf_dropper, autocvar_g_ctf_score_capture_assist); }
746         }
747
748         // reset the flag
749         player.next_take_time = time + autocvar_g_ctf_flag_collect_delay;
750         ctf_RespawnFlag(enemy_flag);
751 }
752
753 void ctf_Handle_Return(entity flag, entity player)
754 {
755         // messages and sounds
756         if(IS_MONSTER(player))
757         {
758                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_MONSTER_), player.monster_name);
759         }
760         else if(flag.team)
761         {
762                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_RETURN_));
763                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_ENT_4(flag, INFO_CTF_RETURN_), player.netname);
764         }
765         _sound(player, CH_TRIGGER, flag.snd_flag_returned, VOL_BASE, ATTEN_NONE);
766         ctf_EventLog("return", flag.team, player);
767
768         // scoring
769         if(IS_PLAYER(player))
770         {
771                 PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return
772                 PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns
773
774                 nades_GiveBonus(player,autocvar_g_nades_bonus_score_medium);
775         }
776
777         TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it
778
779         if(flag.ctf_dropper)
780         {
781                 PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag
782                 ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag
783                 flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time
784         }
785
786         // other
787         if(player.flagcarried == flag)
788                 WaypointSprite_Kill(player.wps_flagcarrier);
789
790         // reset the flag
791         ctf_RespawnFlag(flag);
792 }
793
794 void ctf_Handle_Pickup(entity flag, entity player, int pickuptype)
795 {
796         // declarations
797         float pickup_dropped_score; // used to calculate dropped pickup score
798         entity tmp_entity; // temporary entity
799
800         // attach the flag to the player
801         flag.owner = player;
802         player.flagcarried = flag;
803         if(player.vehicle)
804         {
805                 setattachment(flag, player.vehicle, "");
806                 setorigin(flag, VEHICLE_FLAG_OFFSET);
807                 flag.scale = VEHICLE_FLAG_SCALE;
808         }
809         else
810         {
811                 setattachment(flag, player, "");
812                 setorigin(flag, FLAG_CARRY_OFFSET);
813         }
814
815         // flag setup
816         flag.movetype = MOVETYPE_NONE;
817         flag.takedamage = DAMAGE_NO;
818         flag.solid = SOLID_NOT;
819         flag.angles = '0 0 0';
820         flag.ctf_status = FLAG_CARRY;
821
822         switch(pickuptype)
823         {
824                 case PICKUP_BASE: flag.ctf_pickuptime = time; break; // used for timing runs
825                 case PICKUP_DROPPED: flag.health = flag.max_flag_health; break; // reset health/return timelimit
826                 default: break;
827         }
828
829         // messages and sounds
830         Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_PICKUP_) : INFO_CTF_PICKUP_NEUTRAL), player.netname);
831         if(ctf_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_STALEMATE_CARRIER); }
832         if(!flag.team) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PICKUP_NEUTRAL); }
833         else if(CTF_DIFFTEAM(player, flag)) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_ENT_4(flag, CENTER_CTF_PICKUP_)); }
834         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)); }
835
836         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);
837
838         if(!flag.team)
839         FOR_EACH_PLAYER(tmp_entity)
840         if(tmp_entity != player)
841         if(DIFF_TEAM(player, tmp_entity))
842                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, CHOICE_CTF_PICKUP_ENEMY_NEUTRAL, Team_ColorCode(player.team), player.netname);
843
844         if(flag.team)
845         FOR_EACH_PLAYER(tmp_entity)
846         if(tmp_entity != player)
847         if(CTF_SAMETEAM(flag, tmp_entity))
848         if(SAME_TEAM(player, tmp_entity))
849                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CHOICE, APP_TEAM_ENT_4(flag, CHOICE_CTF_PICKUP_TEAM_), Team_ColorCode(player.team), player.netname);
850         else
851                 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);
852
853         _sound(player, CH_TRIGGER, flag.snd_flag_taken, VOL_BASE, ATTEN_NONE);
854
855         // scoring
856         PlayerScore_Add(player, SP_CTF_PICKUPS, 1);
857         nades_GiveBonus(player, autocvar_g_nades_bonus_score_minor);
858         switch(pickuptype)
859         {
860                 case PICKUP_BASE:
861                 {
862                         PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_pickup_base);
863                         ctf_EventLog("steal", flag.team, player);
864                         break;
865                 }
866
867                 case PICKUP_DROPPED:
868                 {
869                         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);
870                         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);
871                         LOG_TRACE("pickup_dropped_score is ", ftos(pickup_dropped_score), "\n");
872                         PlayerTeamScore_AddScore(player, pickup_dropped_score);
873                         ctf_EventLog("pickup", flag.team, player);
874                         break;
875                 }
876
877                 default: break;
878         }
879
880         // speedrunning
881         if(pickuptype == PICKUP_BASE)
882         {
883                 flag.speedrunning = player.speedrunning; // if speedrunning, flag will flag-return and teleport the owner back after the record
884                 if((player.speedrunning) && (ctf_captimerecord))
885                         ctf_FakeTimeLimit(player, time + ctf_captimerecord);
886         }
887
888         // effects
889         Send_Effect_(flag.toucheffect, player.origin, '0 0 0', 1);
890
891         // waypoints
892         if(pickuptype == PICKUP_DROPPED) { WaypointSprite_Kill(flag.wps_flagdropped); }
893         ctf_FlagcarrierWaypoints(player);
894         WaypointSprite_Ping(player.wps_flagcarrier);
895 }
896
897
898 // ===================
899 // Main Flag Functions
900 // ===================
901
902 void ctf_CheckFlagReturn(entity flag, int returntype)
903 {
904         if((flag.ctf_status == FLAG_DROPPED) || (flag.ctf_status == FLAG_PASSING))
905         {
906                 if(flag.wps_flagdropped) { WaypointSprite_UpdateHealth(flag.wps_flagdropped, flag.health); }
907
908                 if((flag.health <= 0) || (time >= flag.ctf_droptime + autocvar_g_ctf_flag_return_time))
909                 {
910                         switch(returntype)
911                         {
912                                 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;
913                                 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;
914                                 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;
915                                 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;
916
917                                 default:
918                                 case RETURN_TIMEOUT:
919                                         { Send_Notification(NOTIF_ALL, world, MSG_INFO, ((flag.team) ? APP_TEAM_ENT_4(flag, INFO_CTF_FLAGRETURN_TIMEOUT_) : INFO_CTF_FLAGRETURN_TIMEOUT_NEUTRAL)); break; }
920                         }
921                         _sound(flag, CH_TRIGGER, flag.snd_flag_respawn, VOL_BASE, ATTEN_NONE);
922                         ctf_EventLog("returned", flag.team, world);
923                         ctf_RespawnFlag(flag);
924                 }
925         }
926 }
927
928 bool ctf_Stalemate_Customize()
929 {SELFPARAM();
930         // make spectators see what the player would see
931         entity e, wp_owner;
932         e = WaypointSprite_getviewentity(other);
933         wp_owner = self.owner;
934
935         // team waypoints
936         if(CTF_SAMETEAM(wp_owner.flagcarried, wp_owner)) { return false; }
937         if(SAME_TEAM(wp_owner, e)) { return false; }
938         if(!IS_PLAYER(e)) { return false; }
939
940         return true;
941 }
942
943 void ctf_CheckStalemate()
944 {
945         // declarations
946         int stale_flags = 0, stale_red_flags = 0, stale_blue_flags = 0, stale_yellow_flags = 0, stale_pink_flags = 0, stale_neutral_flags = 0;
947         entity tmp_entity;
948
949         entity ctf_staleflaglist = world; // reset the list, we need to build the list each time this function runs
950
951         // build list of stale flags
952         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
953         {
954                 if(autocvar_g_ctf_stalemate)
955                 if(tmp_entity.ctf_status != FLAG_BASE)
956                 if(time >= tmp_entity.ctf_pickuptime + autocvar_g_ctf_stalemate_time || !tmp_entity.team) // instant stalemate in oneflag
957                 {
958                         tmp_entity.ctf_staleflagnext = ctf_staleflaglist; // link flag into staleflaglist
959                         ctf_staleflaglist = tmp_entity;
960
961                         switch(tmp_entity.team)
962                         {
963                                 case NUM_TEAM_1: ++stale_red_flags; break;
964                                 case NUM_TEAM_2: ++stale_blue_flags; break;
965                                 case NUM_TEAM_3: ++stale_yellow_flags; break;
966                                 case NUM_TEAM_4: ++stale_pink_flags; break;
967                                 default: ++stale_neutral_flags; break;
968                         }
969                 }
970         }
971
972         if(ctf_oneflag)
973                 stale_flags = (stale_neutral_flags >= 1);
974         else
975                 stale_flags = (stale_red_flags >= 1) + (stale_blue_flags >= 1) + (stale_yellow_flags >= 1) + (stale_pink_flags >= 1);
976
977         if(ctf_oneflag && stale_flags == 1)
978                 ctf_stalemate = true;
979         else if(stale_flags >= 2)
980                 ctf_stalemate = true;
981         else if(stale_flags == 0 && autocvar_g_ctf_stalemate_endcondition == 2)
982                 { ctf_stalemate = false; wpforenemy_announced = false; }
983         else if(stale_flags < 2 && autocvar_g_ctf_stalemate_endcondition == 1)
984                 { ctf_stalemate = false; wpforenemy_announced = false; }
985
986         // if sufficient stalemate, then set up the waypointsprite and announce the stalemate if necessary
987         if(ctf_stalemate)
988         {
989                 for(tmp_entity = ctf_staleflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_staleflagnext)
990                 {
991                         if((tmp_entity.owner) && (!tmp_entity.owner.wps_enemyflagcarrier))
992                         {
993                                 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);
994                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
995                                 tmp_entity.owner.wps_enemyflagcarrier.customizeentityforclient = ctf_Stalemate_Customize;
996                         }
997                 }
998
999                 if (!wpforenemy_announced)
1000                 {
1001                         FOR_EACH_REALPLAYER(tmp_entity)
1002                                 Send_Notification(NOTIF_ONE, tmp_entity, MSG_CENTER, ((tmp_entity.flagcarried) ? CENTER_CTF_STALEMATE_CARRIER : CENTER_CTF_STALEMATE_OTHER));
1003
1004                         wpforenemy_announced = true;
1005                 }
1006         }
1007 }
1008
1009 void ctf_FlagDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
1010 {SELFPARAM();
1011         if(ITEM_DAMAGE_NEEDKILL(deathtype))
1012         {
1013                 if(autocvar_g_ctf_flag_return_damage_delay)
1014                 {
1015                         self.ctf_flagdamaged = true;
1016                 }
1017                 else
1018                 {
1019                         self.health = 0;
1020                         ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1021                 }
1022                 return;
1023         }
1024         if(autocvar_g_ctf_flag_return_damage)
1025         {
1026                 // reduce health and check if it should be returned
1027                 self.health = self.health - damage;
1028                 ctf_CheckFlagReturn(self, RETURN_DAMAGE);
1029                 return;
1030         }
1031 }
1032
1033 void ctf_FlagThink()
1034 {SELFPARAM();
1035         // declarations
1036         entity tmp_entity;
1037
1038         self.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
1039
1040         // captureshield
1041         if(self == ctf_worldflaglist) // only for the first flag
1042                 FOR_EACH_CLIENT(tmp_entity)
1043                         ctf_CaptureShield_Update(tmp_entity, 1); // release shield only
1044
1045         // sanity checks
1046         if(self.mins != CTF_FLAG.m_mins || self.maxs != CTF_FLAG.m_maxs) { // reset the flag boundaries in case it got squished
1047                 LOG_TRACE("wtf the flag got squashed?\n");
1048                 tracebox(self.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, self.origin, MOVE_NOMONSTERS, self);
1049                 if(!trace_startsolid || self.noalign) // can we resize it without getting stuck?
1050                         setsize(self, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
1051
1052         switch(self.ctf_status) // reset flag angles in case warpzones adjust it
1053         {
1054                 case FLAG_DROPPED:
1055                 {
1056                         self.angles = '0 0 0';
1057                         break;
1058                 }
1059
1060                 default: break;
1061         }
1062
1063         // main think method
1064         switch(self.ctf_status)
1065         {
1066                 case FLAG_BASE:
1067                 {
1068                         if(autocvar_g_ctf_dropped_capture_radius)
1069                         {
1070                                 for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
1071                                         if(tmp_entity.ctf_status == FLAG_DROPPED)
1072                                         if(vlen(self.origin - tmp_entity.origin) < autocvar_g_ctf_dropped_capture_radius)
1073                                         if(time > tmp_entity.ctf_droptime + autocvar_g_ctf_dropped_capture_delay)
1074                                                 ctf_Handle_Capture(self, tmp_entity, CAPTURE_DROPPED);
1075                         }
1076                         return;
1077                 }
1078
1079                 case FLAG_DROPPED:
1080                 {
1081                         if(autocvar_g_ctf_flag_dropped_floatinwater)
1082                         {
1083                                 vector midpoint = ((self.absmin + self.absmax) * 0.5);
1084                                 if(pointcontents(midpoint) == CONTENT_WATER)
1085                                 {
1086                                         self.velocity = self.velocity * 0.5;
1087
1088                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
1089                                                 { self.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
1090                                         else
1091                                                 { self.movetype = MOVETYPE_FLY; }
1092                                 }
1093                                 else if(self.movetype == MOVETYPE_FLY) { self.movetype = MOVETYPE_TOSS; }
1094                         }
1095                         if(autocvar_g_ctf_flag_return_dropped)
1096                         {
1097                                 if((vlen(self.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_dropped) || (autocvar_g_ctf_flag_return_dropped == -1))
1098                                 {
1099                                         self.health = 0;
1100                                         ctf_CheckFlagReturn(self, RETURN_DROPPED);
1101                                         return;
1102                                 }
1103                         }
1104                         if(self.ctf_flagdamaged)
1105                         {
1106                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
1107                                 ctf_CheckFlagReturn(self, RETURN_NEEDKILL);
1108                                 return;
1109                         }
1110                         else if(autocvar_g_ctf_flag_return_time)
1111                         {
1112                                 self.health -= ((self.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
1113                                 ctf_CheckFlagReturn(self, RETURN_TIMEOUT);
1114                                 return;
1115                         }
1116                         return;
1117                 }
1118
1119                 case FLAG_CARRY:
1120                 {
1121                         if(self.speedrunning && ctf_captimerecord && (time >= self.ctf_pickuptime + ctf_captimerecord))
1122                         {
1123                                 self.health = 0;
1124                                 ctf_CheckFlagReturn(self, RETURN_SPEEDRUN);
1125
1126                                 setself(self.owner);
1127                                 self.impulse = CHIMPULSE_SPEEDRUN; // move the player back to the waypoint they set
1128                                 ImpulseCommands();
1129                                 setself(this);
1130                         }
1131                         if(autocvar_g_ctf_stalemate)
1132                         {
1133                                 if(time >= wpforenemy_nextthink)
1134                                 {
1135                                         ctf_CheckStalemate();
1136                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
1137                                 }
1138                         }
1139                         if(CTF_SAMETEAM(self, self.owner) && self.team)
1140                         {
1141                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
1142                                         ctf_Handle_Throw(self.owner, world, DROP_THROW);
1143                                 else if(vlen(self.owner.origin - self.ctf_spawnorigin) <= autocvar_g_ctf_flag_return_carried_radius)
1144                                         ctf_Handle_Return(self, self.owner);
1145                         }
1146                         return;
1147                 }
1148
1149                 case FLAG_PASSING:
1150                 {
1151                         vector targ_origin = ((self.pass_target.absmin + self.pass_target.absmax) * 0.5);
1152                         targ_origin = WarpZone_RefSys_TransformOrigin(self.pass_target, self, targ_origin); // origin of target as seen by the flag (us)
1153                         WarpZone_TraceLine(self.origin, targ_origin, MOVE_NOMONSTERS, self);
1154
1155                         if((self.pass_target == world)
1156                                 || (self.pass_target.deadflag != DEAD_NO)
1157                                 || (self.pass_target.flagcarried)
1158                                 || (vlen(self.origin - targ_origin) > autocvar_g_ctf_pass_radius)
1159                                 || ((trace_fraction < 1) && (trace_ent != self.pass_target))
1160                                 || (time > self.ctf_droptime + autocvar_g_ctf_pass_timelimit))
1161                         {
1162                                 // give up, pass failed
1163                                 ctf_Handle_Drop(self, world, DROP_PASS);
1164                         }
1165                         else
1166                         {
1167                                 // still a viable target, go for it
1168                                 ctf_CalculatePassVelocity(self, targ_origin, self.origin, true);
1169                         }
1170                         return;
1171                 }
1172
1173                 default: // this should never happen
1174                 {
1175                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
1176                         return;
1177                 }
1178         }
1179 }
1180
1181 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1182 {
1183         return = false;
1184         if(gameover) { return; }
1185         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1186
1187         bool is_not_monster = (!IS_MONSTER(toucher));
1188
1189         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1190         if(ITEM_TOUCH_NEEDKILL())
1191         {
1192                 if(!autocvar_g_ctf_flag_return_damage_delay)
1193                 {
1194                         flag.health = 0;
1195                         ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1196                 }
1197                 if(!flag.ctf_flagdamaged) { return; }
1198         }
1199
1200         int num_perteam = 0;
1201         entity tmp_entity; FOR_EACH_PLAYER(tmp_entity) if(SAME_TEAM(toucher, tmp_entity)) { ++num_perteam; }
1202
1203         // special touch behaviors
1204         if(toucher.frozen) { return; }
1205         else if(IS_VEHICLE(toucher))
1206         {
1207                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1208                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1209                 else
1210                         return; // do nothing
1211         }
1212         else if(IS_MONSTER(toucher))
1213         {
1214                 if(!autocvar_g_ctf_allow_monster_touch)
1215                         return; // do nothing
1216         }
1217         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1218         {
1219                 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1220                 {
1221                         Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1222                         _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1223                         flag.wait = time + FLAG_TOUCHRATE;
1224                 }
1225                 return;
1226         }
1227         else if(toucher.deadflag != DEAD_NO) { return; }
1228
1229         switch(flag.ctf_status)
1230         {
1231                 case FLAG_BASE:
1232                 {
1233                         if(ctf_oneflag)
1234                         {
1235                                 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1236                                         ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1237                                 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1238                                         ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1239                         }
1240                         else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1241                                 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1242                         else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1243                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1244                         break;
1245                 }
1246
1247                 case FLAG_DROPPED:
1248                 {
1249                         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
1250                                 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1251                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1252                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1253                         break;
1254                 }
1255
1256                 case FLAG_CARRY:
1257                 {
1258                         LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1259                         break;
1260                 }
1261
1262                 case FLAG_PASSING:
1263                 {
1264                         if((IS_PLAYER(toucher)) && (toucher.deadflag == DEAD_NO) && (toucher != flag.pass_sender))
1265                         {
1266                                 if(DIFF_TEAM(toucher, flag.pass_sender))
1267                                         ctf_Handle_Return(flag, toucher);
1268                                 else
1269                                         ctf_Handle_Retrieve(flag, toucher);
1270                         }
1271                         break;
1272                 }
1273         }
1274 }
1275
1276 .float last_respawn;
1277 void ctf_RespawnFlag(entity flag)
1278 {
1279         // check for flag respawn being called twice in a row
1280         if(flag.last_respawn > time - 0.5)
1281                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1282
1283         flag.last_respawn = time;
1284
1285         // reset the player (if there is one)
1286         if((flag.owner) && (flag.owner.flagcarried == flag))
1287         {
1288                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1289                 WaypointSprite_Kill(flag.wps_flagcarrier);
1290
1291                 flag.owner.flagcarried = world;
1292
1293                 if(flag.speedrunning)
1294                         ctf_FakeTimeLimit(flag.owner, -1);
1295         }
1296
1297         if((flag.owner) && (flag.owner.vehicle))
1298                 flag.scale = FLAG_SCALE;
1299
1300         if(flag.ctf_status == FLAG_DROPPED)
1301                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1302
1303         // reset the flag
1304         setattachment(flag, world, "");
1305         setorigin(flag, flag.ctf_spawnorigin);
1306
1307         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1308         flag.takedamage = DAMAGE_NO;
1309         flag.health = flag.max_flag_health;
1310         flag.solid = SOLID_TRIGGER;
1311         flag.velocity = '0 0 0';
1312         flag.angles = flag.mangle;
1313         flag.flags = FL_ITEM | FL_NOTARGET;
1314
1315         flag.ctf_status = FLAG_BASE;
1316         flag.owner = world;
1317         flag.pass_distance = 0;
1318         flag.pass_sender = world;
1319         flag.pass_target = world;
1320         flag.ctf_dropper = world;
1321         flag.ctf_pickuptime = 0;
1322         flag.ctf_droptime = 0;
1323         flag.ctf_flagdamaged = 0;
1324
1325         ctf_CheckStalemate();
1326 }
1327
1328 void ctf_Reset(entity this)
1329 {
1330         if(this.owner && IS_PLAYER(this.owner))
1331         ctf_Handle_Throw(this.owner, world, DROP_RESET);
1332
1333         ctf_RespawnFlag(this);
1334 }
1335
1336 void ctf_DelayedFlagSetup() // called after a flag is placed on a map by ctf_FlagSetup()
1337 {SELFPARAM();
1338         // bot waypoints
1339         waypoint_spawnforitem_force(self, self.origin);
1340         self.nearestwaypointtimeout = 0; // activate waypointing again
1341         self.bot_basewaypoint = self.nearestwaypoint;
1342
1343         // waypointsprites
1344         entity basename;
1345         switch (self.team)
1346         {
1347                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1348                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1349                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1350                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1351                 default: basename = WP_FlagBaseNeutral; break;
1352         }
1353
1354         entity wp = WaypointSprite_SpawnFixed(basename, self.origin + FLAG_WAYPOINT_OFFSET, self, wps_flagbase, RADARICON_FLAG);
1355         wp.colormod = ((self.team) ? Team_ColorRGB(self.team) : '1 1 1');
1356         WaypointSprite_UpdateTeamRadar(self.wps_flagbase, RADARICON_FLAG, ((self.team) ? colormapPaletteColor(self.team - 1, false) : '1 1 1'));
1357
1358         // captureshield setup
1359         ctf_CaptureShield_Spawn(self);
1360 }
1361
1362 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1363 {SELFPARAM();
1364         // declarations
1365         setself(flag); // for later usage with droptofloor()
1366
1367         // main setup
1368         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1369         ctf_worldflaglist = flag;
1370
1371         setattachment(flag, world, "");
1372
1373         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1374         flag.team = teamnumber;
1375         flag.classname = "item_flag_team";
1376         flag.target = "###item###"; // wut?
1377         flag.flags = FL_ITEM | FL_NOTARGET;
1378         flag.solid = SOLID_TRIGGER;
1379         flag.takedamage = DAMAGE_NO;
1380         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1381         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1382         flag.health = flag.max_flag_health;
1383         flag.event_damage = ctf_FlagDamage;
1384         flag.pushable = true;
1385         flag.teleportable = TELEPORT_NORMAL;
1386         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
1387         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1388         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1389         flag.velocity = '0 0 0';
1390         flag.mangle = flag.angles;
1391         flag.reset = ctf_Reset;
1392         flag.touch = ctf_FlagTouch;
1393         flag.think = ctf_FlagThink;
1394         flag.nextthink = time + FLAG_THINKRATE;
1395         flag.ctf_status = FLAG_BASE;
1396
1397         string teamname = Static_Team_ColorName_Lower(teamnumber);
1398         // appearence
1399         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1400         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1401         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1402         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1403         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1404         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1405
1406         // sounds
1407         flag.snd_flag_taken = strzone(SND(CTF_TAKEN(teamnumber)));
1408         flag.snd_flag_returned = strzone(SND(CTF_RETURNED(teamnumber)));
1409         flag.snd_flag_capture = strzone(SND(CTF_CAPTURE(teamnumber)));
1410         flag.snd_flag_dropped = strzone(SND(CTF_DROPPED(teamnumber)));
1411         if (flag.snd_flag_respawn == "") flag.snd_flag_respawn = strzone(SND(CTF_RESPAWN)); // if there is ever a team-based sound for this, update the code to match.
1412         precache_sound(flag.snd_flag_respawn);
1413         if (flag.snd_flag_touch == "") flag.snd_flag_touch = strzone(SND(CTF_TOUCH)); // again has no team-based sound
1414         precache_sound(flag.snd_flag_touch);
1415         if (flag.snd_flag_pass == "") flag.snd_flag_pass = strzone(SND(CTF_PASS)); // same story here
1416         precache_sound(flag.snd_flag_pass);
1417
1418         // precache
1419         precache_model(flag.model);
1420
1421         // appearence
1422         _setmodel(flag, flag.model); // precision set below
1423         setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1424         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1425
1426         if(autocvar_g_ctf_flag_glowtrails)
1427         {
1428                 switch(teamnumber)
1429                 {
1430                         case NUM_TEAM_1: flag.glow_color = 251; break;
1431                         case NUM_TEAM_2: flag.glow_color = 210; break;
1432                         case NUM_TEAM_3: flag.glow_color = 110; break;
1433                         case NUM_TEAM_4: flag.glow_color = 145; break;
1434                         default:                 flag.glow_color = 254; break;
1435                 }
1436                 flag.glow_size = 25;
1437                 flag.glow_trail = 1;
1438         }
1439
1440         flag.effects |= EF_LOWPRECISION;
1441         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1442         if(autocvar_g_ctf_dynamiclights)
1443         {
1444                 switch(teamnumber)
1445                 {
1446                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1447                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1448                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1449                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1450                         default:                 flag.effects |= EF_DIMLIGHT; break;
1451                 }
1452         }
1453
1454         // flag placement
1455         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1456         {
1457                 flag.dropped_origin = flag.origin;
1458                 flag.noalign = true;
1459                 flag.movetype = MOVETYPE_NONE;
1460         }
1461         else // drop to floor, automatically find a platform and set that as spawn origin
1462         {
1463                 flag.noalign = false;
1464                 setself(flag);
1465                 droptofloor();
1466                 flag.movetype = MOVETYPE_TOSS;
1467         }
1468
1469         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1470 }
1471
1472
1473 // ================
1474 // Bot player logic
1475 // ================
1476
1477 // NOTE: LEGACY CODE, needs to be re-written!
1478
1479 void havocbot_calculate_middlepoint()
1480 {
1481         entity f;
1482         vector s = '0 0 0';
1483         vector fo = '0 0 0';
1484         float n = 0;
1485
1486         f = ctf_worldflaglist;
1487         while (f)
1488         {
1489                 fo = f.origin;
1490                 s = s + fo;
1491                 f = f.ctf_worldflagnext;
1492         }
1493         if(!n)
1494                 return;
1495         havocbot_ctf_middlepoint = s * (1.0 / n);
1496         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1497 }
1498
1499
1500 entity havocbot_ctf_find_flag(entity bot)
1501 {
1502         entity f;
1503         f = ctf_worldflaglist;
1504         while (f)
1505         {
1506                 if (CTF_SAMETEAM(bot, f))
1507                         return f;
1508                 f = f.ctf_worldflagnext;
1509         }
1510         return world;
1511 }
1512
1513 entity havocbot_ctf_find_enemy_flag(entity bot)
1514 {
1515         entity f;
1516         f = ctf_worldflaglist;
1517         while (f)
1518         {
1519                 if(ctf_oneflag)
1520                 {
1521                         if(CTF_DIFFTEAM(bot, f))
1522                         {
1523                                 if(f.team)
1524                                 {
1525                                         if(bot.flagcarried)
1526                                                 return f;
1527                                 }
1528                                 else if(!bot.flagcarried)
1529                                         return f;
1530                         }
1531                 }
1532                 else if (CTF_DIFFTEAM(bot, f))
1533                         return f;
1534                 f = f.ctf_worldflagnext;
1535         }
1536         return world;
1537 }
1538
1539 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1540 {
1541         if (!teamplay)
1542                 return 0;
1543
1544         int c = 0;
1545         entity head;
1546
1547         FOR_EACH_PLAYER(head)
1548         {
1549                 if(DIFF_TEAM(head, bot) || head.deadflag != DEAD_NO || head == bot)
1550                         continue;
1551
1552                 if(vlen(head.origin - org) < tc_radius)
1553                         ++c;
1554         }
1555
1556         return c;
1557 }
1558
1559 void havocbot_goalrating_ctf_ourflag(float ratingscale)
1560 {SELFPARAM();
1561         entity head;
1562         head = ctf_worldflaglist;
1563         while (head)
1564         {
1565                 if (CTF_SAMETEAM(self, head))
1566                         break;
1567                 head = head.ctf_worldflagnext;
1568         }
1569         if (head)
1570                 navigation_routerating(head, ratingscale, 10000);
1571 }
1572
1573 void havocbot_goalrating_ctf_ourbase(float ratingscale)
1574 {SELFPARAM();
1575         entity head;
1576         head = ctf_worldflaglist;
1577         while (head)
1578         {
1579                 if (CTF_SAMETEAM(self, head))
1580                         break;
1581                 head = head.ctf_worldflagnext;
1582         }
1583         if (!head)
1584                 return;
1585
1586         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1587 }
1588
1589 void havocbot_goalrating_ctf_enemyflag(float ratingscale)
1590 {SELFPARAM();
1591         entity head;
1592         head = ctf_worldflaglist;
1593         while (head)
1594         {
1595                 if(ctf_oneflag)
1596                 {
1597                         if(CTF_DIFFTEAM(self, head))
1598                         {
1599                                 if(head.team)
1600                                 {
1601                                         if(self.flagcarried)
1602                                                 break;
1603                                 }
1604                                 else if(!self.flagcarried)
1605                                         break;
1606                         }
1607                 }
1608                 else if(CTF_DIFFTEAM(self, head))
1609                         break;
1610                 head = head.ctf_worldflagnext;
1611         }
1612         if (head)
1613                 navigation_routerating(head, ratingscale, 10000);
1614 }
1615
1616 void havocbot_goalrating_ctf_enemybase(float ratingscale)
1617 {SELFPARAM();
1618         if (!bot_waypoints_for_items)
1619         {
1620                 havocbot_goalrating_ctf_enemyflag(ratingscale);
1621                 return;
1622         }
1623
1624         entity head;
1625
1626         head = havocbot_ctf_find_enemy_flag(self);
1627
1628         if (!head)
1629                 return;
1630
1631         navigation_routerating(head.bot_basewaypoint, ratingscale, 10000);
1632 }
1633
1634 void havocbot_goalrating_ctf_ourstolenflag(float ratingscale)
1635 {SELFPARAM();
1636         entity mf;
1637
1638         mf = havocbot_ctf_find_flag(self);
1639
1640         if(mf.ctf_status == FLAG_BASE)
1641                 return;
1642
1643         if(mf.tag_entity)
1644                 navigation_routerating(mf.tag_entity, ratingscale, 10000);
1645 }
1646
1647 void havocbot_goalrating_ctf_droppedflags(float ratingscale, vector org, float df_radius)
1648 {
1649         entity head;
1650         head = ctf_worldflaglist;
1651         while (head)
1652         {
1653                 // flag is out in the field
1654                 if(head.ctf_status != FLAG_BASE)
1655                 if(head.tag_entity==world)      // dropped
1656                 {
1657                         if(df_radius)
1658                         {
1659                                 if(vlen(org-head.origin)<df_radius)
1660                                         navigation_routerating(head, ratingscale, 10000);
1661                         }
1662                         else
1663                                 navigation_routerating(head, ratingscale, 10000);
1664                 }
1665
1666                 head = head.ctf_worldflagnext;
1667         }
1668 }
1669
1670 void havocbot_goalrating_ctf_carrieritems(float ratingscale, vector org, float sradius)
1671 {SELFPARAM();
1672         entity head;
1673         float t;
1674         head = findchainfloat(bot_pickup, true);
1675         while (head)
1676         {
1677                 // gather health and armor only
1678                 if (head.solid)
1679                 if (head.health || head.armorvalue)
1680                 if (vlen(head.origin - org) < sradius)
1681                 {
1682                         // get the value of the item
1683                         t = head.bot_pickupevalfunc(self, head) * 0.0001;
1684                         if (t > 0)
1685                                 navigation_routerating(head, t * ratingscale, 500);
1686                 }
1687                 head = head.chain;
1688         }
1689 }
1690
1691 void havocbot_ctf_reset_role(entity bot)
1692 {
1693         float cdefense, cmiddle, coffense;
1694         entity mf, ef, head;
1695         float c;
1696
1697         if(bot.deadflag != DEAD_NO)
1698                 return;
1699
1700         if(vlen(havocbot_ctf_middlepoint)==0)
1701                 havocbot_calculate_middlepoint();
1702
1703         // Check ctf flags
1704         if (bot.flagcarried)
1705         {
1706                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_CARRIER);
1707                 return;
1708         }
1709
1710         mf = havocbot_ctf_find_flag(bot);
1711         ef = havocbot_ctf_find_enemy_flag(bot);
1712
1713         // Retrieve stolen flag
1714         if(mf.ctf_status!=FLAG_BASE)
1715         {
1716                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_RETRIEVER);
1717                 return;
1718         }
1719
1720         // If enemy flag is taken go to the middle to intercept pursuers
1721         if(ef.ctf_status!=FLAG_BASE)
1722         {
1723                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1724                 return;
1725         }
1726
1727         // if there is only me on the team switch to offense
1728         c = 0;
1729         FOR_EACH_PLAYER(head)
1730         if(SAME_TEAM(head, bot))
1731                 ++c;
1732
1733         if(c==1)
1734         {
1735                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1736                 return;
1737         }
1738
1739         // Evaluate best position to take
1740         // Count mates on middle position
1741         cmiddle = havocbot_ctf_teamcount(bot, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1742
1743         // Count mates on defense position
1744         cdefense = havocbot_ctf_teamcount(bot, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1745
1746         // Count mates on offense position
1747         coffense = havocbot_ctf_teamcount(bot, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1748
1749         if(cdefense<=coffense)
1750                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_DEFENSE);
1751         else if(coffense<=cmiddle)
1752                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_OFFENSE);
1753         else
1754                 havocbot_role_ctf_setrole(bot, HAVOCBOT_CTF_ROLE_MIDDLE);
1755 }
1756
1757 void havocbot_role_ctf_carrier()
1758 {SELFPARAM();
1759         if(self.deadflag != DEAD_NO)
1760         {
1761                 havocbot_ctf_reset_role(self);
1762                 return;
1763         }
1764
1765         if (self.flagcarried == world)
1766         {
1767                 havocbot_ctf_reset_role(self);
1768                 return;
1769         }
1770
1771         if (self.bot_strategytime < time)
1772         {
1773                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1774
1775                 navigation_goalrating_start();
1776                 if(ctf_oneflag)
1777                         havocbot_goalrating_ctf_enemybase(50000);
1778                 else
1779                         havocbot_goalrating_ctf_ourbase(50000);
1780
1781                 if(self.health<100)
1782                         havocbot_goalrating_ctf_carrieritems(1000, self.origin, 1000);
1783
1784                 navigation_goalrating_end();
1785
1786                 if (self.navigation_hasgoals)
1787                         self.havocbot_cantfindflag = time + 10;
1788                 else if (time > self.havocbot_cantfindflag)
1789                 {
1790                         // Can't navigate to my own base, suicide!
1791                         // TODO: drop it and wander around
1792                         Damage(self, self, self, 100000, DEATH_KILL.m_id, self.origin, '0 0 0');
1793                         return;
1794                 }
1795         }
1796 }
1797
1798 void havocbot_role_ctf_escort()
1799 {SELFPARAM();
1800         entity mf, ef;
1801
1802         if(self.deadflag != DEAD_NO)
1803         {
1804                 havocbot_ctf_reset_role(self);
1805                 return;
1806         }
1807
1808         if (self.flagcarried)
1809         {
1810                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1811                 return;
1812         }
1813
1814         // If enemy flag is back on the base switch to previous role
1815         ef = havocbot_ctf_find_enemy_flag(self);
1816         if(ef.ctf_status==FLAG_BASE)
1817         {
1818                 self.havocbot_role = self.havocbot_previous_role;
1819                 self.havocbot_role_timeout = 0;
1820                 return;
1821         }
1822
1823         // If the flag carrier reached the base switch to defense
1824         mf = havocbot_ctf_find_flag(self);
1825         if(mf.ctf_status!=FLAG_BASE)
1826         if(vlen(ef.origin - mf.dropped_origin) < 300)
1827         {
1828                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_DEFENSE);
1829                 return;
1830         }
1831
1832         // Set the role timeout if necessary
1833         if (!self.havocbot_role_timeout)
1834         {
1835                 self.havocbot_role_timeout = time + random() * 30 + 60;
1836         }
1837
1838         // If nothing happened just switch to previous role
1839         if (time > self.havocbot_role_timeout)
1840         {
1841                 self.havocbot_role = self.havocbot_previous_role;
1842                 self.havocbot_role_timeout = 0;
1843                 return;
1844         }
1845
1846         // Chase the flag carrier
1847         if (self.bot_strategytime < time)
1848         {
1849                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1850                 navigation_goalrating_start();
1851                 havocbot_goalrating_ctf_enemyflag(30000);
1852                 havocbot_goalrating_ctf_ourstolenflag(40000);
1853                 havocbot_goalrating_items(10000, self.origin, 10000);
1854                 navigation_goalrating_end();
1855         }
1856 }
1857
1858 void havocbot_role_ctf_offense()
1859 {SELFPARAM();
1860         entity mf, ef;
1861         vector pos;
1862
1863         if(self.deadflag != DEAD_NO)
1864         {
1865                 havocbot_ctf_reset_role(self);
1866                 return;
1867         }
1868
1869         if (self.flagcarried)
1870         {
1871                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1872                 return;
1873         }
1874
1875         // Check flags
1876         mf = havocbot_ctf_find_flag(self);
1877         ef = havocbot_ctf_find_enemy_flag(self);
1878
1879         // Own flag stolen
1880         if(mf.ctf_status!=FLAG_BASE)
1881         {
1882                 if(mf.tag_entity)
1883                         pos = mf.tag_entity.origin;
1884                 else
1885                         pos = mf.origin;
1886
1887                 // Try to get it if closer than the enemy base
1888                 if(vlen(self.origin-ef.dropped_origin)>vlen(self.origin-pos))
1889                 {
1890                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
1891                         return;
1892                 }
1893         }
1894
1895         // Escort flag carrier
1896         if(ef.ctf_status!=FLAG_BASE)
1897         {
1898                 if(ef.tag_entity)
1899                         pos = ef.tag_entity.origin;
1900                 else
1901                         pos = ef.origin;
1902
1903                 if(vlen(pos-mf.dropped_origin)>700)
1904                 {
1905                         havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_ESCORT);
1906                         return;
1907                 }
1908         }
1909
1910         // About to fail, switch to middlefield
1911         if(self.health<50)
1912         {
1913                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_MIDDLE);
1914                 return;
1915         }
1916
1917         // Set the role timeout if necessary
1918         if (!self.havocbot_role_timeout)
1919                 self.havocbot_role_timeout = time + 120;
1920
1921         if (time > self.havocbot_role_timeout)
1922         {
1923                 havocbot_ctf_reset_role(self);
1924                 return;
1925         }
1926
1927         if (self.bot_strategytime < time)
1928         {
1929                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1930                 navigation_goalrating_start();
1931                 havocbot_goalrating_ctf_ourstolenflag(50000);
1932                 havocbot_goalrating_ctf_enemybase(20000);
1933                 havocbot_goalrating_items(5000, self.origin, 1000);
1934                 havocbot_goalrating_items(1000, self.origin, 10000);
1935                 navigation_goalrating_end();
1936         }
1937 }
1938
1939 // Retriever (temporary role):
1940 void havocbot_role_ctf_retriever()
1941 {SELFPARAM();
1942         entity mf;
1943
1944         if(self.deadflag != DEAD_NO)
1945         {
1946                 havocbot_ctf_reset_role(self);
1947                 return;
1948         }
1949
1950         if (self.flagcarried)
1951         {
1952                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
1953                 return;
1954         }
1955
1956         // If flag is back on the base switch to previous role
1957         mf = havocbot_ctf_find_flag(self);
1958         if(mf.ctf_status==FLAG_BASE)
1959         {
1960                 havocbot_ctf_reset_role(self);
1961                 return;
1962         }
1963
1964         if (!self.havocbot_role_timeout)
1965                 self.havocbot_role_timeout = time + 20;
1966
1967         if (time > self.havocbot_role_timeout)
1968         {
1969                 havocbot_ctf_reset_role(self);
1970                 return;
1971         }
1972
1973         if (self.bot_strategytime < time)
1974         {
1975                 float rt_radius;
1976                 rt_radius = 10000;
1977
1978                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1979                 navigation_goalrating_start();
1980                 havocbot_goalrating_ctf_ourstolenflag(50000);
1981                 havocbot_goalrating_ctf_droppedflags(40000, self.origin, rt_radius);
1982                 havocbot_goalrating_ctf_enemybase(30000);
1983                 havocbot_goalrating_items(500, self.origin, rt_radius);
1984                 navigation_goalrating_end();
1985         }
1986 }
1987
1988 void havocbot_role_ctf_middle()
1989 {SELFPARAM();
1990         entity mf;
1991
1992         if(self.deadflag != DEAD_NO)
1993         {
1994                 havocbot_ctf_reset_role(self);
1995                 return;
1996         }
1997
1998         if (self.flagcarried)
1999         {
2000                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2001                 return;
2002         }
2003
2004         mf = havocbot_ctf_find_flag(self);
2005         if(mf.ctf_status!=FLAG_BASE)
2006         {
2007                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2008                 return;
2009         }
2010
2011         if (!self.havocbot_role_timeout)
2012                 self.havocbot_role_timeout = time + 10;
2013
2014         if (time > self.havocbot_role_timeout)
2015         {
2016                 havocbot_ctf_reset_role(self);
2017                 return;
2018         }
2019
2020         if (self.bot_strategytime < time)
2021         {
2022                 vector org;
2023
2024                 org = havocbot_ctf_middlepoint;
2025                 org.z = self.origin.z;
2026
2027                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2028                 navigation_goalrating_start();
2029                 havocbot_goalrating_ctf_ourstolenflag(50000);
2030                 havocbot_goalrating_ctf_droppedflags(30000, self.origin, 10000);
2031                 havocbot_goalrating_enemyplayers(10000, org, havocbot_ctf_middlepoint_radius * 0.5);
2032                 havocbot_goalrating_items(5000, org, havocbot_ctf_middlepoint_radius * 0.5);
2033                 havocbot_goalrating_items(2500, self.origin, 10000);
2034                 havocbot_goalrating_ctf_enemybase(2500);
2035                 navigation_goalrating_end();
2036         }
2037 }
2038
2039 void havocbot_role_ctf_defense()
2040 {SELFPARAM();
2041         entity mf;
2042
2043         if(self.deadflag != DEAD_NO)
2044         {
2045                 havocbot_ctf_reset_role(self);
2046                 return;
2047         }
2048
2049         if (self.flagcarried)
2050         {
2051                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_CARRIER);
2052                 return;
2053         }
2054
2055         // If own flag was captured
2056         mf = havocbot_ctf_find_flag(self);
2057         if(mf.ctf_status!=FLAG_BASE)
2058         {
2059                 havocbot_role_ctf_setrole(self, HAVOCBOT_CTF_ROLE_RETRIEVER);
2060                 return;
2061         }
2062
2063         if (!self.havocbot_role_timeout)
2064                 self.havocbot_role_timeout = time + 30;
2065
2066         if (time > self.havocbot_role_timeout)
2067         {
2068                 havocbot_ctf_reset_role(self);
2069                 return;
2070         }
2071         if (self.bot_strategytime < time)
2072         {
2073                 float mp_radius;
2074                 vector org;
2075
2076                 org = mf.dropped_origin;
2077                 mp_radius = havocbot_ctf_middlepoint_radius;
2078
2079                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
2080                 navigation_goalrating_start();
2081
2082                 // if enemies are closer to our base, go there
2083                 entity head, closestplayer = world;
2084                 float distance, bestdistance = 10000;
2085                 FOR_EACH_PLAYER(head)
2086                 {
2087                         if(head.deadflag!=DEAD_NO)
2088                                 continue;
2089
2090                         distance = vlen(org - head.origin);
2091                         if(distance<bestdistance)
2092                         {
2093                                 closestplayer = head;
2094                                 bestdistance = distance;
2095                         }
2096                 }
2097
2098                 if(closestplayer)
2099                 if(DIFF_TEAM(closestplayer, self))
2100                 if(vlen(org - self.origin)>1000)
2101                 if(checkpvs(self.origin,closestplayer)||random()<0.5)
2102                         havocbot_goalrating_ctf_ourbase(30000);
2103
2104                 havocbot_goalrating_ctf_ourstolenflag(20000);
2105                 havocbot_goalrating_ctf_droppedflags(20000, org, mp_radius);
2106                 havocbot_goalrating_enemyplayers(15000, org, mp_radius);
2107                 havocbot_goalrating_items(10000, org, mp_radius);
2108                 havocbot_goalrating_items(5000, self.origin, 10000);
2109                 navigation_goalrating_end();
2110         }
2111 }
2112
2113 void havocbot_role_ctf_setrole(entity bot, int role)
2114 {
2115         LOG_TRACE(strcat(bot.netname," switched to "));
2116         switch(role)
2117         {
2118                 case HAVOCBOT_CTF_ROLE_CARRIER:
2119                         LOG_TRACE("carrier");
2120                         bot.havocbot_role = havocbot_role_ctf_carrier;
2121                         bot.havocbot_role_timeout = 0;
2122                         bot.havocbot_cantfindflag = time + 10;
2123                         bot.bot_strategytime = 0;
2124                         break;
2125                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2126                         LOG_TRACE("defense");
2127                         bot.havocbot_role = havocbot_role_ctf_defense;
2128                         bot.havocbot_role_timeout = 0;
2129                         break;
2130                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2131                         LOG_TRACE("middle");
2132                         bot.havocbot_role = havocbot_role_ctf_middle;
2133                         bot.havocbot_role_timeout = 0;
2134                         break;
2135                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2136                         LOG_TRACE("offense");
2137                         bot.havocbot_role = havocbot_role_ctf_offense;
2138                         bot.havocbot_role_timeout = 0;
2139                         break;
2140                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2141                         LOG_TRACE("retriever");
2142                         bot.havocbot_previous_role = bot.havocbot_role;
2143                         bot.havocbot_role = havocbot_role_ctf_retriever;
2144                         bot.havocbot_role_timeout = time + 10;
2145                         bot.bot_strategytime = 0;
2146                         break;
2147                 case HAVOCBOT_CTF_ROLE_ESCORT:
2148                         LOG_TRACE("escort");
2149                         bot.havocbot_previous_role = bot.havocbot_role;
2150                         bot.havocbot_role = havocbot_role_ctf_escort;
2151                         bot.havocbot_role_timeout = time + 30;
2152                         bot.bot_strategytime = 0;
2153                         break;
2154         }
2155         LOG_TRACE("\n");
2156 }
2157
2158
2159 // ==============
2160 // Hook Functions
2161 // ==============
2162
2163 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2164 {SELFPARAM();
2165         entity flag;
2166         int t = 0, t2 = 0, t3 = 0;
2167
2168         // initially clear items so they can be set as necessary later.
2169         self.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING          | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2170                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2171                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2172                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2173                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2174                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED);
2175
2176         // scan through all the flags and notify the client about them
2177         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2178         {
2179                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2180                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2181                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2182                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2183                 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; }
2184
2185                 switch(flag.ctf_status)
2186                 {
2187                         case FLAG_PASSING:
2188                         case FLAG_CARRY:
2189                         {
2190                                 if((flag.owner == self) || (flag.pass_sender == self))
2191                                         self.ctf_flagstatus |= t; // carrying: self is currently carrying the flag
2192                                 else
2193                                         self.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2194                                 break;
2195                         }
2196                         case FLAG_DROPPED:
2197                         {
2198                                 self.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2199                                 break;
2200                         }
2201                 }
2202         }
2203
2204         // item for stopping players from capturing the flag too often
2205         if(self.ctf_captureshielded)
2206                 self.ctf_flagstatus |= CTF_SHIELDED;
2207
2208         // update the health of the flag carrier waypointsprite
2209         if(self.wps_flagcarrier)
2210                 WaypointSprite_UpdateHealth(self.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(self.health, self.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2211
2212         return false;
2213 }
2214
2215 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2216 {
2217         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2218         {
2219                 if(frag_target == frag_attacker) // damage done to yourself
2220                 {
2221                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2222                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2223                 }
2224                 else // damage done to everyone else
2225                 {
2226                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2227                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2228                 }
2229         }
2230         else if(frag_target.flagcarried && (frag_target.deadflag == DEAD_NO) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2231         {
2232                 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)))
2233                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2234                 {
2235                         frag_target.wps_helpme_time = time;
2236                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2237                 }
2238                 // todo: add notification for when flag carrier needs help?
2239         }
2240         return false;
2241 }
2242
2243 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2244 {
2245         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2246         {
2247                 PlayerTeamScore_AddScore(frag_attacker, autocvar_g_ctf_score_kill);
2248                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2249         }
2250
2251         if(frag_target.flagcarried)
2252         {
2253                 entity tmp_entity = frag_target.flagcarried;
2254                 ctf_Handle_Throw(frag_target, world, DROP_NORMAL);
2255                 tmp_entity.ctf_dropper = world;
2256         }
2257
2258         return false;
2259 }
2260
2261 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2262 {
2263         frag_score = 0;
2264         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2265 }
2266
2267 void ctf_RemovePlayer(entity player)
2268 {
2269         if(player.flagcarried)
2270                 { ctf_Handle_Throw(player, world, DROP_NORMAL); }
2271
2272         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2273         {
2274                 if(flag.pass_sender == player) { flag.pass_sender = world; }
2275                 if(flag.pass_target == player) { flag.pass_target = world; }
2276                 if(flag.ctf_dropper == player) { flag.ctf_dropper = world; }
2277         }
2278 }
2279
2280 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2281 {SELFPARAM();
2282         ctf_RemovePlayer(self);
2283         return false;
2284 }
2285
2286 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2287 {SELFPARAM();
2288         ctf_RemovePlayer(self);
2289         return false;
2290 }
2291
2292 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2293 {SELFPARAM();
2294         if(self.flagcarried)
2295         if(!autocvar_g_ctf_portalteleport)
2296                 { ctf_Handle_Throw(self, world, DROP_NORMAL); }
2297
2298         return false;
2299 }
2300
2301 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2302 {SELFPARAM();
2303         if(MUTATOR_RETURNVALUE || gameover) { return false; }
2304
2305         entity player = self;
2306
2307         if((time > player.throw_antispam) && (player.deadflag == DEAD_NO) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2308         {
2309                 // pass the flag to a team mate
2310                 if(autocvar_g_ctf_pass)
2311                 {
2312                         entity head, closest_target = world;
2313                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2314
2315                         while(head) // find the closest acceptable target to pass to
2316                         {
2317                                 if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
2318                                 if(head != player && SAME_TEAM(head, player))
2319                                 if(!head.speedrunning && !head.vehicle)
2320                                 {
2321                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2322                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2323                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2324
2325                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2326                                         {
2327                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2328                                                 {
2329                                                         if(IS_BOT_CLIENT(head))
2330                                                         {
2331                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2332                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2333                                                         }
2334                                                         else
2335                                                         {
2336                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2337                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2338                                                         }
2339                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2340                                                         return true;
2341                                                 }
2342                                                 else if(player.flagcarried)
2343                                                 {
2344                                                         if(closest_target)
2345                                                         {
2346                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2347                                                                 if(vlen(passer_center - head_center) < vlen(passer_center - closest_target_center))
2348                                                                         { closest_target = head; }
2349                                                         }
2350                                                         else { closest_target = head; }
2351                                                 }
2352                                         }
2353                                 }
2354                                 head = head.chain;
2355                         }
2356
2357                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2358                 }
2359
2360                 // throw the flag in front of you
2361                 if(autocvar_g_ctf_throw && player.flagcarried)
2362                 {
2363                         if(player.throw_count == -1)
2364                         {
2365                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2366                                 {
2367                                         player.throw_prevtime = time;
2368                                         player.throw_count = 1;
2369                                         ctf_Handle_Throw(player, world, DROP_THROW);
2370                                         return true;
2371                                 }
2372                                 else
2373                                 {
2374                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2375                                         return false;
2376                                 }
2377                         }
2378                         else
2379                         {
2380                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2381                                 else { player.throw_count += 1; }
2382                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2383
2384                                 player.throw_prevtime = time;
2385                                 ctf_Handle_Throw(player, world, DROP_THROW);
2386                                 return true;
2387                         }
2388                 }
2389         }
2390
2391         return false;
2392 }
2393
2394 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2395 {SELFPARAM();
2396         if(self.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2397         {
2398                 self.wps_helpme_time = time;
2399                 WaypointSprite_HelpMePing(self.wps_flagcarrier);
2400         }
2401         else // create a normal help me waypointsprite
2402         {
2403                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, self, FLAG_WAYPOINT_OFFSET, world, self.team, self, wps_helpme, false, RADARICON_HELPME);
2404                 WaypointSprite_Ping(self.wps_helpme);
2405         }
2406
2407         return true;
2408 }
2409
2410 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2411 {
2412         if(vh_player.flagcarried)
2413         {
2414                 vh_player.flagcarried.nodrawtoclient = vh_player; // hide the flag from the driver
2415
2416                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2417                 {
2418                         ctf_Handle_Throw(vh_player, world, DROP_NORMAL);
2419                 }
2420                 else
2421                 {
2422                         setattachment(vh_player.flagcarried, vh_vehicle, "");
2423                         setorigin(vh_player.flagcarried, VEHICLE_FLAG_OFFSET);
2424                         vh_player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2425                         //vh_player.flagcarried.angles = '0 0 0';
2426                 }
2427                 return true;
2428         }
2429
2430         return false;
2431 }
2432
2433 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2434 {
2435         if(vh_player.flagcarried)
2436         {
2437                 setattachment(vh_player.flagcarried, vh_player, "");
2438                 setorigin(vh_player.flagcarried, FLAG_CARRY_OFFSET);
2439                 vh_player.flagcarried.scale = FLAG_SCALE;
2440                 vh_player.flagcarried.angles = '0 0 0';
2441                 vh_player.flagcarried.nodrawtoclient = world;
2442                 return true;
2443         }
2444
2445         return false;
2446 }
2447
2448 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2449 {SELFPARAM();
2450         if(self.flagcarried)
2451         {
2452                 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));
2453                 ctf_RespawnFlag(self.flagcarried);
2454                 return true;
2455         }
2456
2457         return false;
2458 }
2459
2460 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2461 {
2462         entity flag; // temporary entity for the search method
2463
2464         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2465         {
2466                 switch(flag.ctf_status)
2467                 {
2468                         case FLAG_DROPPED:
2469                         case FLAG_PASSING:
2470                         {
2471                                 // lock the flag, game is over
2472                                 flag.movetype = MOVETYPE_NONE;
2473                                 flag.takedamage = DAMAGE_NO;
2474                                 flag.solid = SOLID_NOT;
2475                                 flag.nextthink = false; // stop thinking
2476
2477                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2478                                 break;
2479                         }
2480
2481                         default:
2482                         case FLAG_BASE:
2483                         case FLAG_CARRY:
2484                         {
2485                                 // do nothing for these flags
2486                                 break;
2487                         }
2488                 }
2489         }
2490
2491         return false;
2492 }
2493
2494 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2495 {SELFPARAM();
2496         havocbot_ctf_reset_role(self);
2497         return true;
2498 }
2499
2500 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2501 {
2502         //ret_float = ctf_teams;
2503         ret_string = "ctf_team";
2504         return true;
2505 }
2506
2507 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2508 {SELFPARAM();
2509         self.ctf_flagstatus = other.ctf_flagstatus;
2510         return false;
2511 }
2512
2513 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2514 {
2515         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2516         {
2517                 if (MapInfo_Get_ByID(i))
2518                 {
2519                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2520
2521                         if(!r)
2522                                 continue;
2523
2524                         // TODO: uid2name
2525                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2526                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2527                 }
2528         }
2529
2530         return false;
2531 }
2532
2533 bool superspec_Spectate(entity _player); // TODO
2534 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2535 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2536 {
2537         if(IS_PLAYER(self) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2538
2539         if(cmd_name == "followfc")
2540         {
2541                 if(!g_ctf)
2542                         return true;
2543
2544                 entity _player;
2545                 int _team = 0;
2546                 bool found = false;
2547
2548                 if(cmd_argc == 2)
2549                 {
2550                         switch(argv(1))
2551                         {
2552                                 case "red": _team = NUM_TEAM_1; break;
2553                                 case "blue": _team = NUM_TEAM_2; break;
2554                                 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2555                                 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2556                         }
2557                 }
2558
2559                 FOR_EACH_PLAYER(_player)
2560                 {
2561                         if(_player.flagcarried && (_player.team == _team || _team == 0))
2562                         {
2563                                 found = true;
2564                                 if(_team == 0 && IS_SPEC(self) && self.enemy == _player)
2565                                         continue; // already spectating a fc, try to find the other fc
2566                                 return superspec_Spectate(_player);
2567                         }
2568                 }
2569
2570                 if(!found)
2571                         superspec_msg("", "", self, "No active flag carrier\n", 1);
2572                 return true;
2573         }
2574
2575         return false;
2576 }
2577
2578 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2579 {
2580         if(frag_target.flagcarried)
2581                 ctf_Handle_Throw(frag_target, world, DROP_THROW);
2582
2583         return false;
2584 }
2585
2586
2587 // ==========
2588 // Spawnfuncs
2589 // ==========
2590
2591 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2592 CTF flag for team one (Red).
2593 Keys:
2594 "angle" Angle the flag will point (minus 90 degrees)...
2595 "model" model to use, note this needs red and blue as skins 0 and 1...
2596 "noise" sound played when flag is picked up...
2597 "noise1" sound played when flag is returned by a teammate...
2598 "noise2" sound played when flag is captured...
2599 "noise3" sound played when flag is lost in the field and respawns itself...
2600 "noise4" sound played when flag is dropped by a player...
2601 "noise5" sound played when flag touches the ground... */
2602 spawnfunc(item_flag_team1)
2603 {
2604         if(!g_ctf) { remove(self); return; }
2605
2606         ctf_FlagSetup(NUM_TEAM_1, self);
2607 }
2608
2609 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2610 CTF flag for team two (Blue).
2611 Keys:
2612 "angle" Angle the flag will point (minus 90 degrees)...
2613 "model" model to use, note this needs red and blue as skins 0 and 1...
2614 "noise" sound played when flag is picked up...
2615 "noise1" sound played when flag is returned by a teammate...
2616 "noise2" sound played when flag is captured...
2617 "noise3" sound played when flag is lost in the field and respawns itself...
2618 "noise4" sound played when flag is dropped by a player...
2619 "noise5" sound played when flag touches the ground... */
2620 spawnfunc(item_flag_team2)
2621 {
2622         if(!g_ctf) { remove(self); return; }
2623
2624         ctf_FlagSetup(NUM_TEAM_2, self);
2625 }
2626
2627 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2628 CTF flag for team three (Yellow).
2629 Keys:
2630 "angle" Angle the flag will point (minus 90 degrees)...
2631 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2632 "noise" sound played when flag is picked up...
2633 "noise1" sound played when flag is returned by a teammate...
2634 "noise2" sound played when flag is captured...
2635 "noise3" sound played when flag is lost in the field and respawns itself...
2636 "noise4" sound played when flag is dropped by a player...
2637 "noise5" sound played when flag touches the ground... */
2638 spawnfunc(item_flag_team3)
2639 {
2640         if(!g_ctf) { remove(self); return; }
2641
2642         ctf_FlagSetup(NUM_TEAM_3, self);
2643 }
2644
2645 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2646 CTF flag for team four (Pink).
2647 Keys:
2648 "angle" Angle the flag will point (minus 90 degrees)...
2649 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2650 "noise" sound played when flag is picked up...
2651 "noise1" sound played when flag is returned by a teammate...
2652 "noise2" sound played when flag is captured...
2653 "noise3" sound played when flag is lost in the field and respawns itself...
2654 "noise4" sound played when flag is dropped by a player...
2655 "noise5" sound played when flag touches the ground... */
2656 spawnfunc(item_flag_team4)
2657 {
2658         if(!g_ctf) { remove(self); return; }
2659
2660         ctf_FlagSetup(NUM_TEAM_4, self);
2661 }
2662
2663 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2664 CTF flag (Neutral).
2665 Keys:
2666 "angle" Angle the flag will point (minus 90 degrees)...
2667 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2668 "noise" sound played when flag is picked up...
2669 "noise1" sound played when flag is returned by a teammate...
2670 "noise2" sound played when flag is captured...
2671 "noise3" sound played when flag is lost in the field and respawns itself...
2672 "noise4" sound played when flag is dropped by a player...
2673 "noise5" sound played when flag touches the ground... */
2674 spawnfunc(item_flag_neutral)
2675 {
2676         if(!g_ctf) { remove(self); return; }
2677         if(!cvar("g_ctf_oneflag")) { remove(self); return; }
2678
2679         ctf_FlagSetup(0, self);
2680 }
2681
2682 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2683 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2684 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.
2685 Keys:
2686 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2687 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2688 spawnfunc(ctf_team)
2689 {
2690         if(!g_ctf) { remove(self); return; }
2691
2692         self.classname = "ctf_team";
2693         self.team = self.cnt + 1;
2694 }
2695
2696 // compatibility for quake maps
2697 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2698 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2699 spawnfunc(info_player_team1);
2700 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2701 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2702 spawnfunc(info_player_team2);
2703 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2704 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2705
2706 void team_CTF_neutralflag()                      { SELFPARAM(); spawnfunc_item_flag_neutral(self);  }
2707 void team_neutralobelisk()                       { SELFPARAM(); spawnfunc_item_flag_neutral(self);  }
2708
2709
2710 // ==============
2711 // Initialization
2712 // ==============
2713
2714 // scoreboard setup
2715 void ctf_ScoreRules(int teams)
2716 {
2717         CheckAllowedTeams(world);
2718         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2719         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2720         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2721         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2722         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2723         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2724         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2725         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2726         ScoreRules_basics_end();
2727 }
2728
2729 // code from here on is just to support maps that don't have flag and team entities
2730 void ctf_SpawnTeam (string teamname, int teamcolor)
2731 {
2732         entity this = new(ctf_team);
2733         this.netname = teamname;
2734         this.cnt = teamcolor;
2735         this.spawnfunc_checked = true;
2736         WITH(entity, self, this, spawnfunc_ctf_team(this));
2737 }
2738
2739 void ctf_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
2740 {
2741         ctf_teams = 2;
2742
2743         entity tmp_entity;
2744         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2745         {
2746                 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2747                 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2748                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2749         }
2750
2751         ctf_teams = bound(2, ctf_teams, 4);
2752
2753         // if no teams are found, spawn defaults
2754         if(find(world, classname, "ctf_team") == world)
2755         {
2756                 LOG_INFO("No ""ctf_team"" entities found on this map, creating them anyway.\n");
2757                 ctf_SpawnTeam("Red", NUM_TEAM_1 - 1);
2758                 ctf_SpawnTeam("Blue", NUM_TEAM_2 - 1);
2759                 if(ctf_teams >= 3)
2760                         ctf_SpawnTeam("Yellow", NUM_TEAM_3 - 1);
2761                 if(ctf_teams >= 4)
2762                         ctf_SpawnTeam("Pink", NUM_TEAM_4 - 1);
2763         }
2764
2765         ctf_ScoreRules(ctf_teams);
2766 }
2767
2768 void ctf_Initialize()
2769 {
2770         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2771
2772         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2773         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2774         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2775
2776         addstat(STAT_CTF_FLAGSTATUS, AS_INT, ctf_flagstatus);
2777
2778         InitializeEntity(world, ctf_DelayedInit, INITPRIO_GAMETYPE);
2779 }
2780
2781 #endif