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