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