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