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