]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
d1c97bd2856f90d2e227f1deb65f1e314a676227
[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_TEAM : CENTER_CTF_PICKUP_TEAM_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(gameover) { 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         float 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         }
1369         if(!n)
1370                 return;
1371         havocbot_ctf_middlepoint = s * (1.0 / n);
1372         havocbot_ctf_middlepoint_radius  = vlen(fo - havocbot_ctf_middlepoint);
1373 }
1374
1375
1376 entity havocbot_ctf_find_flag(entity bot)
1377 {
1378         entity f;
1379         f = ctf_worldflaglist;
1380         while (f)
1381         {
1382                 if (CTF_SAMETEAM(bot, f))
1383                         return f;
1384                 f = f.ctf_worldflagnext;
1385         }
1386         return NULL;
1387 }
1388
1389 entity havocbot_ctf_find_enemy_flag(entity bot)
1390 {
1391         entity f;
1392         f = ctf_worldflaglist;
1393         while (f)
1394         {
1395                 if(ctf_oneflag)
1396                 {
1397                         if(CTF_DIFFTEAM(bot, f))
1398                         {
1399                                 if(f.team)
1400                                 {
1401                                         if(bot.flagcarried)
1402                                                 return f;
1403                                 }
1404                                 else if(!bot.flagcarried)
1405                                         return f;
1406                         }
1407                 }
1408                 else if (CTF_DIFFTEAM(bot, f))
1409                         return f;
1410                 f = f.ctf_worldflagnext;
1411         }
1412         return NULL;
1413 }
1414
1415 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1416 {
1417         if (!teamplay)
1418                 return 0;
1419
1420         int c = 0;
1421
1422         FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
1423                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1424                         continue;
1425
1426                 if(vdist(it.origin - org, <, tc_radius))
1427                         ++c;
1428         ));
1429
1430         return c;
1431 }
1432
1433 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1434 {
1435         entity head;
1436         head = ctf_worldflaglist;
1437         while (head)
1438         {
1439                 if (CTF_SAMETEAM(this, head))
1440                         break;
1441                 head = head.ctf_worldflagnext;
1442         }
1443         if (head)
1444                 navigation_routerating(this, head, ratingscale, 10000);
1445 }
1446
1447 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1448 {
1449         entity head;
1450         head = ctf_worldflaglist;
1451         while (head)
1452         {
1453                 if (CTF_SAMETEAM(this, head))
1454                         break;
1455                 head = head.ctf_worldflagnext;
1456         }
1457         if (!head)
1458                 return;
1459
1460         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1461 }
1462
1463 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1464 {
1465         entity head;
1466         head = ctf_worldflaglist;
1467         while (head)
1468         {
1469                 if(ctf_oneflag)
1470                 {
1471                         if(CTF_DIFFTEAM(this, head))
1472                         {
1473                                 if(head.team)
1474                                 {
1475                                         if(this.flagcarried)
1476                                                 break;
1477                                 }
1478                                 else if(!this.flagcarried)
1479                                         break;
1480                         }
1481                 }
1482                 else if(CTF_DIFFTEAM(this, head))
1483                         break;
1484                 head = head.ctf_worldflagnext;
1485         }
1486         if (head)
1487                 navigation_routerating(this, head, ratingscale, 10000);
1488 }
1489
1490 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1491 {
1492         if (!bot_waypoints_for_items)
1493         {
1494                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1495                 return;
1496         }
1497
1498         entity head;
1499
1500         head = havocbot_ctf_find_enemy_flag(this);
1501
1502         if (!head)
1503                 return;
1504
1505         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1506 }
1507
1508 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1509 {
1510         entity mf;
1511
1512         mf = havocbot_ctf_find_flag(this);
1513
1514         if(mf.ctf_status == FLAG_BASE)
1515                 return;
1516
1517         if(mf.tag_entity)
1518                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1519 }
1520
1521 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1522 {
1523         entity head;
1524         head = ctf_worldflaglist;
1525         while (head)
1526         {
1527                 // flag is out in the field
1528                 if(head.ctf_status != FLAG_BASE)
1529                 if(head.tag_entity==NULL)       // dropped
1530                 {
1531                         if(df_radius)
1532                         {
1533                                 if(vdist(org - head.origin, <, df_radius))
1534                                         navigation_routerating(this, head, ratingscale, 10000);
1535                         }
1536                         else
1537                                 navigation_routerating(this, head, ratingscale, 10000);
1538                 }
1539
1540                 head = head.ctf_worldflagnext;
1541         }
1542 }
1543
1544 void havocbot_goalrating_ctf_carrieritems(entity this, float ratingscale, vector org, float sradius)
1545 {
1546         IL_EACH(g_items, it.bot_pickup,
1547         {
1548                 // gather health and armor only
1549                 if (it.solid)
1550                 if (it.health || it.armorvalue)
1551                 if (vdist(it.origin - org, <, sradius))
1552                 {
1553                         // get the value of the item
1554                         float t = it.bot_pickupevalfunc(this, it) * 0.0001;
1555                         if (t > 0)
1556                                 navigation_routerating(this, it, t * ratingscale, 500);
1557                 }
1558         });
1559 }
1560
1561 void havocbot_ctf_reset_role(entity this)
1562 {
1563         float cdefense, cmiddle, coffense;
1564         entity mf, ef;
1565         float c;
1566
1567         if(IS_DEAD(this))
1568                 return;
1569
1570         if(havocbot_ctf_middlepoint == '0 0 0')
1571                 havocbot_calculate_middlepoint();
1572
1573         // Check ctf flags
1574         if (this.flagcarried)
1575         {
1576                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1577                 return;
1578         }
1579
1580         mf = havocbot_ctf_find_flag(this);
1581         ef = havocbot_ctf_find_enemy_flag(this);
1582
1583         // Retrieve stolen flag
1584         if(mf.ctf_status!=FLAG_BASE)
1585         {
1586                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1587                 return;
1588         }
1589
1590         // If enemy flag is taken go to the middle to intercept pursuers
1591         if(ef.ctf_status!=FLAG_BASE)
1592         {
1593                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1594                 return;
1595         }
1596
1597         // if there is only me on the team switch to offense
1598         c = 0;
1599         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this), LAMBDA(++c));
1600
1601         if(c==1)
1602         {
1603                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1604                 return;
1605         }
1606
1607         // Evaluate best position to take
1608         // Count mates on middle position
1609         cmiddle = havocbot_ctf_teamcount(this, havocbot_ctf_middlepoint, havocbot_ctf_middlepoint_radius * 0.5);
1610
1611         // Count mates on defense position
1612         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_ctf_middlepoint_radius * 0.5);
1613
1614         // Count mates on offense position
1615         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_ctf_middlepoint_radius);
1616
1617         if(cdefense<=coffense)
1618                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1619         else if(coffense<=cmiddle)
1620                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1621         else
1622                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1623 }
1624
1625 void havocbot_role_ctf_carrier(entity this)
1626 {
1627         if(IS_DEAD(this))
1628         {
1629                 havocbot_ctf_reset_role(this);
1630                 return;
1631         }
1632
1633         if (this.flagcarried == NULL)
1634         {
1635                 havocbot_ctf_reset_role(this);
1636                 return;
1637         }
1638
1639         if (this.bot_strategytime < time)
1640         {
1641                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1642
1643                 navigation_goalrating_start(this);
1644                 if(ctf_oneflag)
1645                         havocbot_goalrating_ctf_enemybase(this, 50000);
1646                 else
1647                         havocbot_goalrating_ctf_ourbase(this, 50000);
1648
1649                 if(this.health<100)
1650                         havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1651
1652                 navigation_goalrating_end(this);
1653
1654                 if (this.navigation_hasgoals)
1655                         this.havocbot_cantfindflag = time + 10;
1656                 else if (time > this.havocbot_cantfindflag)
1657                 {
1658                         // Can't navigate to my own base, suicide!
1659                         // TODO: drop it and wander around
1660                         Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
1661                         return;
1662                 }
1663         }
1664 }
1665
1666 void havocbot_role_ctf_escort(entity this)
1667 {
1668         entity mf, ef;
1669
1670         if(IS_DEAD(this))
1671         {
1672                 havocbot_ctf_reset_role(this);
1673                 return;
1674         }
1675
1676         if (this.flagcarried)
1677         {
1678                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1679                 return;
1680         }
1681
1682         // If enemy flag is back on the base switch to previous role
1683         ef = havocbot_ctf_find_enemy_flag(this);
1684         if(ef.ctf_status==FLAG_BASE)
1685         {
1686                 this.havocbot_role = this.havocbot_previous_role;
1687                 this.havocbot_role_timeout = 0;
1688                 return;
1689         }
1690
1691         // If the flag carrier reached the base switch to defense
1692         mf = havocbot_ctf_find_flag(this);
1693         if(mf.ctf_status!=FLAG_BASE)
1694         if(vdist(ef.origin - mf.dropped_origin, <, 300))
1695         {
1696                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1697                 return;
1698         }
1699
1700         // Set the role timeout if necessary
1701         if (!this.havocbot_role_timeout)
1702         {
1703                 this.havocbot_role_timeout = time + random() * 30 + 60;
1704         }
1705
1706         // If nothing happened just switch to previous role
1707         if (time > this.havocbot_role_timeout)
1708         {
1709                 this.havocbot_role = this.havocbot_previous_role;
1710                 this.havocbot_role_timeout = 0;
1711                 return;
1712         }
1713
1714         // Chase the flag carrier
1715         if (this.bot_strategytime < time)
1716         {
1717                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1718                 navigation_goalrating_start(this);
1719                 havocbot_goalrating_ctf_enemyflag(this, 30000);
1720                 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1721                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1722                 navigation_goalrating_end(this);
1723         }
1724 }
1725
1726 void havocbot_role_ctf_offense(entity this)
1727 {
1728         entity mf, ef;
1729         vector pos;
1730
1731         if(IS_DEAD(this))
1732         {
1733                 havocbot_ctf_reset_role(this);
1734                 return;
1735         }
1736
1737         if (this.flagcarried)
1738         {
1739                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1740                 return;
1741         }
1742
1743         // Check flags
1744         mf = havocbot_ctf_find_flag(this);
1745         ef = havocbot_ctf_find_enemy_flag(this);
1746
1747         // Own flag stolen
1748         if(mf.ctf_status!=FLAG_BASE)
1749         {
1750                 if(mf.tag_entity)
1751                         pos = mf.tag_entity.origin;
1752                 else
1753                         pos = mf.origin;
1754
1755                 // Try to get it if closer than the enemy base
1756                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1757                 {
1758                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1759                         return;
1760                 }
1761         }
1762
1763         // Escort flag carrier
1764         if(ef.ctf_status!=FLAG_BASE)
1765         {
1766                 if(ef.tag_entity)
1767                         pos = ef.tag_entity.origin;
1768                 else
1769                         pos = ef.origin;
1770
1771                 if(vdist(pos - mf.dropped_origin, >, 700))
1772                 {
1773                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1774                         return;
1775                 }
1776         }
1777
1778         // About to fail, switch to middlefield
1779         if(this.health<50)
1780         {
1781                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1782                 return;
1783         }
1784
1785         // Set the role timeout if necessary
1786         if (!this.havocbot_role_timeout)
1787                 this.havocbot_role_timeout = time + 120;
1788
1789         if (time > this.havocbot_role_timeout)
1790         {
1791                 havocbot_ctf_reset_role(this);
1792                 return;
1793         }
1794
1795         if (this.bot_strategytime < time)
1796         {
1797                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1798                 navigation_goalrating_start(this);
1799                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1800                 havocbot_goalrating_ctf_enemybase(this, 20000);
1801                 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1802                 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1803                 navigation_goalrating_end(this);
1804         }
1805 }
1806
1807 // Retriever (temporary role):
1808 void havocbot_role_ctf_retriever(entity this)
1809 {
1810         entity mf;
1811
1812         if(IS_DEAD(this))
1813         {
1814                 havocbot_ctf_reset_role(this);
1815                 return;
1816         }
1817
1818         if (this.flagcarried)
1819         {
1820                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1821                 return;
1822         }
1823
1824         // If flag is back on the base switch to previous role
1825         mf = havocbot_ctf_find_flag(this);
1826         if(mf.ctf_status==FLAG_BASE)
1827         {
1828                 if(this.goalcurrent == mf)
1829                 {
1830                         navigation_clearroute(this);
1831                         this.bot_strategytime = 0;
1832                 }
1833                 havocbot_ctf_reset_role(this);
1834                 return;
1835         }
1836
1837         if (!this.havocbot_role_timeout)
1838                 this.havocbot_role_timeout = time + 20;
1839
1840         if (time > this.havocbot_role_timeout)
1841         {
1842                 havocbot_ctf_reset_role(this);
1843                 return;
1844         }
1845
1846         if (this.bot_strategytime < time)
1847         {
1848                 float rt_radius;
1849                 rt_radius = 10000;
1850
1851                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1852                 navigation_goalrating_start(this);
1853                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1854                 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1855                 havocbot_goalrating_ctf_enemybase(this, 30000);
1856                 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1857                 navigation_goalrating_end(this);
1858         }
1859 }
1860
1861 void havocbot_role_ctf_middle(entity this)
1862 {
1863         entity mf;
1864
1865         if(IS_DEAD(this))
1866         {
1867                 havocbot_ctf_reset_role(this);
1868                 return;
1869         }
1870
1871         if (this.flagcarried)
1872         {
1873                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1874                 return;
1875         }
1876
1877         mf = havocbot_ctf_find_flag(this);
1878         if(mf.ctf_status!=FLAG_BASE)
1879         {
1880                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1881                 return;
1882         }
1883
1884         if (!this.havocbot_role_timeout)
1885                 this.havocbot_role_timeout = time + 10;
1886
1887         if (time > this.havocbot_role_timeout)
1888         {
1889                 havocbot_ctf_reset_role(this);
1890                 return;
1891         }
1892
1893         if (this.bot_strategytime < time)
1894         {
1895                 vector org;
1896
1897                 org = havocbot_ctf_middlepoint;
1898                 org.z = this.origin.z;
1899
1900                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1901                 navigation_goalrating_start(this);
1902                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1903                 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1904                 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_ctf_middlepoint_radius * 0.5);
1905                 havocbot_goalrating_items(this, 5000, org, havocbot_ctf_middlepoint_radius * 0.5);
1906                 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1907                 havocbot_goalrating_ctf_enemybase(this, 2500);
1908                 navigation_goalrating_end(this);
1909         }
1910 }
1911
1912 void havocbot_role_ctf_defense(entity this)
1913 {
1914         entity mf;
1915
1916         if(IS_DEAD(this))
1917         {
1918                 havocbot_ctf_reset_role(this);
1919                 return;
1920         }
1921
1922         if (this.flagcarried)
1923         {
1924                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1925                 return;
1926         }
1927
1928         // If own flag was captured
1929         mf = havocbot_ctf_find_flag(this);
1930         if(mf.ctf_status!=FLAG_BASE)
1931         {
1932                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1933                 return;
1934         }
1935
1936         if (!this.havocbot_role_timeout)
1937                 this.havocbot_role_timeout = time + 30;
1938
1939         if (time > this.havocbot_role_timeout)
1940         {
1941                 havocbot_ctf_reset_role(this);
1942                 return;
1943         }
1944         if (this.bot_strategytime < time)
1945         {
1946                 float mp_radius;
1947                 vector org;
1948
1949                 org = mf.dropped_origin;
1950                 mp_radius = havocbot_ctf_middlepoint_radius;
1951
1952                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1953                 navigation_goalrating_start(this);
1954
1955                 // if enemies are closer to our base, go there
1956                 entity closestplayer = NULL;
1957                 float distance, bestdistance = 10000;
1958                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), LAMBDA(
1959                         distance = vlen(org - it.origin);
1960                         if(distance<bestdistance)
1961                         {
1962                                 closestplayer = it;
1963                                 bestdistance = distance;
1964                         }
1965                 ));
1966
1967                 if(closestplayer)
1968                 if(DIFF_TEAM(closestplayer, this))
1969                 if(vdist(org - this.origin, >, 1000))
1970                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
1971                         havocbot_goalrating_ctf_ourbase(this, 30000);
1972
1973                 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
1974                 havocbot_goalrating_ctf_droppedflags(this, 20000, org, mp_radius);
1975                 havocbot_goalrating_enemyplayers(this, 15000, org, mp_radius);
1976                 havocbot_goalrating_items(this, 10000, org, mp_radius);
1977                 havocbot_goalrating_items(this, 5000, this.origin, 10000);
1978                 navigation_goalrating_end(this);
1979         }
1980 }
1981
1982 void havocbot_role_ctf_setrole(entity bot, int role)
1983 {
1984         string s = "(null)";
1985         switch(role)
1986         {
1987                 case HAVOCBOT_CTF_ROLE_CARRIER:
1988                         s = "carrier";
1989                         bot.havocbot_role = havocbot_role_ctf_carrier;
1990                         bot.havocbot_role_timeout = 0;
1991                         bot.havocbot_cantfindflag = time + 10;
1992                         bot.bot_strategytime = 0;
1993                         break;
1994                 case HAVOCBOT_CTF_ROLE_DEFENSE:
1995                         s = "defense";
1996                         bot.havocbot_role = havocbot_role_ctf_defense;
1997                         bot.havocbot_role_timeout = 0;
1998                         break;
1999                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2000                         s = "middle";
2001                         bot.havocbot_role = havocbot_role_ctf_middle;
2002                         bot.havocbot_role_timeout = 0;
2003                         break;
2004                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2005                         s = "offense";
2006                         bot.havocbot_role = havocbot_role_ctf_offense;
2007                         bot.havocbot_role_timeout = 0;
2008                         break;
2009                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2010                         s = "retriever";
2011                         bot.havocbot_previous_role = bot.havocbot_role;
2012                         bot.havocbot_role = havocbot_role_ctf_retriever;
2013                         bot.havocbot_role_timeout = time + 10;
2014                         bot.bot_strategytime = 0;
2015                         break;
2016                 case HAVOCBOT_CTF_ROLE_ESCORT:
2017                         s = "escort";
2018                         bot.havocbot_previous_role = bot.havocbot_role;
2019                         bot.havocbot_role = havocbot_role_ctf_escort;
2020                         bot.havocbot_role_timeout = time + 30;
2021                         bot.bot_strategytime = 0;
2022                         break;
2023         }
2024         LOG_TRACE(bot.netname, " switched to ", s);
2025 }
2026
2027
2028 // ==============
2029 // Hook Functions
2030 // ==============
2031
2032 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2033 {
2034         entity player = M_ARGV(0, entity);
2035
2036         int t = 0, t2 = 0, t3 = 0;
2037
2038         // initially clear items so they can be set as necessary later.
2039         player.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING                | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2040                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2041                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2042                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2043                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2044                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2045
2046         // scan through all the flags and notify the client about them
2047         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2048         {
2049                 if(flag.team == NUM_TEAM_1) { t = CTF_RED_FLAG_CARRYING;                t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2050                 if(flag.team == NUM_TEAM_2) { t = CTF_BLUE_FLAG_CARRYING;               t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2051                 if(flag.team == NUM_TEAM_3) { t = CTF_YELLOW_FLAG_CARRYING;     t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2052                 if(flag.team == NUM_TEAM_4) { t = CTF_PINK_FLAG_CARRYING;               t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2053                 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; }
2054
2055                 switch(flag.ctf_status)
2056                 {
2057                         case FLAG_PASSING:
2058                         case FLAG_CARRY:
2059                         {
2060                                 if((flag.owner == player) || (flag.pass_sender == player))
2061                                         player.ctf_flagstatus |= t; // carrying: player is currently carrying the flag
2062                                 else
2063                                         player.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2064                                 break;
2065                         }
2066                         case FLAG_DROPPED:
2067                         {
2068                                 player.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2069                                 break;
2070                         }
2071                 }
2072         }
2073
2074         // item for stopping players from capturing the flag too often
2075         if(player.ctf_captureshielded)
2076                 player.ctf_flagstatus |= CTF_SHIELDED;
2077
2078         if(ctf_stalemate)
2079                 player.ctf_flagstatus |= CTF_STALEMATE;
2080
2081         // update the health of the flag carrier waypointsprite
2082         if(player.wps_flagcarrier)
2083                 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2084 }
2085
2086 MUTATOR_HOOKFUNCTION(ctf, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2087 {
2088         entity frag_attacker = M_ARGV(1, entity);
2089         entity frag_target = M_ARGV(2, entity);
2090         float frag_damage = M_ARGV(4, float);
2091         vector frag_force = M_ARGV(6, vector);
2092
2093         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2094         {
2095                 if(frag_target == frag_attacker) // damage done to yourself
2096                 {
2097                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2098                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2099                 }
2100                 else // damage done to everyone else
2101                 {
2102                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2103                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2104                 }
2105
2106                 M_ARGV(4, float) = frag_damage;
2107                 M_ARGV(6, vector) = frag_force;
2108         }
2109         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2110         {
2111                 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)))
2112                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2113                 {
2114                         frag_target.wps_helpme_time = time;
2115                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2116                 }
2117                 // todo: add notification for when flag carrier needs help?
2118         }
2119 }
2120
2121 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2122 {
2123         entity frag_attacker = M_ARGV(1, entity);
2124         entity frag_target = M_ARGV(2, entity);
2125
2126         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2127         {
2128                 PlayerTeamScore_AddScore(frag_attacker, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2129                 PlayerScore_Add(frag_attacker, SP_CTF_FCKILLS, 1);
2130         }
2131
2132         if(frag_target.flagcarried)
2133         {
2134                 entity tmp_entity = frag_target.flagcarried;
2135                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2136                 tmp_entity.ctf_dropper = NULL;
2137         }
2138 }
2139
2140 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2141 {
2142         M_ARGV(2, float) = 0; // frag score
2143         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2144 }
2145
2146 void ctf_RemovePlayer(entity player)
2147 {
2148         if(player.flagcarried)
2149                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2150
2151         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2152         {
2153                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2154                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2155                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2156         }
2157 }
2158
2159 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2160 {
2161         entity player = M_ARGV(0, entity);
2162
2163         ctf_RemovePlayer(player);
2164 }
2165
2166 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2167 {
2168         entity player = M_ARGV(0, entity);
2169
2170         ctf_RemovePlayer(player);
2171 }
2172
2173 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2174 {
2175         entity player = M_ARGV(0, entity);
2176
2177         if(player.flagcarried)
2178         if(!autocvar_g_ctf_portalteleport)
2179                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2180 }
2181
2182 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2183 {
2184         if(MUTATOR_RETURNVALUE || gameover) { return; }
2185
2186         entity player = M_ARGV(0, entity);
2187
2188         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2189         {
2190                 // pass the flag to a team mate
2191                 if(autocvar_g_ctf_pass)
2192                 {
2193                         entity head, closest_target = NULL;
2194                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2195
2196                         while(head) // find the closest acceptable target to pass to
2197                         {
2198                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2199                                 if(head != player && SAME_TEAM(head, player))
2200                                 if(!head.speedrunning && !head.vehicle)
2201                                 {
2202                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2203                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2204                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2205
2206                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2207                                         {
2208                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2209                                                 {
2210                                                         if(IS_BOT_CLIENT(head))
2211                                                         {
2212                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2213                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2214                                                         }
2215                                                         else
2216                                                         {
2217                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2218                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2219                                                         }
2220                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2221                                                         return true;
2222                                                 }
2223                                                 else if(player.flagcarried)
2224                                                 {
2225                                                         if(closest_target)
2226                                                         {
2227                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2228                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2229                                                                         { closest_target = head; }
2230                                                         }
2231                                                         else { closest_target = head; }
2232                                                 }
2233                                         }
2234                                 }
2235                                 head = head.chain;
2236                         }
2237
2238                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2239                 }
2240
2241                 // throw the flag in front of you
2242                 if(autocvar_g_ctf_throw && player.flagcarried)
2243                 {
2244                         if(player.throw_count == -1)
2245                         {
2246                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2247                                 {
2248                                         player.throw_prevtime = time;
2249                                         player.throw_count = 1;
2250                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2251                                         return true;
2252                                 }
2253                                 else
2254                                 {
2255                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2256                                         return false;
2257                                 }
2258                         }
2259                         else
2260                         {
2261                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2262                                 else { player.throw_count += 1; }
2263                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2264
2265                                 player.throw_prevtime = time;
2266                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2267                                 return true;
2268                         }
2269                 }
2270         }
2271 }
2272
2273 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2274 {
2275         entity player = M_ARGV(0, entity);
2276
2277         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2278         {
2279                 player.wps_helpme_time = time;
2280                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2281         }
2282         else // create a normal help me waypointsprite
2283         {
2284                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2285                 WaypointSprite_Ping(player.wps_helpme);
2286         }
2287
2288         return true;
2289 }
2290
2291 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2292 {
2293         entity player = M_ARGV(0, entity);
2294         entity veh = M_ARGV(1, entity);
2295
2296         if(player.flagcarried)
2297         {
2298                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2299                 {
2300                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2301                 }
2302                 else
2303                 {
2304                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2305                         setattachment(player.flagcarried, veh, "");
2306                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2307                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2308                         //player.flagcarried.angles = '0 0 0';
2309                 }
2310                 return true;
2311         }
2312 }
2313
2314 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2315 {
2316         entity player = M_ARGV(0, entity);
2317
2318         if(player.flagcarried)
2319         {
2320                 setattachment(player.flagcarried, player, "");
2321                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2322                 player.flagcarried.scale = FLAG_SCALE;
2323                 player.flagcarried.angles = '0 0 0';
2324                 player.flagcarried.nodrawtoclient = NULL;
2325                 return true;
2326         }
2327 }
2328
2329 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2330 {
2331         entity player = M_ARGV(0, entity);
2332
2333         if(player.flagcarried)
2334         {
2335                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2336                 ctf_RespawnFlag(player.flagcarried);
2337                 return true;
2338         }
2339 }
2340
2341 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2342 {
2343         entity flag; // temporary entity for the search method
2344
2345         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2346         {
2347                 switch(flag.ctf_status)
2348                 {
2349                         case FLAG_DROPPED:
2350                         case FLAG_PASSING:
2351                         {
2352                                 // lock the flag, game is over
2353                                 set_movetype(flag, MOVETYPE_NONE);
2354                                 flag.takedamage = DAMAGE_NO;
2355                                 flag.solid = SOLID_NOT;
2356                                 flag.nextthink = false; // stop thinking
2357
2358                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2359                                 break;
2360                         }
2361
2362                         default:
2363                         case FLAG_BASE:
2364                         case FLAG_CARRY:
2365                         {
2366                                 // do nothing for these flags
2367                                 break;
2368                         }
2369                 }
2370         }
2371 }
2372
2373 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2374 {
2375         entity bot = M_ARGV(0, entity);
2376
2377         havocbot_ctf_reset_role(bot);
2378         return true;
2379 }
2380
2381 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2382 {
2383         //M_ARGV(0, float) = ctf_teams;
2384         M_ARGV(1, string) = "ctf_team";
2385         return true;
2386 }
2387
2388 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2389 {
2390         entity spectatee = M_ARGV(0, entity);
2391         entity client = M_ARGV(1, entity);
2392
2393         client.ctf_flagstatus = spectatee.ctf_flagstatus;
2394 }
2395
2396 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2397 {
2398         int record_page = M_ARGV(0, int);
2399         string ret_string = M_ARGV(1, string);
2400
2401         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2402         {
2403                 if (MapInfo_Get_ByID(i))
2404                 {
2405                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2406
2407                         if(!r)
2408                                 continue;
2409
2410                         // TODO: uid2name
2411                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2412                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2413                 }
2414         }
2415
2416         M_ARGV(1, string) = ret_string;
2417 }
2418
2419 bool superspec_Spectate(entity this, entity targ); // TODO
2420 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2421 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2422 {
2423         entity player = M_ARGV(0, entity);
2424         string cmd_name = M_ARGV(1, string);
2425         int cmd_argc = M_ARGV(2, int);
2426
2427         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2428
2429         if(cmd_name == "followfc")
2430         {
2431                 if(!g_ctf)
2432                         return true;
2433
2434                 int _team = 0;
2435                 bool found = false;
2436
2437                 if(cmd_argc == 2)
2438                 {
2439                         switch(argv(1))
2440                         {
2441                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2442                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2443                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2444                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2445                         }
2446                 }
2447
2448                 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
2449                         if(it.flagcarried && (it.team == _team || _team == 0))
2450                         {
2451                                 found = true;
2452                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2453                                         continue; // already spectating this fc, try another
2454                                 return superspec_Spectate(player, it);
2455                         }
2456                 ));
2457
2458                 if(!found)
2459                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2460                 return true;
2461         }
2462 }
2463
2464 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2465 {
2466         entity frag_target = M_ARGV(0, entity);
2467
2468         if(frag_target.flagcarried)
2469                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2470 }
2471
2472
2473 // ==========
2474 // Spawnfuncs
2475 // ==========
2476
2477 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2478 CTF flag for team one (Red).
2479 Keys:
2480 "angle" Angle the flag will point (minus 90 degrees)...
2481 "model" model to use, note this needs red and blue as skins 0 and 1...
2482 "noise" sound played when flag is picked up...
2483 "noise1" sound played when flag is returned by a teammate...
2484 "noise2" sound played when flag is captured...
2485 "noise3" sound played when flag is lost in the field and respawns itself...
2486 "noise4" sound played when flag is dropped by a player...
2487 "noise5" sound played when flag touches the ground... */
2488 spawnfunc(item_flag_team1)
2489 {
2490         if(!g_ctf) { delete(this); return; }
2491
2492         ctf_FlagSetup(NUM_TEAM_1, this);
2493 }
2494
2495 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2496 CTF flag for team two (Blue).
2497 Keys:
2498 "angle" Angle the flag will point (minus 90 degrees)...
2499 "model" model to use, note this needs red and blue as skins 0 and 1...
2500 "noise" sound played when flag is picked up...
2501 "noise1" sound played when flag is returned by a teammate...
2502 "noise2" sound played when flag is captured...
2503 "noise3" sound played when flag is lost in the field and respawns itself...
2504 "noise4" sound played when flag is dropped by a player...
2505 "noise5" sound played when flag touches the ground... */
2506 spawnfunc(item_flag_team2)
2507 {
2508         if(!g_ctf) { delete(this); return; }
2509
2510         ctf_FlagSetup(NUM_TEAM_2, this);
2511 }
2512
2513 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2514 CTF flag for team three (Yellow).
2515 Keys:
2516 "angle" Angle the flag will point (minus 90 degrees)...
2517 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2518 "noise" sound played when flag is picked up...
2519 "noise1" sound played when flag is returned by a teammate...
2520 "noise2" sound played when flag is captured...
2521 "noise3" sound played when flag is lost in the field and respawns itself...
2522 "noise4" sound played when flag is dropped by a player...
2523 "noise5" sound played when flag touches the ground... */
2524 spawnfunc(item_flag_team3)
2525 {
2526         if(!g_ctf) { delete(this); return; }
2527
2528         ctf_FlagSetup(NUM_TEAM_3, this);
2529 }
2530
2531 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2532 CTF flag for team four (Pink).
2533 Keys:
2534 "angle" Angle the flag will point (minus 90 degrees)...
2535 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2536 "noise" sound played when flag is picked up...
2537 "noise1" sound played when flag is returned by a teammate...
2538 "noise2" sound played when flag is captured...
2539 "noise3" sound played when flag is lost in the field and respawns itself...
2540 "noise4" sound played when flag is dropped by a player...
2541 "noise5" sound played when flag touches the ground... */
2542 spawnfunc(item_flag_team4)
2543 {
2544         if(!g_ctf) { delete(this); return; }
2545
2546         ctf_FlagSetup(NUM_TEAM_4, this);
2547 }
2548
2549 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2550 CTF flag (Neutral).
2551 Keys:
2552 "angle" Angle the flag will point (minus 90 degrees)...
2553 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2554 "noise" sound played when flag is picked up...
2555 "noise1" sound played when flag is returned by a teammate...
2556 "noise2" sound played when flag is captured...
2557 "noise3" sound played when flag is lost in the field and respawns itself...
2558 "noise4" sound played when flag is dropped by a player...
2559 "noise5" sound played when flag touches the ground... */
2560 spawnfunc(item_flag_neutral)
2561 {
2562         if(!g_ctf) { delete(this); return; }
2563         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2564
2565         ctf_FlagSetup(0, this);
2566 }
2567
2568 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2569 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2570 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.
2571 Keys:
2572 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2573 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2574 spawnfunc(ctf_team)
2575 {
2576         if(!g_ctf) { delete(this); return; }
2577
2578         this.classname = "ctf_team";
2579         this.team = this.cnt + 1;
2580 }
2581
2582 // compatibility for quake maps
2583 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2584 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2585 spawnfunc(info_player_team1);
2586 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2587 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2588 spawnfunc(info_player_team2);
2589 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2590 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2591
2592 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2593 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2594
2595 // compatibility for wop maps
2596 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2597 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2598 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2599 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2600 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2601 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2602
2603
2604 // ==============
2605 // Initialization
2606 // ==============
2607
2608 // scoreboard setup
2609 void ctf_ScoreRules(int teams)
2610 {
2611         CheckAllowedTeams(NULL);
2612         ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
2613         ScoreInfo_SetLabel_TeamScore  (ST_CTF_CAPS,     "caps",      SFL_SORT_PRIO_PRIMARY);
2614         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPS,    "caps",      SFL_SORT_PRIO_SECONDARY);
2615         ScoreInfo_SetLabel_PlayerScore(SP_CTF_CAPTIME, "captime",   SFL_LOWER_IS_BETTER | SFL_TIME);
2616         ScoreInfo_SetLabel_PlayerScore(SP_CTF_PICKUPS, "pickups",   0);
2617         ScoreInfo_SetLabel_PlayerScore(SP_CTF_FCKILLS, "fckills",   0);
2618         ScoreInfo_SetLabel_PlayerScore(SP_CTF_RETURNS, "returns",   0);
2619         ScoreInfo_SetLabel_PlayerScore(SP_CTF_DROPS,   "drops",     SFL_LOWER_IS_BETTER);
2620         ScoreRules_basics_end();
2621 }
2622
2623 // code from here on is just to support maps that don't have flag and team entities
2624 void ctf_SpawnTeam (string teamname, int teamcolor)
2625 {
2626         entity this = new_pure(ctf_team);
2627         this.netname = teamname;
2628         this.cnt = teamcolor - 1;
2629         this.spawnfunc_checked = true;
2630         this.team = teamcolor;
2631 }
2632
2633 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2634 {
2635         ctf_teams = 0;
2636
2637         entity tmp_entity;
2638         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2639         {
2640                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2641                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2642
2643                 switch(tmp_entity.team)
2644                 {
2645                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2646                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2647                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2648                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2649                 }
2650                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2651         }
2652
2653         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2654         {
2655                 ctf_teams = 0; // so set the default red and blue teams
2656                 BITSET_ASSIGN(ctf_teams, BIT(0));
2657                 BITSET_ASSIGN(ctf_teams, BIT(1));
2658         }
2659
2660         //ctf_teams = bound(2, ctf_teams, 4);
2661
2662         // if no teams are found, spawn defaults
2663         if(find(NULL, classname, "ctf_team") == NULL)
2664         {
2665                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2666                 if(ctf_teams & BIT(0))
2667                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2668                 if(ctf_teams & BIT(1))
2669                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2670                 if(ctf_teams & BIT(2))
2671                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2672                 if(ctf_teams & BIT(3))
2673                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2674         }
2675
2676         ctf_ScoreRules(ctf_teams);
2677 }
2678
2679 void ctf_Initialize()
2680 {
2681         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2682
2683         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2684         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2685         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2686
2687         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2688 }