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