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