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