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