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