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