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