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