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