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