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