]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_ctf.qc
Merge branch 'master' into Lyberta/StandaloneOverkillWeapons
[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 (this.bot_strategytime < time)
1657         {
1658                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1659
1660                 navigation_goalrating_start(this);
1661                 if(ctf_oneflag)
1662                         havocbot_goalrating_ctf_enemybase(this, 50000);
1663                 else
1664                         havocbot_goalrating_ctf_ourbase(this, 50000);
1665
1666                 if(this.health<100)
1667                         havocbot_goalrating_ctf_carrieritems(this, 1000, this.origin, 1000);
1668
1669                 navigation_goalrating_end(this);
1670
1671                 if (this.goalentity)
1672                         this.havocbot_cantfindflag = time + 10;
1673                 else if (time > this.havocbot_cantfindflag)
1674                 {
1675                         // Can't navigate to my own base, suicide!
1676                         // TODO: drop it and wander around
1677                         Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
1678                         return;
1679                 }
1680         }
1681 }
1682
1683 void havocbot_role_ctf_escort(entity this)
1684 {
1685         entity mf, ef;
1686
1687         if(IS_DEAD(this))
1688         {
1689                 havocbot_ctf_reset_role(this);
1690                 return;
1691         }
1692
1693         if (this.flagcarried)
1694         {
1695                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1696                 return;
1697         }
1698
1699         // If enemy flag is back on the base switch to previous role
1700         ef = havocbot_ctf_find_enemy_flag(this);
1701         if(ef.ctf_status==FLAG_BASE)
1702         {
1703                 this.havocbot_role = this.havocbot_previous_role;
1704                 this.havocbot_role_timeout = 0;
1705                 return;
1706         }
1707
1708         // If the flag carrier reached the base switch to defense
1709         mf = havocbot_ctf_find_flag(this);
1710         if(mf.ctf_status!=FLAG_BASE)
1711         if(vdist(ef.origin - mf.dropped_origin, <, 300))
1712         {
1713                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1714                 return;
1715         }
1716
1717         // Set the role timeout if necessary
1718         if (!this.havocbot_role_timeout)
1719         {
1720                 this.havocbot_role_timeout = time + random() * 30 + 60;
1721         }
1722
1723         // If nothing happened just switch to previous role
1724         if (time > this.havocbot_role_timeout)
1725         {
1726                 this.havocbot_role = this.havocbot_previous_role;
1727                 this.havocbot_role_timeout = 0;
1728                 return;
1729         }
1730
1731         // Chase the flag carrier
1732         if (this.bot_strategytime < time)
1733         {
1734                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1735                 navigation_goalrating_start(this);
1736                 havocbot_goalrating_ctf_enemyflag(this, 30000);
1737                 havocbot_goalrating_ctf_ourstolenflag(this, 40000);
1738                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
1739                 navigation_goalrating_end(this);
1740         }
1741 }
1742
1743 void havocbot_role_ctf_offense(entity this)
1744 {
1745         entity mf, ef;
1746         vector pos;
1747
1748         if(IS_DEAD(this))
1749         {
1750                 havocbot_ctf_reset_role(this);
1751                 return;
1752         }
1753
1754         if (this.flagcarried)
1755         {
1756                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1757                 return;
1758         }
1759
1760         // Check flags
1761         mf = havocbot_ctf_find_flag(this);
1762         ef = havocbot_ctf_find_enemy_flag(this);
1763
1764         // Own flag stolen
1765         if(mf.ctf_status!=FLAG_BASE)
1766         {
1767                 if(mf.tag_entity)
1768                         pos = mf.tag_entity.origin;
1769                 else
1770                         pos = mf.origin;
1771
1772                 // Try to get it if closer than the enemy base
1773                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1774                 {
1775                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1776                         return;
1777                 }
1778         }
1779
1780         // Escort flag carrier
1781         if(ef.ctf_status!=FLAG_BASE)
1782         {
1783                 if(ef.tag_entity)
1784                         pos = ef.tag_entity.origin;
1785                 else
1786                         pos = ef.origin;
1787
1788                 if(vdist(pos - mf.dropped_origin, >, 700))
1789                 {
1790                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1791                         return;
1792                 }
1793         }
1794
1795         // About to fail, switch to middlefield
1796         if(this.health<50)
1797         {
1798                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1799                 return;
1800         }
1801
1802         // Set the role timeout if necessary
1803         if (!this.havocbot_role_timeout)
1804                 this.havocbot_role_timeout = time + 120;
1805
1806         if (time > this.havocbot_role_timeout)
1807         {
1808                 havocbot_ctf_reset_role(this);
1809                 return;
1810         }
1811
1812         if (this.bot_strategytime < time)
1813         {
1814                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1815                 navigation_goalrating_start(this);
1816                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1817                 havocbot_goalrating_ctf_enemybase(this, 20000);
1818                 havocbot_goalrating_items(this, 5000, this.origin, 1000);
1819                 havocbot_goalrating_items(this, 1000, this.origin, 10000);
1820                 navigation_goalrating_end(this);
1821         }
1822 }
1823
1824 // Retriever (temporary role):
1825 void havocbot_role_ctf_retriever(entity this)
1826 {
1827         entity mf;
1828
1829         if(IS_DEAD(this))
1830         {
1831                 havocbot_ctf_reset_role(this);
1832                 return;
1833         }
1834
1835         if (this.flagcarried)
1836         {
1837                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1838                 return;
1839         }
1840
1841         // If flag is back on the base switch to previous role
1842         mf = havocbot_ctf_find_flag(this);
1843         if(mf.ctf_status==FLAG_BASE)
1844         {
1845                 if(this.goalcurrent == mf)
1846                 {
1847                         navigation_clearroute(this);
1848                         this.bot_strategytime = 0;
1849                 }
1850                 havocbot_ctf_reset_role(this);
1851                 return;
1852         }
1853
1854         if (!this.havocbot_role_timeout)
1855                 this.havocbot_role_timeout = time + 20;
1856
1857         if (time > this.havocbot_role_timeout)
1858         {
1859                 havocbot_ctf_reset_role(this);
1860                 return;
1861         }
1862
1863         if (this.bot_strategytime < time)
1864         {
1865                 float rt_radius;
1866                 rt_radius = 10000;
1867
1868                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1869                 navigation_goalrating_start(this);
1870                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1871                 havocbot_goalrating_ctf_droppedflags(this, 40000, this.origin, rt_radius);
1872                 havocbot_goalrating_ctf_enemybase(this, 30000);
1873                 havocbot_goalrating_items(this, 500, this.origin, rt_radius);
1874                 navigation_goalrating_end(this);
1875         }
1876 }
1877
1878 void havocbot_role_ctf_middle(entity this)
1879 {
1880         entity mf;
1881
1882         if(IS_DEAD(this))
1883         {
1884                 havocbot_ctf_reset_role(this);
1885                 return;
1886         }
1887
1888         if (this.flagcarried)
1889         {
1890                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1891                 return;
1892         }
1893
1894         mf = havocbot_ctf_find_flag(this);
1895         if(mf.ctf_status!=FLAG_BASE)
1896         {
1897                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1898                 return;
1899         }
1900
1901         if (!this.havocbot_role_timeout)
1902                 this.havocbot_role_timeout = time + 10;
1903
1904         if (time > this.havocbot_role_timeout)
1905         {
1906                 havocbot_ctf_reset_role(this);
1907                 return;
1908         }
1909
1910         if (this.bot_strategytime < time)
1911         {
1912                 vector org;
1913
1914                 org = havocbot_middlepoint;
1915                 org.z = this.origin.z;
1916
1917                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1918                 navigation_goalrating_start(this);
1919                 havocbot_goalrating_ctf_ourstolenflag(this, 50000);
1920                 havocbot_goalrating_ctf_droppedflags(this, 30000, this.origin, 10000);
1921                 havocbot_goalrating_enemyplayers(this, 10000, org, havocbot_middlepoint_radius * 0.5);
1922                 havocbot_goalrating_items(this, 5000, org, havocbot_middlepoint_radius * 0.5);
1923                 havocbot_goalrating_items(this, 2500, this.origin, 10000);
1924                 havocbot_goalrating_ctf_enemybase(this, 2500);
1925                 navigation_goalrating_end(this);
1926         }
1927 }
1928
1929 void havocbot_role_ctf_defense(entity this)
1930 {
1931         entity mf;
1932
1933         if(IS_DEAD(this))
1934         {
1935                 havocbot_ctf_reset_role(this);
1936                 return;
1937         }
1938
1939         if (this.flagcarried)
1940         {
1941                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1942                 return;
1943         }
1944
1945         // If own flag was captured
1946         mf = havocbot_ctf_find_flag(this);
1947         if(mf.ctf_status!=FLAG_BASE)
1948         {
1949                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1950                 return;
1951         }
1952
1953         if (!this.havocbot_role_timeout)
1954                 this.havocbot_role_timeout = time + 30;
1955
1956         if (time > this.havocbot_role_timeout)
1957         {
1958                 havocbot_ctf_reset_role(this);
1959                 return;
1960         }
1961         if (this.bot_strategytime < time)
1962         {
1963                 vector org = mf.dropped_origin;
1964
1965                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1966                 navigation_goalrating_start(this);
1967
1968                 // if enemies are closer to our base, go there
1969                 entity closestplayer = NULL;
1970                 float distance, bestdistance = 10000;
1971                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
1972                         distance = vlen(org - it.origin);
1973                         if(distance<bestdistance)
1974                         {
1975                                 closestplayer = it;
1976                                 bestdistance = distance;
1977                         }
1978                 });
1979
1980                 if(closestplayer)
1981                 if(DIFF_TEAM(closestplayer, this))
1982                 if(vdist(org - this.origin, >, 1000))
1983                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
1984                         havocbot_goalrating_ctf_ourbase(this, 30000);
1985
1986                 havocbot_goalrating_ctf_ourstolenflag(this, 20000);
1987                 havocbot_goalrating_ctf_droppedflags(this, 20000, org, havocbot_middlepoint_radius);
1988                 havocbot_goalrating_enemyplayers(this, 15000, org, havocbot_middlepoint_radius);
1989                 havocbot_goalrating_items(this, 10000, org, havocbot_middlepoint_radius);
1990                 havocbot_goalrating_items(this, 5000, this.origin, 10000);
1991                 navigation_goalrating_end(this);
1992         }
1993 }
1994
1995 void havocbot_role_ctf_setrole(entity bot, int role)
1996 {
1997         string s = "(null)";
1998         switch(role)
1999         {
2000                 case HAVOCBOT_CTF_ROLE_CARRIER:
2001                         s = "carrier";
2002                         bot.havocbot_role = havocbot_role_ctf_carrier;
2003                         bot.havocbot_role_timeout = 0;
2004                         bot.havocbot_cantfindflag = time + 10;
2005                         bot.bot_strategytime = 0;
2006                         break;
2007                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2008                         s = "defense";
2009                         bot.havocbot_role = havocbot_role_ctf_defense;
2010                         bot.havocbot_role_timeout = 0;
2011                         break;
2012                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2013                         s = "middle";
2014                         bot.havocbot_role = havocbot_role_ctf_middle;
2015                         bot.havocbot_role_timeout = 0;
2016                         break;
2017                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2018                         s = "offense";
2019                         bot.havocbot_role = havocbot_role_ctf_offense;
2020                         bot.havocbot_role_timeout = 0;
2021                         break;
2022                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2023                         s = "retriever";
2024                         bot.havocbot_previous_role = bot.havocbot_role;
2025                         bot.havocbot_role = havocbot_role_ctf_retriever;
2026                         bot.havocbot_role_timeout = time + 10;
2027                         bot.bot_strategytime = 0;
2028                         break;
2029                 case HAVOCBOT_CTF_ROLE_ESCORT:
2030                         s = "escort";
2031                         bot.havocbot_previous_role = bot.havocbot_role;
2032                         bot.havocbot_role = havocbot_role_ctf_escort;
2033                         bot.havocbot_role_timeout = time + 30;
2034                         bot.bot_strategytime = 0;
2035                         break;
2036         }
2037         LOG_TRACE(bot.netname, " switched to ", s);
2038 }
2039
2040
2041 // ==============
2042 // Hook Functions
2043 // ==============
2044
2045 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2046 {
2047         entity player = M_ARGV(0, entity);
2048
2049         int t = 0, t2 = 0, t3 = 0;
2050         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)
2051
2052         // initially clear items so they can be set as necessary later.
2053         player.ctf_flagstatus &= ~(CTF_RED_FLAG_CARRYING                | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2054                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2055                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2056                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2057                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2058                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2059
2060         // scan through all the flags and notify the client about them
2061         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2062         {
2063                 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2064                 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2065                 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2066                 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2067                 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; }
2068
2069                 switch(flag.ctf_status)
2070                 {
2071                         case FLAG_PASSING:
2072                         case FLAG_CARRY:
2073                         {
2074                                 if((flag.owner == player) || (flag.pass_sender == player))
2075                                         player.ctf_flagstatus |= t; // carrying: player is currently carrying the flag
2076                                 else
2077                                         player.ctf_flagstatus |= t2; // taken: someone else is carrying the flag
2078                                 break;
2079                         }
2080                         case FLAG_DROPPED:
2081                         {
2082                                 player.ctf_flagstatus |= t3; // lost: the flag is dropped somewhere on the map
2083                                 break;
2084                         }
2085                 }
2086         }
2087
2088         // item for stopping players from capturing the flag too often
2089         if(player.ctf_captureshielded)
2090                 player.ctf_flagstatus |= CTF_SHIELDED;
2091
2092         if(ctf_stalemate)
2093                 player.ctf_flagstatus |= CTF_STALEMATE;
2094
2095         // update the health of the flag carrier waypointsprite
2096         if(player.wps_flagcarrier)
2097                 WaypointSprite_UpdateHealth(player.wps_flagcarrier, '1 0 0' * healtharmor_maxdamage(player.health, player.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
2098 }
2099
2100 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2101 {
2102         entity frag_attacker = M_ARGV(1, entity);
2103         entity frag_target = M_ARGV(2, entity);
2104         float frag_damage = M_ARGV(4, float);
2105         vector frag_force = M_ARGV(6, vector);
2106
2107         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2108         {
2109                 if(frag_target == frag_attacker) // damage done to yourself
2110                 {
2111                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2112                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2113                 }
2114                 else // damage done to everyone else
2115                 {
2116                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2117                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2118                 }
2119
2120                 M_ARGV(4, float) = frag_damage;
2121                 M_ARGV(6, vector) = frag_force;
2122         }
2123         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2124         {
2125                 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)))
2126                 if(time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2127                 {
2128                         frag_target.wps_helpme_time = time;
2129                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2130                 }
2131                 // todo: add notification for when flag carrier needs help?
2132         }
2133 }
2134
2135 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2136 {
2137         entity frag_attacker = M_ARGV(1, entity);
2138         entity frag_target = M_ARGV(2, entity);
2139
2140         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2141         {
2142                 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2143                 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2144         }
2145
2146         if(frag_target.flagcarried)
2147         {
2148                 entity tmp_entity = frag_target.flagcarried;
2149                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2150                 tmp_entity.ctf_dropper = NULL;
2151         }
2152 }
2153
2154 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2155 {
2156         M_ARGV(2, float) = 0; // frag score
2157         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2158 }
2159
2160 void ctf_RemovePlayer(entity player)
2161 {
2162         if(player.flagcarried)
2163                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2164
2165         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2166         {
2167                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2168                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2169                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2170         }
2171 }
2172
2173 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2174 {
2175         entity player = M_ARGV(0, entity);
2176
2177         ctf_RemovePlayer(player);
2178 }
2179
2180 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2181 {
2182         entity player = M_ARGV(0, entity);
2183
2184         ctf_RemovePlayer(player);
2185 }
2186
2187 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2188 {
2189         if(!autocvar_g_ctf_leaderboard)
2190                 return;
2191
2192         entity player = M_ARGV(0, entity);
2193
2194         if(IS_REAL_CLIENT(player))
2195         {
2196                 for(int i = 1; i <= RANKINGS_CNT; ++i)
2197                 {
2198                         race_SendRankings(i, 0, 0, MSG_ONE);
2199                 }
2200         }
2201 }
2202
2203 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2204 {
2205         if(!autocvar_g_ctf_leaderboard)
2206                 return;
2207
2208         entity player = M_ARGV(0, entity);
2209
2210         if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2211         {
2212                 if (!player.stored_netname)
2213                         player.stored_netname = strzone(uid2name(player.crypto_idfp));
2214                 if(player.stored_netname != player.netname)
2215                 {
2216                         db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2217                         strunzone(player.stored_netname);
2218                         player.stored_netname = strzone(player.netname);
2219                 }
2220         }
2221 }
2222
2223 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2224 {
2225         entity player = M_ARGV(0, entity);
2226
2227         if(player.flagcarried)
2228         if(!autocvar_g_ctf_portalteleport)
2229                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2230 }
2231
2232 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2233 {
2234         if(MUTATOR_RETURNVALUE || game_stopped) return;
2235
2236         entity player = M_ARGV(0, entity);
2237
2238         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2239         {
2240                 // pass the flag to a team mate
2241                 if(autocvar_g_ctf_pass)
2242                 {
2243                         entity head, closest_target = NULL;
2244                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2245
2246                         while(head) // find the closest acceptable target to pass to
2247                         {
2248                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2249                                 if(head != player && SAME_TEAM(head, player))
2250                                 if(!head.speedrunning && !head.vehicle)
2251                                 {
2252                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2253                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2254                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2255
2256                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2257                                         {
2258                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2259                                                 {
2260                                                         if(IS_BOT_CLIENT(head))
2261                                                         {
2262                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2263                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2264                                                         }
2265                                                         else
2266                                                         {
2267                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2268                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2269                                                         }
2270                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2271                                                         return true;
2272                                                 }
2273                                                 else if(player.flagcarried && !head.flagcarried)
2274                                                 {
2275                                                         if(closest_target)
2276                                                         {
2277                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2278                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2279                                                                         { closest_target = head; }
2280                                                         }
2281                                                         else { closest_target = head; }
2282                                                 }
2283                                         }
2284                                 }
2285                                 head = head.chain;
2286                         }
2287
2288                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2289                 }
2290
2291                 // throw the flag in front of you
2292                 if(autocvar_g_ctf_throw && player.flagcarried)
2293                 {
2294                         if(player.throw_count == -1)
2295                         {
2296                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2297                                 {
2298                                         player.throw_prevtime = time;
2299                                         player.throw_count = 1;
2300                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2301                                         return true;
2302                                 }
2303                                 else
2304                                 {
2305                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2306                                         return false;
2307                                 }
2308                         }
2309                         else
2310                         {
2311                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2312                                 else { player.throw_count += 1; }
2313                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2314
2315                                 player.throw_prevtime = time;
2316                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2317                                 return true;
2318                         }
2319                 }
2320         }
2321 }
2322
2323 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2324 {
2325         entity player = M_ARGV(0, entity);
2326
2327         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2328         {
2329                 player.wps_helpme_time = time;
2330                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2331         }
2332         else // create a normal help me waypointsprite
2333         {
2334                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2335                 WaypointSprite_Ping(player.wps_helpme);
2336         }
2337
2338         return true;
2339 }
2340
2341 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2342 {
2343         entity player = M_ARGV(0, entity);
2344         entity veh = M_ARGV(1, entity);
2345
2346         if(player.flagcarried)
2347         {
2348                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2349                 {
2350                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2351                 }
2352                 else
2353                 {
2354                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2355                         setattachment(player.flagcarried, veh, "");
2356                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2357                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2358                         //player.flagcarried.angles = '0 0 0';
2359                 }
2360                 return true;
2361         }
2362 }
2363
2364 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2365 {
2366         entity player = M_ARGV(0, entity);
2367
2368         if(player.flagcarried)
2369         {
2370                 setattachment(player.flagcarried, player, "");
2371                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2372                 player.flagcarried.scale = FLAG_SCALE;
2373                 player.flagcarried.angles = '0 0 0';
2374                 player.flagcarried.nodrawtoclient = NULL;
2375                 return true;
2376         }
2377 }
2378
2379 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2380 {
2381         entity player = M_ARGV(0, entity);
2382
2383         if(player.flagcarried)
2384         {
2385                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2386                 ctf_RespawnFlag(player.flagcarried);
2387                 return true;
2388         }
2389 }
2390
2391 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2392 {
2393         entity flag; // temporary entity for the search method
2394
2395         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2396         {
2397                 switch(flag.ctf_status)
2398                 {
2399                         case FLAG_DROPPED:
2400                         case FLAG_PASSING:
2401                         {
2402                                 // lock the flag, game is over
2403                                 set_movetype(flag, MOVETYPE_NONE);
2404                                 flag.takedamage = DAMAGE_NO;
2405                                 flag.solid = SOLID_NOT;
2406                                 flag.nextthink = false; // stop thinking
2407
2408                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2409                                 break;
2410                         }
2411
2412                         default:
2413                         case FLAG_BASE:
2414                         case FLAG_CARRY:
2415                         {
2416                                 // do nothing for these flags
2417                                 break;
2418                         }
2419                 }
2420         }
2421 }
2422
2423 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2424 {
2425         entity bot = M_ARGV(0, entity);
2426
2427         havocbot_ctf_reset_role(bot);
2428         return true;
2429 }
2430
2431 MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
2432 {
2433         //M_ARGV(0, float) = ctf_teams;
2434         M_ARGV(1, string) = "ctf_team";
2435         return true;
2436 }
2437
2438 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2439 {
2440         entity spectatee = M_ARGV(0, entity);
2441         entity client = M_ARGV(1, entity);
2442
2443         client.ctf_flagstatus = spectatee.ctf_flagstatus;
2444 }
2445
2446 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2447 {
2448         int record_page = M_ARGV(0, int);
2449         string ret_string = M_ARGV(1, string);
2450
2451         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2452         {
2453                 if (MapInfo_Get_ByID(i))
2454                 {
2455                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2456
2457                         if(!r)
2458                                 continue;
2459
2460                         // TODO: uid2name
2461                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2462                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2463                 }
2464         }
2465
2466         M_ARGV(1, string) = ret_string;
2467 }
2468
2469 bool superspec_Spectate(entity this, entity targ); // TODO
2470 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2471 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2472 {
2473         entity player = M_ARGV(0, entity);
2474         string cmd_name = M_ARGV(1, string);
2475         int cmd_argc = M_ARGV(2, int);
2476
2477         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2478
2479         if(cmd_name == "followfc")
2480         {
2481                 if(!g_ctf)
2482                         return true;
2483
2484                 int _team = 0;
2485                 bool found = false;
2486
2487                 if(cmd_argc == 2)
2488                 {
2489                         switch(argv(1))
2490                         {
2491                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2492                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2493                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2494                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2495                         }
2496                 }
2497
2498                 FOREACH_CLIENT(IS_PLAYER(it), {
2499                         if(it.flagcarried && (it.team == _team || _team == 0))
2500                         {
2501                                 found = true;
2502                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2503                                         continue; // already spectating this fc, try another
2504                                 return superspec_Spectate(player, it);
2505                         }
2506                 });
2507
2508                 if(!found)
2509                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2510                 return true;
2511         }
2512 }
2513
2514 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2515 {
2516         entity frag_target = M_ARGV(0, entity);
2517
2518         if(frag_target.flagcarried)
2519                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2520 }
2521
2522
2523 // ==========
2524 // Spawnfuncs
2525 // ==========
2526
2527 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2528 CTF flag for team one (Red).
2529 Keys:
2530 "angle" Angle the flag will point (minus 90 degrees)...
2531 "model" model to use, note this needs red and blue as skins 0 and 1...
2532 "noise" sound played when flag is picked up...
2533 "noise1" sound played when flag is returned by a teammate...
2534 "noise2" sound played when flag is captured...
2535 "noise3" sound played when flag is lost in the field and respawns itself...
2536 "noise4" sound played when flag is dropped by a player...
2537 "noise5" sound played when flag touches the ground... */
2538 spawnfunc(item_flag_team1)
2539 {
2540         if(!g_ctf) { delete(this); return; }
2541
2542         ctf_FlagSetup(NUM_TEAM_1, this);
2543 }
2544
2545 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2546 CTF flag for team two (Blue).
2547 Keys:
2548 "angle" Angle the flag will point (minus 90 degrees)...
2549 "model" model to use, note this needs red and blue as skins 0 and 1...
2550 "noise" sound played when flag is picked up...
2551 "noise1" sound played when flag is returned by a teammate...
2552 "noise2" sound played when flag is captured...
2553 "noise3" sound played when flag is lost in the field and respawns itself...
2554 "noise4" sound played when flag is dropped by a player...
2555 "noise5" sound played when flag touches the ground... */
2556 spawnfunc(item_flag_team2)
2557 {
2558         if(!g_ctf) { delete(this); return; }
2559
2560         ctf_FlagSetup(NUM_TEAM_2, this);
2561 }
2562
2563 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2564 CTF flag for team three (Yellow).
2565 Keys:
2566 "angle" Angle the flag will point (minus 90 degrees)...
2567 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2568 "noise" sound played when flag is picked up...
2569 "noise1" sound played when flag is returned by a teammate...
2570 "noise2" sound played when flag is captured...
2571 "noise3" sound played when flag is lost in the field and respawns itself...
2572 "noise4" sound played when flag is dropped by a player...
2573 "noise5" sound played when flag touches the ground... */
2574 spawnfunc(item_flag_team3)
2575 {
2576         if(!g_ctf) { delete(this); return; }
2577
2578         ctf_FlagSetup(NUM_TEAM_3, this);
2579 }
2580
2581 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2582 CTF flag for team four (Pink).
2583 Keys:
2584 "angle" Angle the flag will point (minus 90 degrees)...
2585 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2586 "noise" sound played when flag is picked up...
2587 "noise1" sound played when flag is returned by a teammate...
2588 "noise2" sound played when flag is captured...
2589 "noise3" sound played when flag is lost in the field and respawns itself...
2590 "noise4" sound played when flag is dropped by a player...
2591 "noise5" sound played when flag touches the ground... */
2592 spawnfunc(item_flag_team4)
2593 {
2594         if(!g_ctf) { delete(this); return; }
2595
2596         ctf_FlagSetup(NUM_TEAM_4, this);
2597 }
2598
2599 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2600 CTF flag (Neutral).
2601 Keys:
2602 "angle" Angle the flag will point (minus 90 degrees)...
2603 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2604 "noise" sound played when flag is picked up...
2605 "noise1" sound played when flag is returned by a teammate...
2606 "noise2" sound played when flag is captured...
2607 "noise3" sound played when flag is lost in the field and respawns itself...
2608 "noise4" sound played when flag is dropped by a player...
2609 "noise5" sound played when flag touches the ground... */
2610 spawnfunc(item_flag_neutral)
2611 {
2612         if(!g_ctf) { delete(this); return; }
2613         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2614
2615         ctf_FlagSetup(0, this);
2616 }
2617
2618 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2619 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2620 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.
2621 Keys:
2622 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2623 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2624 spawnfunc(ctf_team)
2625 {
2626         if(!g_ctf) { delete(this); return; }
2627
2628         this.classname = "ctf_team";
2629         this.team = this.cnt + 1;
2630 }
2631
2632 // compatibility for quake maps
2633 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2634 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2635 spawnfunc(info_player_team1);
2636 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2637 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2638 spawnfunc(info_player_team2);
2639 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2640 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2641
2642 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2643 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2644
2645 // compatibility for wop maps
2646 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2647 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2648 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2649 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2650 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2651 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2652
2653
2654 // ==============
2655 // Initialization
2656 // ==============
2657
2658 // scoreboard setup
2659 void ctf_ScoreRules(int teams)
2660 {
2661         CheckAllowedTeams(NULL);
2662         GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2663         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2664         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2665         field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2666         field(SP_CTF_PICKUPS, "pickups", 0);
2667         field(SP_CTF_FCKILLS, "fckills", 0);
2668         field(SP_CTF_RETURNS, "returns", 0);
2669         field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2670         });
2671 }
2672
2673 // code from here on is just to support maps that don't have flag and team entities
2674 void ctf_SpawnTeam (string teamname, int teamcolor)
2675 {
2676         entity this = new_pure(ctf_team);
2677         this.netname = teamname;
2678         this.cnt = teamcolor - 1;
2679         this.spawnfunc_checked = true;
2680         this.team = teamcolor;
2681 }
2682
2683 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2684 {
2685         ctf_teams = 0;
2686
2687         entity tmp_entity;
2688         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2689         {
2690                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2691                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2692
2693                 switch(tmp_entity.team)
2694                 {
2695                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2696                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2697                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2698                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2699                 }
2700                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2701         }
2702
2703         havocbot_ctf_calculate_middlepoint();
2704
2705         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2706         {
2707                 ctf_teams = 0; // so set the default red and blue teams
2708                 BITSET_ASSIGN(ctf_teams, BIT(0));
2709                 BITSET_ASSIGN(ctf_teams, BIT(1));
2710         }
2711
2712         //ctf_teams = bound(2, ctf_teams, 4);
2713
2714         // if no teams are found, spawn defaults
2715         if(find(NULL, classname, "ctf_team") == NULL)
2716         {
2717                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2718                 if(ctf_teams & BIT(0))
2719                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2720                 if(ctf_teams & BIT(1))
2721                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2722                 if(ctf_teams & BIT(2))
2723                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2724                 if(ctf_teams & BIT(3))
2725                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2726         }
2727
2728         ctf_ScoreRules(ctf_teams);
2729 }
2730
2731 void ctf_Initialize()
2732 {
2733         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2734
2735         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2736         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2737         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2738
2739         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2740 }