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