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