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