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