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