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