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