]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Merge branch 'master' into sev/hud_ctf_update
[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, autocvar_timelimit_override, -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 != NULL) ? ftos(actor.playerid) : "")));
129                 //GameLogEcho(strcat(":ctf:", mode, ":", ftos(flagteam), ((actor != NULL) ? (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, NULL, MSG_INFO, INFO_CTF_CAPTURE_NEUTRAL, player.netname); }
140         else if(!ctf_captimerecord) { Send_Notification(NOTIF_ALL, NULL, 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, NULL, 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, NULL, 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, NULL, 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(entity this, entity client)
282 {
283         if(!client.ctf_captureshielded) { return false; }
284         if(CTF_SAMETEAM(this, client)) { return false; }
285
286         return true;
287 }
288
289 void ctf_CaptureShield_Touch(entity this, entity toucher)
290 {
291         if(!toucher.ctf_captureshielded) { return; }
292         if(CTF_SAMETEAM(this, toucher)) { return; }
293
294         vector mymid = (this.absmin + this.absmax) * 0.5;
295         vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
296
297         Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ctf_captureshield_force);
298         if(IS_REAL_CLIENT(toucher)) { Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_CTF_CAPTURESHIELD_SHIELDED); }
299 }
300
301 void ctf_CaptureShield_Spawn(entity flag)
302 {
303         entity shield = new(ctf_captureshield);
304
305         shield.enemy = flag;
306         shield.team = flag.team;
307         settouch(shield, ctf_CaptureShield_Touch);
308         setcefc(shield, 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, flag.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, NULL, 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, NULL, ((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 = NULL;
366                 flag.pass_target = NULL;
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 = NULL;
417         flag.pass_target = NULL;
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, NULL, "");
432         setorigin(flag, player.origin + FLAG_DROP_OFFSET);
433         flag.owner.flagcarried = NULL;
434         flag.owner = NULL;
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(NULL, _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, 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, 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 = NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL, 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, NULL);
745                         ctf_RespawnFlag(flag);
746                 }
747         }
748 }
749
750 bool ctf_Stalemate_Customize(entity this, entity client)
751 {
752         // make spectators see what the player would see
753         entity e, wp_owner;
754         e = WaypointSprite_getviewentity(client);
755         wp_owner = this.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 = NULL; // 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, NULL, 0, tmp_entity.owner, wps_enemyflagcarrier, true, RADARICON_FLAG);
816                                 wp.colormod = WPCOLOR_ENEMYFC(tmp_entity.owner.team);
817                                 setcefc(tmp_entity.owner.wps_enemyflagcarrier, 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(entity this)
855 {
856         // declarations
857         entity tmp_entity;
858
859         this.nextthink = time + FLAG_THINKRATE; // only 5 fps, more is unnecessary.
860
861         // captureshield
862         if(this == 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(this.mins != CTF_FLAG.m_mins || this.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(this.origin, CTF_FLAG.m_mins, CTF_FLAG.m_maxs, this.origin, MOVE_NOMONSTERS, this);
869                 if(!trace_startsolid || this.noalign) // can we resize it without getting stuck?
870                         setsize(this, CTF_FLAG.m_mins, CTF_FLAG.m_maxs); }
871
872         switch(this.ctf_status) // reset flag angles in case warpzones adjust it
873         {
874                 case FLAG_DROPPED:
875                 {
876                         this.angles = '0 0 0';
877                         break;
878                 }
879
880                 default: break;
881         }
882
883         // main think method
884         switch(this.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(vdist(this.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(this, 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 = ((this.absmin + this.absmax) * 0.5);
904                                 if(pointcontents(midpoint) == CONTENT_WATER)
905                                 {
906                                         this.velocity = this.velocity * 0.5;
907
908                                         if(pointcontents(midpoint + FLAG_FLOAT_OFFSET) == CONTENT_WATER)
909                                                 { this.velocity_z = autocvar_g_ctf_flag_dropped_floatinwater; }
910                                         else
911                                                 { this.movetype = MOVETYPE_FLY; }
912                                 }
913                                 else if(this.movetype == MOVETYPE_FLY) { this.movetype = MOVETYPE_TOSS; }
914                         }
915                         if(autocvar_g_ctf_flag_return_dropped)
916                         {
917                                 if((vdist(this.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_dropped)) || (autocvar_g_ctf_flag_return_dropped == -1))
918                                 {
919                                         this.health = 0;
920                                         ctf_CheckFlagReturn(this, RETURN_DROPPED);
921                                         return;
922                                 }
923                         }
924                         if(this.ctf_flagdamaged)
925                         {
926                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_damage_delay) * FLAG_THINKRATE);
927                                 ctf_CheckFlagReturn(this, RETURN_NEEDKILL);
928                                 return;
929                         }
930                         else if(autocvar_g_ctf_flag_return_time)
931                         {
932                                 this.health -= ((this.max_flag_health / autocvar_g_ctf_flag_return_time) * FLAG_THINKRATE);
933                                 ctf_CheckFlagReturn(this, RETURN_TIMEOUT);
934                                 return;
935                         }
936                         return;
937                 }
938
939                 case FLAG_CARRY:
940                 {
941                         if(this.speedrunning && ctf_captimerecord && (time >= this.ctf_pickuptime + ctf_captimerecord))
942                         {
943                                 this.health = 0;
944                                 ctf_CheckFlagReturn(this, RETURN_SPEEDRUN);
945
946                                 this.owner.impulse = CHIMPULSE_SPEEDRUN.impulse; // move the player back to the waypoint they set
947                                 ImpulseCommands(this.owner);
948                         }
949                         if(autocvar_g_ctf_stalemate)
950                         {
951                                 if(time >= wpforenemy_nextthink)
952                                 {
953                                         ctf_CheckStalemate();
954                                         wpforenemy_nextthink = time + WPFE_THINKRATE; // waypoint for enemy think rate (to reduce unnecessary spam of this check)
955                                 }
956                         }
957                         if(CTF_SAMETEAM(this, this.owner) && this.team)
958                         {
959                                 if(autocvar_g_ctf_flag_return) // drop the flag if reverse status has changed
960                                         ctf_Handle_Throw(this.owner, NULL, DROP_THROW);
961                                 else if(vdist(this.owner.origin - this.ctf_spawnorigin, <=, autocvar_g_ctf_flag_return_carried_radius))
962                                         ctf_Handle_Return(this, this.owner);
963                         }
964                         return;
965                 }
966
967                 case FLAG_PASSING:
968                 {
969                         vector targ_origin = ((this.pass_target.absmin + this.pass_target.absmax) * 0.5);
970                         targ_origin = WarpZone_RefSys_TransformOrigin(this.pass_target, this, targ_origin); // origin of target as seen by the flag (us)
971                         WarpZone_TraceLine(this.origin, targ_origin, MOVE_NOMONSTERS, this);
972
973                         if((this.pass_target == NULL)
974                                 || (IS_DEAD(this.pass_target))
975                                 || (this.pass_target.flagcarried)
976                                 || (vdist(this.origin - targ_origin, <, autocvar_g_ctf_pass_radius))
977                                 || ((trace_fraction < 1) && (trace_ent != this.pass_target))
978                                 || (time > this.ctf_droptime + autocvar_g_ctf_pass_timelimit))
979                         {
980                                 // give up, pass failed
981                                 ctf_Handle_Drop(this, NULL, DROP_PASS);
982                         }
983                         else
984                         {
985                                 // still a viable target, go for it
986                                 ctf_CalculatePassVelocity(this, targ_origin, this.origin, true);
987                         }
988                         return;
989                 }
990
991                 default: // this should never happen
992                 {
993                         LOG_TRACE("ctf_FlagThink(): Flag exists with no status?\n");
994                         return;
995                 }
996         }
997 }
998
999 METHOD(Flag, giveTo, bool(Flag this, entity flag, entity toucher))
1000 {
1001         return = false;
1002         if(gameover) { return; }
1003         if(trace_dphitcontents & (DPCONTENTS_PLAYERCLIP | DPCONTENTS_MONSTERCLIP)) { return; }
1004
1005         bool is_not_monster = (!IS_MONSTER(toucher));
1006
1007         // automatically kill the flag and return it if it touched lava/slime/nodrop surfaces
1008         if(ITEM_TOUCH_NEEDKILL())
1009         {
1010                 if(!autocvar_g_ctf_flag_return_damage_delay)
1011                 {
1012                         flag.health = 0;
1013                         ctf_CheckFlagReturn(flag, RETURN_NEEDKILL);
1014                 }
1015                 if(!flag.ctf_flagdamaged) { return; }
1016         }
1017
1018         int num_perteam = 0;
1019         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(toucher, it), LAMBDA(++num_perteam));
1020
1021         // special touch behaviors
1022         if(STAT(FROZEN, toucher)) { return; }
1023         else if(IS_VEHICLE(toucher))
1024         {
1025                 if(autocvar_g_ctf_allow_vehicle_touch && toucher.owner)
1026                         toucher = toucher.owner; // the player is actually the vehicle owner, not other
1027                 else
1028                         return; // do nothing
1029         }
1030         else if(IS_MONSTER(toucher))
1031         {
1032                 if(!autocvar_g_ctf_allow_monster_touch)
1033                         return; // do nothing
1034         }
1035         else if (!IS_PLAYER(toucher)) // The flag just touched an object, most likely the world
1036         {
1037                 if(time > flag.wait) // if we haven't in a while, play a sound/effect
1038                 {
1039                         Send_Effect_(flag.toucheffect, flag.origin, '0 0 0', 1);
1040                         _sound(flag, CH_TRIGGER, flag.snd_flag_touch, VOL_BASE, ATTEN_NORM);
1041                         flag.wait = time + FLAG_TOUCHRATE;
1042                 }
1043                 return;
1044         }
1045         else if(IS_DEAD(toucher)) { return; }
1046
1047         switch(flag.ctf_status)
1048         {
1049                 case FLAG_BASE:
1050                 {
1051                         if(ctf_oneflag)
1052                         {
1053                                 if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && !toucher.flagcarried.team && is_not_monster)
1054                                         ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the neutral flag to enemy base
1055                                 else if(!flag.team && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1056                                         ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the neutral flag
1057                         }
1058                         else if(CTF_SAMETEAM(toucher, flag) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, flag) && is_not_monster)
1059                                 ctf_Handle_Capture(flag, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base
1060                         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)
1061                         {
1062                                 ctf_Handle_Return(toucher.flagcarried, toucher); // return their current flag
1063                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // now pickup the flag
1064                         }
1065                         else if(CTF_DIFFTEAM(toucher, flag) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && is_not_monster)
1066                                 ctf_Handle_Pickup(flag, toucher, PICKUP_BASE); // toucher just stole the enemies flag
1067                         break;
1068                 }
1069
1070                 case FLAG_DROPPED:
1071                 {
1072                         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
1073                                 ctf_Handle_Return(flag, toucher); // toucher just returned his own flag
1074                         else if(is_not_monster && (!toucher.flagcarried) && ((toucher != flag.ctf_dropper) || (time > flag.ctf_droptime + autocvar_g_ctf_flag_collect_delay)))
1075                                 ctf_Handle_Pickup(flag, toucher, PICKUP_DROPPED); // toucher just picked up a dropped enemy flag
1076                         break;
1077                 }
1078
1079                 case FLAG_CARRY:
1080                 {
1081                         LOG_TRACE("Someone touched a flag even though it was being carried?\n");
1082                         break;
1083                 }
1084
1085                 case FLAG_PASSING:
1086                 {
1087                         if((IS_PLAYER(toucher)) && !IS_DEAD(toucher) && (toucher != flag.pass_sender))
1088                         {
1089                                 if(DIFF_TEAM(toucher, flag.pass_sender))
1090                                         ctf_Handle_Return(flag, toucher);
1091                                 else
1092                                         ctf_Handle_Retrieve(flag, toucher);
1093                         }
1094                         break;
1095                 }
1096         }
1097 }
1098
1099 .float last_respawn;
1100 void ctf_RespawnFlag(entity flag)
1101 {
1102         // check for flag respawn being called twice in a row
1103         if(flag.last_respawn > time - 0.5)
1104                 { backtrace("flag respawn called twice quickly! please notify Samual about this..."); }
1105
1106         flag.last_respawn = time;
1107
1108         // reset the player (if there is one)
1109         if((flag.owner) && (flag.owner.flagcarried == flag))
1110         {
1111                 WaypointSprite_Kill(flag.owner.wps_enemyflagcarrier);
1112                 WaypointSprite_Kill(flag.wps_flagcarrier);
1113
1114                 flag.owner.flagcarried = NULL;
1115
1116                 if(flag.speedrunning)
1117                         ctf_FakeTimeLimit(flag.owner, -1);
1118         }
1119
1120         if((flag.owner) && (flag.owner.vehicle))
1121                 flag.scale = FLAG_SCALE;
1122
1123         if(flag.ctf_status == FLAG_DROPPED)
1124                 { WaypointSprite_Kill(flag.wps_flagdropped); }
1125
1126         // reset the flag
1127         setattachment(flag, NULL, "");
1128         setorigin(flag, flag.ctf_spawnorigin);
1129
1130         flag.movetype = ((flag.noalign) ? MOVETYPE_NONE : MOVETYPE_TOSS);
1131         flag.takedamage = DAMAGE_NO;
1132         flag.health = flag.max_flag_health;
1133         flag.solid = SOLID_TRIGGER;
1134         flag.velocity = '0 0 0';
1135         flag.angles = flag.mangle;
1136         flag.flags = FL_ITEM | FL_NOTARGET;
1137
1138         flag.ctf_status = FLAG_BASE;
1139         flag.owner = NULL;
1140         flag.pass_distance = 0;
1141         flag.pass_sender = NULL;
1142         flag.pass_target = NULL;
1143         flag.ctf_dropper = NULL;
1144         flag.ctf_pickuptime = 0;
1145         flag.ctf_droptime = 0;
1146         flag.ctf_flagdamaged = 0;
1147
1148         ctf_CheckStalemate();
1149 }
1150
1151 void ctf_Reset(entity this)
1152 {
1153         if(this.owner && IS_PLAYER(this.owner))
1154         ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1155
1156         ctf_RespawnFlag(this);
1157 }
1158
1159 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1160 {
1161         // bot waypoints
1162         waypoint_spawnforitem_force(this, this.origin);
1163         this.nearestwaypointtimeout = 0; // activate waypointing again
1164         this.bot_basewaypoint = this.nearestwaypoint;
1165
1166         // waypointsprites
1167         entity basename;
1168         switch (this.team)
1169         {
1170                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1171                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1172                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1173                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1174                 default: basename = WP_FlagBaseNeutral; break;
1175         }
1176
1177         entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1178         wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1179         WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1180
1181         // captureshield setup
1182         ctf_CaptureShield_Spawn(this);
1183 }
1184
1185 .bool pushable;
1186
1187 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1188 {
1189         // main setup
1190         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1191         ctf_worldflaglist = flag;
1192
1193         setattachment(flag, NULL, "");
1194
1195         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1196         flag.team = teamnumber;
1197         flag.classname = "item_flag_team";
1198         flag.target = "###item###"; // wut?
1199         flag.flags = FL_ITEM | FL_NOTARGET;
1200         flag.solid = SOLID_TRIGGER;
1201         flag.takedamage = DAMAGE_NO;
1202         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1203         flag.max_flag_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1204         flag.health = flag.max_flag_health;
1205         flag.event_damage = ctf_FlagDamage;
1206         flag.pushable = true;
1207         flag.teleportable = TELEPORT_NORMAL;
1208         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1209         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1210         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1211         flag.velocity = '0 0 0';
1212         flag.mangle = flag.angles;
1213         flag.reset = ctf_Reset;
1214         settouch(flag, ctf_FlagTouch);
1215         setthink(flag, ctf_FlagThink);
1216         flag.nextthink = time + FLAG_THINKRATE;
1217         flag.ctf_status = FLAG_BASE;
1218
1219         string teamname = Static_Team_ColorName_Lower(teamnumber);
1220         // appearence
1221         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1222         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1223         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1224         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1225         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1226         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1227
1228         // sounds
1229 #define X(s,b) \
1230                 if(flag.s == "") flag.s = b; \
1231                 precache_sound(flag.s);
1232
1233         X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnumber))))
1234         X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnumber))))
1235         X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnumber))))
1236         X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnumber))))
1237         X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
1238         X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
1239         X(snd_flag_pass,                strzone(SND(CTF_PASS)))
1240 #undef X
1241
1242         // precache
1243         precache_model(flag.model);
1244
1245         // appearence
1246         _setmodel(flag, flag.model); // precision set below
1247         setsize(flag, CTF_FLAG.m_mins, CTF_FLAG.m_maxs);
1248         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1249
1250         if(autocvar_g_ctf_flag_glowtrails)
1251         {
1252                 switch(teamnumber)
1253                 {
1254                         case NUM_TEAM_1: flag.glow_color = 251; break;
1255                         case NUM_TEAM_2: flag.glow_color = 210; break;
1256                         case NUM_TEAM_3: flag.glow_color = 110; break;
1257                         case NUM_TEAM_4: flag.glow_color = 145; break;
1258                         default:                 flag.glow_color = 254; break;
1259                 }
1260                 flag.glow_size = 25;
1261                 flag.glow_trail = 1;
1262         }
1263
1264         flag.effects |= EF_LOWPRECISION;
1265         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1266         if(autocvar_g_ctf_dynamiclights)
1267         {
1268                 switch(teamnumber)
1269                 {
1270                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1271                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1272                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1273                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1274                         default:                 flag.effects |= EF_DIMLIGHT; break;
1275                 }
1276         }
1277
1278         // flag placement
1279         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1280         {
1281                 flag.dropped_origin = flag.origin;
1282                 flag.noalign = true;
1283                 flag.movetype = MOVETYPE_NONE;
1284         }
1285         else // drop to floor, automatically find a platform and set that as spawn origin
1286         {
1287                 flag.noalign = false;
1288                 droptofloor(flag);
1289                 flag.movetype = MOVETYPE_TOSS;
1290         }
1291
1292         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1293 }
1294
1295
1296 // ================
1297 // Bot player logic
1298 // ================
1299
1300 // NOTE: LEGACY CODE, needs to be re-written!
1301
1302 void havocbot_calculate_middlepoint()
1303 {
1304         entity f;
1305         vector s = '0 0 0';
1306         vector fo = '0 0 0';
1307         float n = 0;
1308
1309         f = ctf_worldflaglist;
1310         while (f)
1311         {
1312                 fo = f.origin;
1313                 s = s + fo;
1314                 f = f.ctf_worldflagnext;
1315         }
1316         if(!n)
1317                 return;
1318         havocbot_ctf_middlepoint = s * (1.0 / n);
1319         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1320 }
1321
1322
1323 entity havocbot_ctf_find_flag(entity bot)
1324 {
1325         entity f;
1326         f = ctf_worldflaglist;
1327         while (f)
1328         {
1329                 if (CTF_SAMETEAM(bot, f))
1330                         return f;
1331                 f = f.ctf_worldflagnext;
1332         }
1333         return NULL;
1334 }
1335
1336 entity havocbot_ctf_find_enemy_flag(entity bot)
1337 {
1338         entity f;
1339         f = ctf_worldflaglist;
1340         while (f)
1341         {
1342                 if(ctf_oneflag)
1343                 {
1344                         if(CTF_DIFFTEAM(bot, f))
1345                         {
1346                                 if(f.team)
1347                                 {
1348                                         if(bot.flagcarried)
1349                                                 return f;
1350                                 }
1351                                 else if(!bot.flagcarried)
1352                                         return f;
1353                         }
1354                 }
1355                 else if (CTF_DIFFTEAM(bot, f))
1356                         return f;
1357                 f = f.ctf_worldflagnext;
1358         }
1359         return NULL;
1360 }
1361
1362 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1363 {
1364         if (!teamplay)
1365                 return 0;
1366
1367         int c = 0;
1368
1369         FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1370                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1371                         continue;
1372
1373                 if(vdist(it.origin - org, <, tc_radius))
1374                         ++c;
1375         ));
1376
1377         return c;
1378 }
1379
1380 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1381 {
1382         entity head;
1383         head = ctf_worldflaglist;
1384         while (head)
1385         {
1386                 if (CTF_SAMETEAM(this, head))
1387                         break;
1388                 head = head.ctf_worldflagnext;
1389         }
1390         if (head)
1391                 navigation_routerating(this, head, ratingscale, 10000);
1392 }
1393
1394 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1395 {
1396         entity head;
1397         head = ctf_worldflaglist;
1398         while (head)
1399         {
1400                 if (CTF_SAMETEAM(this, head))
1401                         break;
1402                 head = head.ctf_worldflagnext;
1403         }
1404         if (!head)
1405                 return;
1406
1407         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1408 }
1409
1410 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1411 {
1412         entity head;
1413         head = ctf_worldflaglist;
1414         while (head)
1415         {
1416                 if(ctf_oneflag)
1417                 {
1418                         if(CTF_DIFFTEAM(this, head))
1419                         {
1420                                 if(head.team)
1421                                 {
1422                                         if(this.flagcarried)
1423                                                 break;
1424                                 }
1425                                 else if(!this.flagcarried)
1426                                         break;
1427                         }
1428                 }
1429                 else if(CTF_DIFFTEAM(this, head))
1430                         break;
1431                 head = head.ctf_worldflagnext;
1432         }
1433         if (head)
1434                 navigation_routerating(this, head, ratingscale, 10000);
1435 }
1436
1437 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1438 {
1439         if (!bot_waypoints_for_items)
1440         {
1441                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1442                 return;
1443         }
1444
1445         entity head;
1446
1447         head = havocbot_ctf_find_enemy_flag(this);
1448
1449         if (!head)
1450                 return;
1451
1452         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1453 }
1454
1455 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1456 {
1457         entity mf;
1458
1459         mf = havocbot_ctf_find_flag(this);
1460
1461         if(mf.ctf_status == FLAG_BASE)
1462                 return;
1463
1464         if(mf.tag_entity)
1465                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1466 }
1467
1468 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1469 {
1470         entity head;
1471         head = ctf_worldflaglist;
1472         while (head)
1473         {
1474                 // flag is out in the field
1475                 if(head.ctf_status != FLAG_BASE)
1476                 if(head.tag_entity==NULL)       // dropped
1477                 {
1478                         if(df_radius)
1479                         {
1480                                 if(vdist(org - head.origin, <, df_radius))
1481                                         navigation_routerating(this, head, ratingscale, 10000);
1482                         }
1483                         else
1484                                 navigation_routerating(this, head, ratingscale, 10000);
1485                 }
1486
1487                 head = head.ctf_worldflagnext;
1488         }
1489 }
1490
1491 void havocbot_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
1492 {
1493         entity head;
1494         float t;
1495         head = findchainfloat(bot_pickup, true);
1496         while (head)
1497         {
1498                 // gather health and armor only
1499                 if (head.solid)
1500                 if (head.health || head.armorvalue)
1501                 if (vdist(head.origin - org, <, sradius))
1502                 {
1503                         // get the value of the item
1504                         t = head.bot_pickupevalfunc(this, head) * 0.0001;
1505                         if (t > 0)
1506                                 navigation_routerating(this, head, t * ratingscale, 500);
1507                 }
1508                 head = head.chain;
1509         }
1510 }
1511
1512 void havocbot_ctf_reset_role(entity this)
1513 {
1514         float cdefense, cmiddle, coffense;
1515         entity mf, ef;
1516         float c;
1517
1518         if(IS_DEAD(this))
1519                 return;
1520
1521         if(havocbot_ctf_middlepoint == '0 0 0')
1522                 havocbot_calculate_middlepoint();
1523
1524         // Check ctf flags
1525         if (this.flagcarried)
1526         {
1527                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1528                 return;
1529         }
1530
1531         mf = havocbot_ctf_find_flag(this);
1532         ef = havocbot_ctf_find_enemy_flag(this);
1533
1534         // Retrieve stolen flag
1535         if(mf.ctf_status!=FLAG_BASE)
1536         {
1537                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1538                 return;
1539         }
1540
1541         // If enemy flag is taken go to the middle to intercept pursuers
1542         if(ef.ctf_status!=FLAG_BASE)
1543         {
1544                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1545                 return;
1546         }
1547
1548         // if there is only me on the team switch to offense
1549         c = 0;
1550         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), LAMBDA(++c));
1551
1552         if(c==1)
1553         {
1554                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1555                 return;
1556         }
1557
1558         // Evaluate best position to take
1559         // Count mates on middle position
1560         cmiddle = havocbot_ctf_teamcount(this, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1561
1562         // Count mates on defense position
1563         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1564
1565         // Count mates on offense position
1566         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1567
1568         if(cdefense<=coffense)
1569                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1570         else if(coffense<=cmiddle)
1571                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1572         else
1573                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1574 }
1575
1576 void havocbot_role_ctf_carrier(entity this)
1577 {
1578         if(IS_DEAD(this))
1579         {
1580                 havocbot_ctf_reset_role(this);
1581                 return;
1582         }
1583
1584         if (this.flagcarried == NULL)
1585         {
1586                 havocbot_ctf_reset_role(this);
1587                 return;
1588         }
1589
1590         if (this.bot_strategytime < time)
1591         {
1592                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1593
1594                 navigation_goalrating_start(this);
1595                 if(ctf_oneflag)
1596                         havocbot_goalrating_ctf_enemybase(this, 50000);
1597                 else
1598                         havocbot_goalrating_ctf_ourbase(this, 50000);
1599
1600                 if(this.health<100)
1601                         havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1602
1603                 navigation_goalrating_end(this);
1604
1605                 if (this.navigation_hasgoals)
1606                         this.havocbot_cantfindflag = time + 10;
1607                 else if (time > this.havocbot_cantfindflag)
1608                 {
1609                         // Can't navigate to my own base, suicide!
1610                         // TODO: drop it and wander around
1611                         Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
1612                         return;
1613                 }
1614         }
1615 }
1616
1617 void havocbot_role_ctf_escort(entity this)
1618 {
1619         entity mf, ef;
1620
1621         if(IS_DEAD(this))
1622         {
1623                 havocbot_ctf_reset_role(this);
1624                 return;
1625         }
1626
1627         if (this.flagcarried)
1628         {
1629                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1630                 return;
1631         }
1632
1633         // If enemy flag is back on the base switch to previous role
1634         ef = havocbot_ctf_find_enemy_flag(this);
1635         if(ef.ctf_status==FLAG_BASE)
1636         {
1637                 this.havocbot_role = this.havocbot_previous_role;
1638                 this.havocbot_role_timeout = 0;
1639                 return;
1640         }
1641
1642         // If the flag carrier reached the base switch to defense
1643         mf = havocbot_ctf_find_flag(this);
1644         if(mf.ctf_status!=FLAG_BASE)
1645         if(vdist(ef.origin - mf.dropped_origin, <, 300))
1646         {
1647                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1648                 return;
1649         }
1650
1651         // Set the role timeout if necessary
1652         if (!this.havocbot_role_timeout)
1653         {
1654                 this.havocbot_role_timeout = time + random() * 30 + 60;
1655         }
1656
1657         // If nothing happened just switch to previous role
1658         if (time > this.havocbot_role_timeout)
1659         {
1660                 this.havocbot_role = this.havocbot_previous_role;
1661                 this.havocbot_role_timeout = 0;
1662                 return;
1663         }
1664
1665         // Chase the flag carrier
1666         if (this.bot_strategytime < time)
1667         {
1668                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1669                 navigation_goalrating_start(this);
1670                 havocbot_goalrating_ctf_enemyflag(this, 30000);
1671                 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1672                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1673                 navigation_goalrating_end(this);
1674         }
1675 }
1676
1677 void havocbot_role_ctf_offense(entity this)
1678 {
1679         entity mf, ef;
1680         vector pos;
1681
1682         if(IS_DEAD(this))
1683         {
1684                 havocbot_ctf_reset_role(this);
1685                 return;
1686         }
1687
1688         if (this.flagcarried)
1689         {
1690                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1691                 return;
1692         }
1693
1694         // Check flags
1695         mf = havocbot_ctf_find_flag(this);
1696         ef = havocbot_ctf_find_enemy_flag(this);
1697
1698         // Own flag stolen
1699         if(mf.ctf_status!=FLAG_BASE)
1700         {
1701                 if(mf.tag_entity)
1702                         pos = mf.tag_entity.origin;
1703                 else
1704                         pos = mf.origin;
1705
1706                 // Try to get it if closer than the enemy base
1707                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1708                 {
1709                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1710                         return;
1711                 }
1712         }
1713
1714         // Escort flag carrier
1715         if(ef.ctf_status!=FLAG_BASE)
1716         {
1717                 if(ef.tag_entity)
1718                         pos = ef.tag_entity.origin;
1719                 else
1720                         pos = ef.origin;
1721
1722                 if(vdist(pos - mf.dropped_origin, >, 700))
1723                 {
1724                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1725                         return;
1726                 }
1727         }
1728
1729         // About to fail, switch to middlefield
1730         if(this.health<50)
1731         {
1732                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1733                 return;
1734         }
1735
1736         // Set the role timeout if necessary
1737         if (!this.havocbot_role_timeout)
1738                 this.havocbot_role_timeout = time + 120;
1739
1740         if (time > this.havocbot_role_timeout)
1741         {
1742                 havocbot_ctf_reset_role(this);
1743                 return;
1744         }
1745
1746         if (this.bot_strategytime < time)
1747         {
1748                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1749                 navigation_goalrating_start(this);
1750                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1751                 havocbot_goalrating_ctf_enemybase(this, 20000);
1752                 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1753                 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1754                 navigation_goalrating_end(this);
1755         }
1756 }
1757
1758 // Retriever (temporary role):
1759 void havocbot_role_ctf_retriever(entity this)
1760 {
1761         entity mf;
1762
1763         if(IS_DEAD(this))
1764         {
1765                 havocbot_ctf_reset_role(this);
1766                 return;
1767         }
1768
1769         if (this.flagcarried)
1770         {
1771                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1772                 return;
1773         }
1774
1775         // If flag is back on the base switch to previous role
1776         mf = havocbot_ctf_find_flag(this);
1777         if(mf.ctf_status==FLAG_BASE)
1778         {
1779                 havocbot_ctf_reset_role(this);
1780                 return;
1781         }
1782
1783         if (!this.havocbot_role_timeout)
1784                 this.havocbot_role_timeout = time + 20;
1785
1786         if (time > this.havocbot_role_timeout)
1787         {
1788                 havocbot_ctf_reset_role(this);
1789                 return;
1790         }
1791
1792         if (this.bot_strategytime < time)
1793         {
1794                 float rt_radius;
1795                 rt_radius = 10000;
1796
1797                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1798                 navigation_goalrating_start(this);
1799                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1800                 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1801                 havocbot_goalrating_ctf_enemybase(this, 30000);
1802                 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1803                 navigation_goalrating_end(this);
1804         }
1805 }
1806
1807 void havocbot_role_ctf_middle(entity this)
1808 {
1809         entity mf;
1810
1811         if(IS_DEAD(this))
1812         {
1813                 havocbot_ctf_reset_role(this);
1814                 return;
1815         }
1816
1817         if (this.flagcarried)
1818         {
1819                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1820                 return;
1821         }
1822
1823         mf = havocbot_ctf_find_flag(this);
1824         if(mf.ctf_status!=FLAG_BASE)
1825         {
1826                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1827                 return;
1828         }
1829
1830         if (!this.havocbot_role_timeout)
1831                 this.havocbot_role_timeout = time + 10;
1832
1833         if (time > this.havocbot_role_timeout)
1834         {
1835                 havocbot_ctf_reset_role(this);
1836                 return;
1837         }
1838
1839         if (this.bot_strategytime < time)
1840         {
1841                 vector org;
1842
1843                 org = havocbot_ctf_middlepoint;
1844                 org.z = this.origin.z;
1845
1846                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1847                 navigation_goalrating_start(this);
1848                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1849                 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1850                 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1851                 havocbot_goalrating_items(this, 5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1852                 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1853                 havocbot_goalrating_ctf_enemybase(this, 2500);
1854                 navigation_goalrating_end(this);
1855         }
1856 }
1857
1858 void havocbot_role_ctf_defense(entity this)
1859 {
1860         entity mf;
1861
1862         if(IS_DEAD(this))
1863         {
1864                 havocbot_ctf_reset_role(this);
1865                 return;
1866         }
1867
1868         if (this.flagcarried)
1869         {
1870                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1871                 return;
1872         }
1873
1874         // If own flag was captured
1875         mf = havocbot_ctf_find_flag(this);
1876         if(mf.ctf_status!=FLAG_BASE)
1877         {
1878                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1879                 return;
1880         }
1881
1882         if (!this.havocbot_role_timeout)
1883                 this.havocbot_role_timeout = time + 30;
1884
1885         if (time > this.havocbot_role_timeout)
1886         {
1887                 havocbot_ctf_reset_role(this);
1888                 return;
1889         }
1890         if (this.bot_strategytime < time)
1891         {
1892                 float mp_radius;
1893                 vector org;
1894
1895                 org = mf.dropped_origin;
1896                 mp_radius = havocbot_ctf_middlepoint_radius;
1897
1898                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1899                 navigation_goalrating_start(this);
1900
1901                 // if enemies are closer to our base, go there
1902                 entity closestplayer = NULL;
1903                 float distance, bestdistance = 10000;
1904                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
1905                         distance = vlen(org - it.origin);
1906                         if(distance<bestdistance)
1907                         {
1908                                 closestplayer = it;
1909                                 bestdistance = distance;
1910                         }
1911                 ));
1912
1913                 if(closestplayer)
1914                 if(DIFF_TEAM(closestplayer, this))
1915                 if(vdist(org - this.origin, >, 1000))
1916                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
1917                         havocbot_goalrating_ctf_ourbase(this, 30000);
1918
1919                 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
1920                 havocbot_goalrating_ctf_droppedflags(this, 20000, org, mp_radius);
1921                 havocbot_goalrating_enemyplayers(this, 15000, org, mp_radius);
1922                 havocbot_goalrating_items(this, 10000, org, mp_radius);
1923                 havocbot_goalrating_items(this, 5000, this.origin, 10000);
1924                 navigation_goalrating_end(this);
1925         }
1926 }
1927
1928 void havocbot_role_ctf_setrole(entity bot, int role)
1929 {
1930         LOG_TRACE(strcat(bot.netname," switched to "));
1931         switch(role)
1932         {
1933                 case HAVOCBOT_CTF_ROLE_CARRIER:
1934                         LOG_TRACE("carrier");
1935                         bot.havocbot_role = havocbot_role_ctf_carrier;
1936                         bot.havocbot_role_timeout = 0;
1937                         bot.havocbot_cantfindflag = time + 10;
1938                         bot.bot_strategytime = 0;
1939                         break;
1940                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1941                         LOG_TRACE("defense");
1942                         bot.havocbot_role = havocbot_role_ctf_defense;
1943                         bot.havocbot_role_timeout = 0;
1944                         break;
1945                 case HAVOCBOT_CTF_ROLE_MIDDLE:
1946                         LOG_TRACE("middle");
1947                         bot.havocbot_role = havocbot_role_ctf_middle;
1948                         bot.havocbot_role_timeout = 0;
1949                         break;
1950                 case HAVOCBOT_CTF_ROLE_OFFENSE:
1951                         LOG_TRACE("offense");
1952                         bot.havocbot_role = havocbot_role_ctf_offense;
1953                         bot.havocbot_role_timeout = 0;
1954                         break;
1955                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
1956                         LOG_TRACE("retriever");
1957                         bot.havocbot_previous_role = bot.havocbot_role;
1958                         bot.havocbot_role = havocbot_role_ctf_retriever;
1959                         bot.havocbot_role_timeout = time + 10;
1960                         bot.bot_strategytime = 0;
1961                         break;
1962                 case HAVOCBOT_CTF_ROLE_ESCORT:
1963                         LOG_TRACE("escort");
1964                         bot.havocbot_previous_role = bot.havocbot_role;
1965                         bot.havocbot_role = havocbot_role_ctf_escort;
1966                         bot.havocbot_role_timeout = time + 30;
1967                         bot.bot_strategytime = 0;
1968                         break;
1969         }
1970         LOG_TRACE("\n");
1971 }
1972
1973
1974 // ==============
1975 // Hook Functions
1976 // ==============
1977
1978 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
1979 {
1980         entity player = M_ARGV(0, entity);
1981
1982         int t = 0, t2 = 0, t3 = 0;
1983
1984         // initially clear items so they can be set as necessary later.
1985         player.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING                | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
1986                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
1987                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
1988                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
1989                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
1990                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
1991
1992         // scan through all the flags and notify the client about them
1993         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
1994         {
1995                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
1996                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
1997                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
1998                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
1999                 if(flag.team == 0)                      { t = CTF_NEUTRAL_FLAG_CARRYING;        t2 = CTF_NEUTRAL_FLAG_TAKEN;    t3 = CTF_NEUTRAL_FLAG_LOST; player.ctf_flagstatus |= CTF_FLAG_NEUTRAL; }
2000
2001                 switch(flag.ctf_status)
2002                 {
2003                         case FLAG_PASSING:
2004                         case FLAG_CARRY:
2005                         {
2006                                 if((flag.owner == player) || (flag.pass_sender == player))
2007                                         player.ctf_flagstatus |= t; // carrying: player is currently carrying the flag
2008                                 else
2009                                         player.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2010                                 break;
2011                         }
2012                         case FLAG_DROPPED:
2013                         {
2014                                 player.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2015                                 break;
2016                         }
2017                 }
2018         }
2019
2020         // item for stopping players from capturing the flag too often
2021         if(player.ctf_captureshielded)
2022                 player.ctf_flagstatus |= CTF_SHIELDED;
2023
2024         if(ctf_stalemate)
2025                 player.ctf_flagstatus |= CTF_STALEMATE;
2026
2027         // update the health of the flag carrier waypointsprite
2028         if(player.wps_flagcarrier)
2029                 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2030 }
2031
2032 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2033 {
2034         entity frag_attacker = M_ARGV(1, entity);
2035         entity frag_target = M_ARGV(2, entity);
2036         float frag_damage = M_ARGV(4, float);
2037         vector frag_force = M_ARGV(6, vector);
2038
2039         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2040         {
2041                 if(frag_target == frag_attacker) // damage done to yourself
2042                 {
2043                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2044                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2045                 }
2046                 else // damage done to everyone else
2047                 {
2048                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2049                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2050                 }
2051
2052                 M_ARGV(4, float) = frag_damage;
2053                 M_ARGV(6, vector) = frag_force;
2054         }
2055         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2056         {
2057                 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)))
2058                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2059                 {
2060                         frag_target.wps_helpme_time = time;
2061                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2062                 }
2063                 // todo: add notification for when flag carrier needs help?
2064         }
2065 }
2066
2067 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2068 {
2069         entity frag_attacker = M_ARGV(1, entity);
2070         entity frag_target = M_ARGV(2, entity);
2071
2072         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2073         {
2074                 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2075                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2076         }
2077
2078         if(frag_target.flagcarried)
2079         {
2080                 entity tmp_entity = frag_target.flagcarried;
2081                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2082                 tmp_entity.ctf_dropper = NULL;
2083         }
2084 }
2085
2086 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2087 {
2088         M_ARGV(2, float) = 0; // frag score
2089         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2090 }
2091
2092 void ctf_RemovePlayer(entity player)
2093 {
2094         if(player.flagcarried)
2095                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2096
2097         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2098         {
2099                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2100                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2101                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2102         }
2103 }
2104
2105 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2106 {
2107         entity player = M_ARGV(0, entity);
2108
2109         ctf_RemovePlayer(player);
2110 }
2111
2112 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2113 {
2114         entity player = M_ARGV(0, entity);
2115
2116         ctf_RemovePlayer(player);
2117 }
2118
2119 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2120 {
2121         entity player = M_ARGV(0, entity);
2122
2123         if(player.flagcarried)
2124         if(!autocvar_g_ctf_portalteleport)
2125                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2126 }
2127
2128 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2129 {
2130         if(MUTATOR_RETURNVALUE || gameover) { return; }
2131
2132         entity player = M_ARGV(0, entity);
2133
2134         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2135         {
2136                 // pass the flag to a team mate
2137                 if(autocvar_g_ctf_pass)
2138                 {
2139                         entity head, closest_target = NULL;
2140                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2141
2142                         while(head) // find the closest acceptable target to pass to
2143                         {
2144                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2145                                 if(head != player && SAME_TEAM(head, player))
2146                                 if(!head.speedrunning && !head.vehicle)
2147                                 {
2148                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2149                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2150                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2151
2152                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2153                                         {
2154                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2155                                                 {
2156                                                         if(IS_BOT_CLIENT(head))
2157                                                         {
2158                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2159                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2160                                                         }
2161                                                         else
2162                                                         {
2163                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2164                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2165                                                         }
2166                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2167                                                         return true;
2168                                                 }
2169                                                 else if(player.flagcarried)
2170                                                 {
2171                                                         if(closest_target)
2172                                                         {
2173                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2174                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2175                                                                         { closest_target = head; }
2176                                                         }
2177                                                         else { closest_target = head; }
2178                                                 }
2179                                         }
2180                                 }
2181                                 head = head.chain;
2182                         }
2183
2184                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2185                 }
2186
2187                 // throw the flag in front of you
2188                 if(autocvar_g_ctf_throw && player.flagcarried)
2189                 {
2190                         if(player.throw_count == -1)
2191                         {
2192                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2193                                 {
2194                                         player.throw_prevtime = time;
2195                                         player.throw_count = 1;
2196                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2197                                         return true;
2198                                 }
2199                                 else
2200                                 {
2201                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2202                                         return false;
2203                                 }
2204                         }
2205                         else
2206                         {
2207                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2208                                 else { player.throw_count += 1; }
2209                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2210
2211                                 player.throw_prevtime = time;
2212                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2213                                 return true;
2214                         }
2215                 }
2216         }
2217 }
2218
2219 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2220 {
2221         entity player = M_ARGV(0, entity);
2222
2223         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2224         {
2225                 player.wps_helpme_time = time;
2226                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2227         }
2228         else // create a normal help me waypointsprite
2229         {
2230                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2231                 WaypointSprite_Ping(player.wps_helpme);
2232         }
2233
2234         return true;
2235 }
2236
2237 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2238 {
2239         entity player = M_ARGV(0, entity);
2240         entity veh = M_ARGV(1, entity);
2241
2242         if(player.flagcarried)
2243         {
2244                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2245                 {
2246                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2247                 }
2248                 else
2249                 {
2250                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2251                         setattachment(player.flagcarried, veh, "");
2252                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2253                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2254                         //player.flagcarried.angles = '0 0 0';
2255                 }
2256                 return true;
2257         }
2258 }
2259
2260 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2261 {
2262         entity player = M_ARGV(0, entity);
2263
2264         if(player.flagcarried)
2265         {
2266                 setattachment(player.flagcarried, player, "");
2267                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2268                 player.flagcarried.scale = FLAG_SCALE;
2269                 player.flagcarried.angles = '0 0 0';
2270                 player.flagcarried.nodrawtoclient = NULL;
2271                 return true;
2272         }
2273 }
2274
2275 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2276 {
2277         entity player = M_ARGV(0, entity);
2278
2279         if(player.flagcarried)
2280         {
2281                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, ((player.flagcarried.team) ? APP_TEAM_ENT(player.flagcarried, INFO_CTF_FLAGRETURN_ABORTRUN) : INFO_CTF_FLAGRETURN_ABORTRUN_NEUTRAL));
2282                 ctf_RespawnFlag(player.flagcarried);
2283                 return true;
2284         }
2285 }
2286
2287 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2288 {
2289         entity flag; // temporary entity for the search method
2290
2291         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2292         {
2293                 switch(flag.ctf_status)
2294                 {
2295                         case FLAG_DROPPED:
2296                         case FLAG_PASSING:
2297                         {
2298                                 // lock the flag, game is over
2299                                 flag.movetype = MOVETYPE_NONE;
2300                                 flag.takedamage = DAMAGE_NO;
2301                                 flag.solid = SOLID_NOT;
2302                                 flag.nextthink = false; // stop thinking
2303
2304                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2305                                 break;
2306                         }
2307
2308                         default:
2309                         case FLAG_BASE:
2310                         case FLAG_CARRY:
2311                         {
2312                                 // do nothing for these flags
2313                                 break;
2314                         }
2315                 }
2316         }
2317 }
2318
2319 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2320 {
2321         entity bot = M_ARGV(0, entity);
2322
2323         havocbot_ctf_reset_role(bot);
2324         return true;
2325 }
2326
2327 MUTATOR_HOOKFUNCTION(ctf, GetTeamCount)
2328 {
2329         //M_ARGV(0, float) = ctf_teams;
2330         M_ARGV(1, string) = "ctf_team";
2331         return true;
2332 }
2333
2334 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2335 {
2336         entity spectatee = M_ARGV(0, entity);
2337         entity client = M_ARGV(1, entity);
2338
2339         client.ctf_flagstatus = spectatee.ctf_flagstatus;
2340 }
2341
2342 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2343 {
2344         int record_page = M_ARGV(0, int);
2345         string ret_string = M_ARGV(1, string);
2346
2347         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2348         {
2349                 if (MapInfo_Get_ByID(i))
2350                 {
2351                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2352
2353                         if(!r)
2354                                 continue;
2355
2356                         // TODO: uid2name
2357                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2358                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2359                 }
2360         }
2361
2362         M_ARGV(1, string) = ret_string;
2363 }
2364
2365 bool superspec_Spectate(entity this, entity targ); // TODO
2366 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2367 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2368 {
2369         entity player = M_ARGV(0, entity);
2370         string cmd_name = M_ARGV(1, string);
2371         int cmd_argc = M_ARGV(2, int);
2372
2373         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2374
2375         if(cmd_name == "followfc")
2376         {
2377                 if(!g_ctf)
2378                         return true;
2379
2380                 int _team = 0;
2381                 bool found = false;
2382
2383                 if(cmd_argc == 2)
2384                 {
2385                         switch(argv(1))
2386                         {
2387                                 case "red": _team = NUM_TEAM_1; break;
2388                                 case "blue": _team = NUM_TEAM_2; break;
2389                                 case "yellow": if(ctf_teams >= 3) _team = NUM_TEAM_3; break;
2390                                 case "pink": if(ctf_teams >= 4) _team = NUM_TEAM_4; break;
2391                         }
2392                 }
2393
2394                 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2395                         if(it.flagcarried && (it.team == _team || _team == 0))
2396                         {
2397                                 found = true;
2398                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2399                                         continue; // already spectating this fc, try another
2400                                 return superspec_Spectate(player, it);
2401                         }
2402                 ));
2403
2404                 if(!found)
2405                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2406                 return true;
2407         }
2408 }
2409
2410 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2411 {
2412         entity frag_target = M_ARGV(0, entity);
2413         
2414         if(frag_target.flagcarried)
2415                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2416 }
2417
2418
2419 // ==========
2420 // Spawnfuncs
2421 // ==========
2422
2423 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2424 CTF flag for team one (Red).
2425 Keys:
2426 "angle" Angle the flag will point (minus 90 degrees)...
2427 "model" model to use, note this needs red and blue as skins 0 and 1...
2428 "noise" sound played when flag is picked up...
2429 "noise1" sound played when flag is returned by a teammate...
2430 "noise2" sound played when flag is captured...
2431 "noise3" sound played when flag is lost in the field and respawns itself...
2432 "noise4" sound played when flag is dropped by a player...
2433 "noise5" sound played when flag touches the ground... */
2434 spawnfunc(item_flag_team1)
2435 {
2436         if(!g_ctf) { remove(this); return; }
2437
2438         ctf_FlagSetup(NUM_TEAM_1, this);
2439 }
2440
2441 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2442 CTF flag for team two (Blue).
2443 Keys:
2444 "angle" Angle the flag will point (minus 90 degrees)...
2445 "model" model to use, note this needs red and blue as skins 0 and 1...
2446 "noise" sound played when flag is picked up...
2447 "noise1" sound played when flag is returned by a teammate...
2448 "noise2" sound played when flag is captured...
2449 "noise3" sound played when flag is lost in the field and respawns itself...
2450 "noise4" sound played when flag is dropped by a player...
2451 "noise5" sound played when flag touches the ground... */
2452 spawnfunc(item_flag_team2)
2453 {
2454         if(!g_ctf) { remove(this); return; }
2455
2456         ctf_FlagSetup(NUM_TEAM_2, this);
2457 }
2458
2459 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2460 CTF flag for team three (Yellow).
2461 Keys:
2462 "angle" Angle the flag will point (minus 90 degrees)...
2463 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2464 "noise" sound played when flag is picked up...
2465 "noise1" sound played when flag is returned by a teammate...
2466 "noise2" sound played when flag is captured...
2467 "noise3" sound played when flag is lost in the field and respawns itself...
2468 "noise4" sound played when flag is dropped by a player...
2469 "noise5" sound played when flag touches the ground... */
2470 spawnfunc(item_flag_team3)
2471 {
2472         if(!g_ctf) { remove(this); return; }
2473
2474         ctf_FlagSetup(NUM_TEAM_3, this);
2475 }
2476
2477 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2478 CTF flag for team four (Pink).
2479 Keys:
2480 "angle" Angle the flag will point (minus 90 degrees)...
2481 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2482 "noise" sound played when flag is picked up...
2483 "noise1" sound played when flag is returned by a teammate...
2484 "noise2" sound played when flag is captured...
2485 "noise3" sound played when flag is lost in the field and respawns itself...
2486 "noise4" sound played when flag is dropped by a player...
2487 "noise5" sound played when flag touches the ground... */
2488 spawnfunc(item_flag_team4)
2489 {
2490         if(!g_ctf) { remove(this); return; }
2491
2492         ctf_FlagSetup(NUM_TEAM_4, this);
2493 }
2494
2495 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2496 CTF flag (Neutral).
2497 Keys:
2498 "angle" Angle the flag will point (minus 90 degrees)...
2499 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2500 "noise" sound played when flag is picked up...
2501 "noise1" sound played when flag is returned by a teammate...
2502 "noise2" sound played when flag is captured...
2503 "noise3" sound played when flag is lost in the field and respawns itself...
2504 "noise4" sound played when flag is dropped by a player...
2505 "noise5" sound played when flag touches the ground... */
2506 spawnfunc(item_flag_neutral)
2507 {
2508         if(!g_ctf) { remove(this); return; }
2509         if(!cvar("g_ctf_oneflag")) { remove(this); return; }
2510
2511         ctf_FlagSetup(0, this);
2512 }
2513
2514 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2515 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2516 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.
2517 Keys:
2518 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2519 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2520 spawnfunc(ctf_team)
2521 {
2522         if(!g_ctf) { remove(this); return; }
2523
2524         this.classname = "ctf_team";
2525         this.team = this.cnt + 1;
2526 }
2527
2528 // compatibility for quake maps
2529 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2530 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2531 spawnfunc(info_player_team1);
2532 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2533 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2534 spawnfunc(info_player_team2);
2535 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2536 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2537
2538 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2539 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2540
2541
2542 // ==============
2543 // Initialization
2544 // ==============
2545
2546 // scoreboard setup
2547 void ctf_ScoreRules(int teams)
2548 {
2549         CheckAllowedTeams(NULL);
2550         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2551         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2552         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2553         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME,  "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2554         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS,  "pickups",   0);
2555         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS,  "fckills",   0);
2556         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS,  "returns",   0);
2557         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,    "drops",     SFL_LOWER_IS_BETTER);
2558         ScoreRules_basics_end();
2559 }
2560
2561 // code from here on is just to support maps that don't have flag and team entities
2562 void ctf_SpawnTeam (string teamname, int teamcolor)
2563 {
2564         entity this = new_pure(ctf_team);
2565         this.netname = teamname;
2566         this.cnt = teamcolor - 1;
2567         this.spawnfunc_checked = true;
2568         this.team = teamcolor;
2569 }
2570
2571 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2572 {
2573         ctf_teams = 2;
2574
2575         entity tmp_entity;
2576         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2577         {
2578                 if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2579                 if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2580                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2581         }
2582
2583         ctf_teams = bound(2, ctf_teams, 4);
2584
2585         // if no teams are found, spawn defaults
2586         if(find(NULL, classname, "ctf_team") == NULL)
2587         {
2588                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.\n");
2589                 ctf_SpawnTeam("Red", NUM_TEAM_1);
2590                 ctf_SpawnTeam("Blue", NUM_TEAM_2);
2591                 if(ctf_teams >= 3)
2592                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2593                 if(ctf_teams >= 4)
2594                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2595         }
2596
2597         ctf_ScoreRules(ctf_teams);
2598 }
2599
2600 void ctf_Initialize()
2601 {
2602         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2603
2604         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2605         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2606         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2607
2608         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2609 }
2610
2611 #endif