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