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