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