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