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