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