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