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