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