]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
Merge branch 'martin-t/settings' into 'master'
[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_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_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_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_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_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)); // would be desired, except maps that want floating flags have it set to fall!
1166         set_movetype(flag, MOVETYPE_NONE); // match the initial setup handling (flag doesn't move when spawned)
1167         flag.takedamage = DAMAGE_NO;
1168         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1169         flag.solid = SOLID_TRIGGER;
1170         flag.velocity = '0 0 0';
1171         flag.angles = flag.mangle;
1172         flag.flags = FL_ITEM | FL_NOTARGET;
1173
1174         flag.ctf_status = FLAG_BASE;
1175         flag.owner = NULL;
1176         flag.pass_distance = 0;
1177         flag.pass_sender = NULL;
1178         flag.pass_target = NULL;
1179         flag.ctf_dropper = NULL;
1180         flag.ctf_pickuptime = 0;
1181         flag.ctf_droptime = 0;
1182         flag.ctf_flagdamaged_byworld = false;
1183         navigation_dynamicgoal_unset(flag);
1184
1185         ctf_CheckStalemate();
1186 }
1187
1188 void ctf_Reset(entity this)
1189 {
1190         if(this.owner && IS_PLAYER(this.owner))
1191                 ctf_Handle_Throw(this.owner, NULL, DROP_RESET);
1192
1193         this.enemy = NULL;
1194         ctf_RespawnFlag(this);
1195 }
1196
1197 bool ctf_FlagBase_Customize(entity this, entity client)
1198 {
1199         entity e = WaypointSprite_getviewentity(client);
1200         entity wp_owner = this.owner;
1201         entity flag = e.flagcarried;
1202         if(flag && CTF_SAMETEAM(e, flag))
1203                 return false;
1204         if(flag && (flag.cnt || wp_owner.cnt) && wp_owner.cnt != flag.cnt)
1205                 return false;
1206         return true;
1207 }
1208
1209 void ctf_DelayedFlagSetup(entity this) // called after a flag is placed on a map by ctf_FlagSetup()
1210 {
1211         // bot waypoints
1212         waypoint_spawnforitem_force(this, this.origin);
1213         navigation_dynamicgoal_init(this, true);
1214
1215         // waypointsprites
1216         entity basename;
1217         switch (this.team)
1218         {
1219                 case NUM_TEAM_1: basename = WP_FlagBaseRed; break;
1220                 case NUM_TEAM_2: basename = WP_FlagBaseBlue; break;
1221                 case NUM_TEAM_3: basename = WP_FlagBaseYellow; break;
1222                 case NUM_TEAM_4: basename = WP_FlagBasePink; break;
1223                 default: basename = WP_FlagBaseNeutral; break;
1224         }
1225
1226         entity wp = WaypointSprite_SpawnFixed(basename, this.origin + FLAG_WAYPOINT_OFFSET, this, wps_flagbase, RADARICON_FLAG);
1227         wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 1 1');
1228         WaypointSprite_UpdateTeamRadar(this.wps_flagbase, RADARICON_FLAG, ((this.team) ? colormapPaletteColor(this.team - 1, false) : '1 1 1'));
1229         setcefc(wp, ctf_FlagBase_Customize);
1230
1231         // captureshield setup
1232         ctf_CaptureShield_Spawn(this);
1233 }
1234
1235 .bool pushable;
1236
1237 void ctf_FlagSetup(int teamnumber, entity flag) // called when spawning a flag entity on the map as a spawnfunc
1238 {
1239         // main setup
1240         flag.ctf_worldflagnext = ctf_worldflaglist; // link flag into ctf_worldflaglist
1241         ctf_worldflaglist = flag;
1242
1243         setattachment(flag, NULL, "");
1244
1245         flag.netname = strzone(sprintf("%s%s^7 flag", Team_ColorCode(teamnumber), Team_ColorName_Upper(teamnumber)));
1246         flag.team = teamnumber;
1247         flag.classname = "item_flag_team";
1248         flag.target = "###item###"; // for finding the nearest item using findnearest
1249         flag.flags = FL_ITEM | FL_NOTARGET;
1250         IL_PUSH(g_items, flag);
1251         flag.solid = SOLID_TRIGGER;
1252         flag.takedamage = DAMAGE_NO;
1253         flag.damageforcescale = autocvar_g_ctf_flag_damageforcescale;
1254         flag.max_health = ((autocvar_g_ctf_flag_return_damage && autocvar_g_ctf_flag_health) ? autocvar_g_ctf_flag_health : 100);
1255         SetResourceExplicit(flag, RES_HEALTH, flag.max_health);
1256         flag.event_damage = ctf_FlagDamage;
1257         flag.pushable = true;
1258         flag.teleportable = TELEPORT_NORMAL;
1259         flag.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
1260         flag.damagedbytriggers = autocvar_g_ctf_flag_return_when_unreachable;
1261         flag.damagedbycontents = autocvar_g_ctf_flag_return_when_unreachable;
1262         if(flag.damagedbycontents)
1263                 IL_PUSH(g_damagedbycontents, flag);
1264         flag.velocity = '0 0 0';
1265         flag.mangle = flag.angles;
1266         flag.reset = ctf_Reset;
1267         settouch(flag, ctf_FlagTouch);
1268         setthink(flag, ctf_FlagThink);
1269         flag.nextthink = time + FLAG_THINKRATE;
1270         flag.ctf_status = FLAG_BASE;
1271
1272         // crudely force them all to 0
1273         if(autocvar_g_ctf_score_ignore_fields)
1274                 flag.cnt = flag.score_assist = flag.score_team_capture = flag.score_capture = flag.score_drop = flag.score_pickup = flag.score_return = 0;
1275
1276         string teamname = Static_Team_ColorName_Lower(teamnumber);
1277         // appearence
1278         if(!flag.scale)                         { flag.scale = FLAG_SCALE; }
1279         if(flag.skin == 0)                      { flag.skin = cvar(sprintf("g_ctf_flag_%s_skin", teamname)); }
1280         if(flag.model == "")            { flag.model = cvar_string(sprintf("g_ctf_flag_%s_model", teamname)); }
1281         if (flag.toucheffect == "") { flag.toucheffect = EFFECT_FLAG_TOUCH(teamnumber).eent_eff_name; }
1282         if (flag.passeffect == "")      { flag.passeffect = EFFECT_PASS(teamnumber).eent_eff_name; }
1283         if (flag.capeffect == "")       { flag.capeffect = EFFECT_CAP(teamnumber).eent_eff_name; }
1284
1285         // sounds
1286 #define X(s,b) \
1287                 if(flag.s == "") flag.s = b; \
1288                 precache_sound(flag.s);
1289
1290         X(snd_flag_taken,               strzone(SND(CTF_TAKEN(teamnumber))))
1291         X(snd_flag_returned,    strzone(SND(CTF_RETURNED(teamnumber))))
1292         X(snd_flag_capture,     strzone(SND(CTF_CAPTURE(teamnumber))))
1293         X(snd_flag_dropped,     strzone(SND(CTF_DROPPED(teamnumber))))
1294         X(snd_flag_respawn,     strzone(SND(CTF_RESPAWN)))
1295         X(snd_flag_touch,               strzone(SND(CTF_TOUCH)))
1296         X(snd_flag_pass,                strzone(SND(CTF_PASS)))
1297 #undef X
1298
1299         // precache
1300         precache_model(flag.model);
1301
1302         // appearence
1303         _setmodel(flag, flag.model); // precision set below
1304         setsize(flag, CTF_FLAG.m_mins * flag.scale, CTF_FLAG.m_maxs * flag.scale);
1305         flag.m_mins = flag.mins; // store these for squash checks
1306         flag.m_maxs = flag.maxs;
1307         setorigin(flag, (flag.origin + FLAG_SPAWN_OFFSET));
1308
1309         if(autocvar_g_ctf_flag_glowtrails)
1310         {
1311                 switch(teamnumber)
1312                 {
1313                         case NUM_TEAM_1: flag.glow_color = 251; break;
1314                         case NUM_TEAM_2: flag.glow_color = 210; break;
1315                         case NUM_TEAM_3: flag.glow_color = 110; break;
1316                         case NUM_TEAM_4: flag.glow_color = 145; break;
1317                         default:                 flag.glow_color = 254; break;
1318                 }
1319                 flag.glow_size = 25;
1320                 flag.glow_trail = 1;
1321         }
1322
1323         flag.effects |= EF_LOWPRECISION;
1324         if(autocvar_g_ctf_fullbrightflags) { flag.effects |= EF_FULLBRIGHT; }
1325         if(autocvar_g_ctf_dynamiclights)
1326         {
1327                 switch(teamnumber)
1328                 {
1329                         case NUM_TEAM_1: flag.effects |= EF_RED; break;
1330                         case NUM_TEAM_2: flag.effects |= EF_BLUE; break;
1331                         case NUM_TEAM_3: flag.effects |= EF_DIMLIGHT; break;
1332                         case NUM_TEAM_4: flag.effects |= EF_RED; break;
1333                         default:                 flag.effects |= EF_DIMLIGHT; break;
1334                 }
1335         }
1336
1337         // flag placement
1338         if((flag.spawnflags & 1) || flag.noalign) // don't drop to floor, just stay at fixed location
1339         {
1340                 flag.dropped_origin = flag.origin;
1341                 flag.noalign = true;
1342                 set_movetype(flag, MOVETYPE_NONE);
1343         }
1344         else // drop to floor, automatically find a platform and set that as spawn origin
1345         {
1346                 flag.noalign = false;
1347                 droptofloor(flag);
1348                 set_movetype(flag, MOVETYPE_NONE);
1349         }
1350
1351         InitializeEntity(flag, ctf_DelayedFlagSetup, INITPRIO_SETLOCATION);
1352 }
1353
1354
1355 // ================
1356 // Bot player logic
1357 // ================
1358
1359 // NOTE: LEGACY CODE, needs to be re-written!
1360
1361 void havocbot_ctf_calculate_middlepoint()
1362 {
1363         entity f;
1364         vector s = '0 0 0';
1365         vector fo = '0 0 0';
1366         int n = 0;
1367
1368         f = ctf_worldflaglist;
1369         while (f)
1370         {
1371                 fo = f.origin;
1372                 s = s + fo;
1373                 f = f.ctf_worldflagnext;
1374                 n++;
1375         }
1376         if(!n)
1377                 return;
1378
1379         havocbot_middlepoint = s / n;
1380         havocbot_middlepoint_radius = vlen(fo - havocbot_middlepoint);
1381
1382         havocbot_symmetry_axis_m = 0;
1383         havocbot_symmetry_axis_q = 0;
1384         if(n == 2)
1385         {
1386                 // for symmetrical editing of waypoints
1387                 entity f1 = ctf_worldflaglist;
1388                 entity f2 = f1.ctf_worldflagnext;
1389                 float m = -(f1.origin.y - f2.origin.y) / (f1.origin.x - f2.origin.x);
1390                 float q = havocbot_middlepoint.y - m * havocbot_middlepoint.x;
1391                 havocbot_symmetry_axis_m = m;
1392                 havocbot_symmetry_axis_q = q;
1393         }
1394         havocbot_symmetry_origin_order = n;
1395 }
1396
1397
1398 entity havocbot_ctf_find_flag(entity bot)
1399 {
1400         entity f;
1401         f = ctf_worldflaglist;
1402         while (f)
1403         {
1404                 if (CTF_SAMETEAM(bot, f))
1405                         return f;
1406                 f = f.ctf_worldflagnext;
1407         }
1408         return NULL;
1409 }
1410
1411 entity havocbot_ctf_find_enemy_flag(entity bot)
1412 {
1413         entity f;
1414         f = ctf_worldflaglist;
1415         while (f)
1416         {
1417                 if(ctf_oneflag)
1418                 {
1419                         if(CTF_DIFFTEAM(bot, f))
1420                         {
1421                                 if(f.team)
1422                                 {
1423                                         if(bot.flagcarried)
1424                                                 return f;
1425                                 }
1426                                 else if(!bot.flagcarried)
1427                                         return f;
1428                         }
1429                 }
1430                 else if (CTF_DIFFTEAM(bot, f))
1431                         return f;
1432                 f = f.ctf_worldflagnext;
1433         }
1434         return NULL;
1435 }
1436
1437 int havocbot_ctf_teamcount(entity bot, vector org, float tc_radius)
1438 {
1439         if (!teamplay)
1440                 return 0;
1441
1442         int c = 0;
1443
1444         FOREACH_CLIENT(IS_PLAYER(it), {
1445                 if(DIFF_TEAM(it, bot) || IS_DEAD(it) || it == bot)
1446                         continue;
1447
1448                 if(vdist(it.origin - org, <, tc_radius))
1449                         ++c;
1450         });
1451
1452         return c;
1453 }
1454
1455 // unused
1456 #if 0
1457 void havocbot_goalrating_ctf_ourflag(entity this, float ratingscale)
1458 {
1459         entity head;
1460         head = ctf_worldflaglist;
1461         while (head)
1462         {
1463                 if (CTF_SAMETEAM(this, head))
1464                         break;
1465                 head = head.ctf_worldflagnext;
1466         }
1467         if (head)
1468                 navigation_routerating(this, head, ratingscale, 10000);
1469 }
1470 #endif
1471
1472 void havocbot_goalrating_ctf_ourbase(entity this, float ratingscale)
1473 {
1474         entity head;
1475         head = ctf_worldflaglist;
1476         while (head)
1477         {
1478                 if (CTF_SAMETEAM(this, head))
1479                 {
1480                         if (this.flagcarried)
1481                         if ((this.flagcarried.cnt || head.cnt) && this.flagcarried.cnt != head.cnt)
1482                         {
1483                                 head = head.ctf_worldflagnext; // skip base if it has a different group
1484                                 continue;
1485                         }
1486                         break;
1487                 }
1488                 head = head.ctf_worldflagnext;
1489         }
1490         if (!head)
1491                 return;
1492
1493         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1494 }
1495
1496 void havocbot_goalrating_ctf_enemyflag(entity this, float ratingscale)
1497 {
1498         entity head;
1499         head = ctf_worldflaglist;
1500         while (head)
1501         {
1502                 if(ctf_oneflag)
1503                 {
1504                         if(CTF_DIFFTEAM(this, head))
1505                         {
1506                                 if(head.team)
1507                                 {
1508                                         if(this.flagcarried)
1509                                                 break;
1510                                 }
1511                                 else if(!this.flagcarried)
1512                                         break;
1513                         }
1514                 }
1515                 else if(CTF_DIFFTEAM(this, head))
1516                         break;
1517                 head = head.ctf_worldflagnext;
1518         }
1519         if (head)
1520         {
1521                 if (head.ctf_status == FLAG_CARRY)
1522                 {
1523                         // adjust rating of our flag carrier depending on his health
1524                         head = head.tag_entity;
1525                         float f = bound(0, (GetResource(head, RES_HEALTH) + GetResource(head, RES_ARMOR)) / 100, 2) - 1;
1526                         ratingscale += ratingscale * f * 0.1;
1527                 }
1528                 navigation_routerating(this, head, ratingscale, 10000);
1529         }
1530 }
1531
1532 void havocbot_goalrating_ctf_enemybase(entity this, float ratingscale)
1533 {
1534         // disabled because we always spawn waypoints for flags with waypoint_spawnforitem_force
1535         /*
1536         if (!bot_waypoints_for_items)
1537         {
1538                 havocbot_goalrating_ctf_enemyflag(this, ratingscale);
1539                 return;
1540         }
1541         */
1542         entity head;
1543
1544         head = havocbot_ctf_find_enemy_flag(this);
1545
1546         if (!head)
1547                 return;
1548
1549         navigation_routerating(this, head.bot_basewaypoint, ratingscale, 10000);
1550 }
1551
1552 void havocbot_goalrating_ctf_ourstolenflag(entity this, float ratingscale)
1553 {
1554         entity mf;
1555
1556         mf = havocbot_ctf_find_flag(this);
1557
1558         if(mf.ctf_status == FLAG_BASE)
1559                 return;
1560
1561         if(mf.tag_entity)
1562                 navigation_routerating(this, mf.tag_entity, ratingscale, 10000);
1563 }
1564
1565 void havocbot_goalrating_ctf_droppedflags(entity this, float ratingscale, vector org, float df_radius)
1566 {
1567         entity head;
1568         head = ctf_worldflaglist;
1569         while (head)
1570         {
1571                 // flag is out in the field
1572                 if(head.ctf_status != FLAG_BASE)
1573                 if(head.tag_entity==NULL)       // dropped
1574                 {
1575                         if(df_radius)
1576                         {
1577                                 if(vdist(org - head.origin, <, df_radius))
1578                                         navigation_routerating(this, head, ratingscale, 10000);
1579                         }
1580                         else
1581                                 navigation_routerating(this, head, ratingscale, 10000);
1582                 }
1583
1584                 head = head.ctf_worldflagnext;
1585         }
1586 }
1587
1588 void havocbot_ctf_reset_role(entity this)
1589 {
1590         float cdefense, cmiddle, coffense;
1591         entity mf, ef;
1592
1593         if(IS_DEAD(this))
1594                 return;
1595
1596         // Check ctf flags
1597         if (this.flagcarried)
1598         {
1599                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1600                 return;
1601         }
1602
1603         mf = havocbot_ctf_find_flag(this);
1604         ef = havocbot_ctf_find_enemy_flag(this);
1605
1606         // Retrieve stolen flag
1607         if(mf.ctf_status!=FLAG_BASE)
1608         {
1609                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1610                 return;
1611         }
1612
1613         // If enemy flag is taken go to the middle to intercept pursuers
1614         if(ef.ctf_status!=FLAG_BASE)
1615         {
1616                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1617                 return;
1618         }
1619
1620         // if there is no one else on the team switch to offense
1621         int count = 0;
1622         // don't check if this bot is a player since it isn't true when the bot is added to the server
1623         FOREACH_CLIENT(it != this && IS_PLAYER(it) && SAME_TEAM(it, this), { ++count; });
1624
1625         if (count == 0)
1626         {
1627                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1628                 return;
1629         }
1630         else if (time < CS(this).jointime + 1)
1631         {
1632                 // if bots spawn all at once set good default roles
1633                 if (count == 1)
1634                 {
1635                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1636                         return;
1637                 }
1638                 else if (count == 2)
1639                 {
1640                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1641                         return;
1642                 }
1643         }
1644
1645         // Evaluate best position to take
1646         // Count mates on middle position
1647         cmiddle = havocbot_ctf_teamcount(this, havocbot_middlepoint, havocbot_middlepoint_radius * 0.5);
1648
1649         // Count mates on defense position
1650         cdefense = havocbot_ctf_teamcount(this, mf.dropped_origin, havocbot_middlepoint_radius * 0.5);
1651
1652         // Count mates on offense position
1653         coffense = havocbot_ctf_teamcount(this, ef.dropped_origin, havocbot_middlepoint_radius);
1654
1655         if(cdefense<=coffense)
1656                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1657         else if(coffense<=cmiddle)
1658                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_OFFENSE);
1659         else
1660                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_MIDDLE);
1661
1662         // if bots spawn all at once assign them a more appropriated role after a while
1663         if (time < CS(this).jointime + 1 && count > 2)
1664                 this.havocbot_role_timeout = time + 10 + random() * 10;
1665 }
1666
1667 bool havocbot_ctf_is_basewaypoint(entity item)
1668 {
1669         if (item.classname != "waypoint")
1670                 return false;
1671
1672         entity head = ctf_worldflaglist;
1673         while (head)
1674         {
1675                 if (item == head.bot_basewaypoint)
1676                         return true;
1677                 head = head.ctf_worldflagnext;
1678         }
1679         return false;
1680 }
1681
1682 void havocbot_role_ctf_carrier(entity this)
1683 {
1684         if(IS_DEAD(this))
1685         {
1686                 havocbot_ctf_reset_role(this);
1687                 return;
1688         }
1689
1690         if (this.flagcarried == NULL)
1691         {
1692                 havocbot_ctf_reset_role(this);
1693                 return;
1694         }
1695
1696         if (navigation_goalrating_timeout(this))
1697         {
1698                 navigation_goalrating_start(this);
1699
1700                 // role: carrier
1701                 entity mf = havocbot_ctf_find_flag(this);
1702                 vector base_org = mf.dropped_origin;
1703                 float base_rating = (mf.ctf_status == FLAG_BASE) ? 10000 : (vdist(this.origin - base_org, >, 100) ? 2000 : 1000);
1704                 if(ctf_oneflag)
1705                         havocbot_goalrating_ctf_enemybase(this, base_rating);
1706                 else
1707                         havocbot_goalrating_ctf_ourbase(this, base_rating);
1708
1709                 // start collecting items very close to the bot but only inside of own base radius
1710                 if (vdist(this.origin - base_org, <, havocbot_middlepoint_radius))
1711                         havocbot_goalrating_items(this, 15000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1712
1713                 havocbot_goalrating_items(this, 10000, base_org, havocbot_middlepoint_radius * 0.5);
1714
1715                 navigation_goalrating_end(this);
1716
1717                 navigation_goalrating_timeout_set(this);
1718
1719                 entity goal = this.goalentity;
1720                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1721                         this.goalentity_lock_timeout = time + ((this.bot_aimtarg) ? 2 : 3);
1722
1723                 if (goal)
1724                         this.havocbot_cantfindflag = time + 10;
1725                 else if (time > this.havocbot_cantfindflag)
1726                 {
1727                         // Can't navigate to my own base, suicide!
1728                         // TODO: drop it and wander around
1729                         Damage(this, this, this, 100000, DEATH_KILL.m_id, DMG_NOWEP, this.origin, '0 0 0');
1730                         return;
1731                 }
1732         }
1733 }
1734
1735 void havocbot_role_ctf_escort(entity this)
1736 {
1737         entity mf, ef;
1738
1739         if(IS_DEAD(this))
1740         {
1741                 havocbot_ctf_reset_role(this);
1742                 return;
1743         }
1744
1745         if (this.flagcarried)
1746         {
1747                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1748                 return;
1749         }
1750
1751         // If enemy flag is back on the base switch to previous role
1752         ef = havocbot_ctf_find_enemy_flag(this);
1753         if(ef.ctf_status==FLAG_BASE)
1754         {
1755                 this.havocbot_role = this.havocbot_previous_role;
1756                 this.havocbot_role_timeout = 0;
1757                 return;
1758         }
1759         if (ef.ctf_status == FLAG_DROPPED)
1760         {
1761                 navigation_goalrating_timeout_expire(this, 1);
1762                 return;
1763         }
1764
1765         // If the flag carrier reached the base switch to defense
1766         mf = havocbot_ctf_find_flag(this);
1767         if (mf.ctf_status != FLAG_BASE && vdist(ef.origin - mf.dropped_origin, <, 900))
1768         {
1769                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_DEFENSE);
1770                 return;
1771         }
1772
1773         // Set the role timeout if necessary
1774         if (!this.havocbot_role_timeout)
1775         {
1776                 this.havocbot_role_timeout = time + random() * 30 + 60;
1777         }
1778
1779         // If nothing happened just switch to previous role
1780         if (time > this.havocbot_role_timeout)
1781         {
1782                 this.havocbot_role = this.havocbot_previous_role;
1783                 this.havocbot_role_timeout = 0;
1784                 return;
1785         }
1786
1787         // Chase the flag carrier
1788         if (navigation_goalrating_timeout(this))
1789         {
1790                 navigation_goalrating_start(this);
1791
1792                 // role: escort
1793                 havocbot_goalrating_ctf_enemyflag(this, 10000);
1794                 havocbot_goalrating_ctf_ourstolenflag(this, 6000);
1795                 havocbot_goalrating_items(this, 21000, this.origin, 10000);
1796
1797                 navigation_goalrating_end(this);
1798
1799                 navigation_goalrating_timeout_set(this);
1800         }
1801 }
1802
1803 void havocbot_role_ctf_offense(entity this)
1804 {
1805         entity mf, ef;
1806         vector pos;
1807
1808         if(IS_DEAD(this))
1809         {
1810                 havocbot_ctf_reset_role(this);
1811                 return;
1812         }
1813
1814         if (this.flagcarried)
1815         {
1816                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1817                 return;
1818         }
1819
1820         // Check flags
1821         mf = havocbot_ctf_find_flag(this);
1822         ef = havocbot_ctf_find_enemy_flag(this);
1823
1824         // Own flag stolen
1825         if(mf.ctf_status!=FLAG_BASE)
1826         {
1827                 if(mf.tag_entity)
1828                         pos = mf.tag_entity.origin;
1829                 else
1830                         pos = mf.origin;
1831
1832                 // Try to get it if closer than the enemy base
1833                 if(vlen2(this.origin-ef.dropped_origin)>vlen2(this.origin-pos))
1834                 {
1835                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1836                         return;
1837                 }
1838         }
1839
1840         // Escort flag carrier
1841         if(ef.ctf_status!=FLAG_BASE)
1842         {
1843                 if(ef.tag_entity)
1844                         pos = ef.tag_entity.origin;
1845                 else
1846                         pos = ef.origin;
1847
1848                 if(vdist(pos - mf.dropped_origin, >, 700))
1849                 {
1850                         havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_ESCORT);
1851                         return;
1852                 }
1853         }
1854
1855         // Set the role timeout if necessary
1856         if (!this.havocbot_role_timeout)
1857                 this.havocbot_role_timeout = time + 120;
1858
1859         if (time > this.havocbot_role_timeout)
1860         {
1861                 havocbot_ctf_reset_role(this);
1862                 return;
1863         }
1864
1865         if (navigation_goalrating_timeout(this))
1866         {
1867                 navigation_goalrating_start(this);
1868
1869                 // role: offense
1870                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1871                 havocbot_goalrating_ctf_enemybase(this, 10000);
1872                 havocbot_goalrating_items(this, 22000, this.origin, 10000);
1873
1874                 navigation_goalrating_end(this);
1875
1876                 navigation_goalrating_timeout_set(this);
1877         }
1878 }
1879
1880 // Retriever (temporary role):
1881 void havocbot_role_ctf_retriever(entity this)
1882 {
1883         entity mf;
1884
1885         if(IS_DEAD(this))
1886         {
1887                 havocbot_ctf_reset_role(this);
1888                 return;
1889         }
1890
1891         if (this.flagcarried)
1892         {
1893                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1894                 return;
1895         }
1896
1897         // If flag is back on the base switch to previous role
1898         mf = havocbot_ctf_find_flag(this);
1899         if(mf.ctf_status==FLAG_BASE)
1900         {
1901                 if (mf.enemy == this) // did this bot return the flag?
1902                         navigation_goalrating_timeout_force(this);
1903                 havocbot_ctf_reset_role(this);
1904                 return;
1905         }
1906
1907         if (!this.havocbot_role_timeout)
1908                 this.havocbot_role_timeout = time + 20;
1909
1910         if (time > this.havocbot_role_timeout)
1911         {
1912                 havocbot_ctf_reset_role(this);
1913                 return;
1914         }
1915
1916         if (navigation_goalrating_timeout(this))
1917         {
1918                 const float RT_RADIUS = 10000;
1919
1920                 navigation_goalrating_start(this);
1921
1922                 // role: retriever
1923                 havocbot_goalrating_ctf_ourstolenflag(this, 10000);
1924                 havocbot_goalrating_ctf_droppedflags(this, 12000, this.origin, RT_RADIUS);
1925                 havocbot_goalrating_ctf_enemybase(this, 8000);
1926                 entity ef = havocbot_ctf_find_enemy_flag(this);
1927                 vector enemy_base_org = ef.dropped_origin;
1928                 // start collecting items very close to the bot but only inside of enemy base radius
1929                 if (vdist(this.origin - enemy_base_org, <, havocbot_middlepoint_radius))
1930                         havocbot_goalrating_items(this, 27000, this.origin, min(500, havocbot_middlepoint_radius * 0.5));
1931                 havocbot_goalrating_items(this, 18000, this.origin, havocbot_middlepoint_radius);
1932
1933                 navigation_goalrating_end(this);
1934
1935                 navigation_goalrating_timeout_set(this);
1936         }
1937 }
1938
1939 void havocbot_role_ctf_middle(entity this)
1940 {
1941         entity mf;
1942
1943         if(IS_DEAD(this))
1944         {
1945                 havocbot_ctf_reset_role(this);
1946                 return;
1947         }
1948
1949         if (this.flagcarried)
1950         {
1951                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
1952                 return;
1953         }
1954
1955         mf = havocbot_ctf_find_flag(this);
1956         if(mf.ctf_status!=FLAG_BASE)
1957         {
1958                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
1959                 return;
1960         }
1961
1962         if (!this.havocbot_role_timeout)
1963                 this.havocbot_role_timeout = time + 10;
1964
1965         if (time > this.havocbot_role_timeout)
1966         {
1967                 havocbot_ctf_reset_role(this);
1968                 return;
1969         }
1970
1971         if (navigation_goalrating_timeout(this))
1972         {
1973                 vector org;
1974
1975                 org = havocbot_middlepoint;
1976                 org.z = this.origin.z;
1977
1978                 navigation_goalrating_start(this);
1979
1980                 // role: middle
1981                 havocbot_goalrating_ctf_ourstolenflag(this, 8000);
1982                 havocbot_goalrating_ctf_droppedflags(this, 9000, this.origin, 10000);
1983                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1984                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius * 0.5);
1985                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
1986                 havocbot_goalrating_ctf_enemybase(this, 3000);
1987
1988                 navigation_goalrating_end(this);
1989
1990                 entity goal = this.goalentity;
1991                 if (havocbot_ctf_is_basewaypoint(goal) && vdist(goal.origin - this.origin, <, 100))
1992                         this.goalentity_lock_timeout = time + 2;
1993
1994                 navigation_goalrating_timeout_set(this);
1995         }
1996 }
1997
1998 void havocbot_role_ctf_defense(entity this)
1999 {
2000         entity mf;
2001
2002         if(IS_DEAD(this))
2003         {
2004                 havocbot_ctf_reset_role(this);
2005                 return;
2006         }
2007
2008         if (this.flagcarried)
2009         {
2010                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_CARRIER);
2011                 return;
2012         }
2013
2014         // If own flag was captured
2015         mf = havocbot_ctf_find_flag(this);
2016         if(mf.ctf_status!=FLAG_BASE)
2017         {
2018                 havocbot_role_ctf_setrole(this, HAVOCBOT_CTF_ROLE_RETRIEVER);
2019                 return;
2020         }
2021
2022         if (!this.havocbot_role_timeout)
2023                 this.havocbot_role_timeout = time + 30;
2024
2025         if (time > this.havocbot_role_timeout)
2026         {
2027                 havocbot_ctf_reset_role(this);
2028                 return;
2029         }
2030         if (navigation_goalrating_timeout(this))
2031         {
2032                 vector org = mf.dropped_origin;
2033
2034                 navigation_goalrating_start(this);
2035
2036                 // if enemies are closer to our base, go there
2037                 entity closestplayer = NULL;
2038                 float distance, bestdistance = 10000;
2039                 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
2040                         distance = vlen(org - it.origin);
2041                         if(distance<bestdistance)
2042                         {
2043                                 closestplayer = it;
2044                                 bestdistance = distance;
2045                         }
2046                 });
2047
2048                 // role: defense
2049                 if(closestplayer)
2050                 if(DIFF_TEAM(closestplayer, this))
2051                 if(vdist(org - this.origin, >, 1000))
2052                 if(checkpvs(this.origin,closestplayer)||random()<0.5)
2053                         havocbot_goalrating_ctf_ourbase(this, 10000);
2054
2055                 havocbot_goalrating_ctf_ourstolenflag(this, 5000);
2056                 havocbot_goalrating_ctf_droppedflags(this, 6000, org, havocbot_middlepoint_radius);
2057                 havocbot_goalrating_enemyplayers(this, 25000, org, havocbot_middlepoint_radius);
2058                 havocbot_goalrating_items(this, 25000, org, havocbot_middlepoint_radius);
2059                 havocbot_goalrating_items(this, 18000, this.origin, 10000);
2060
2061                 navigation_goalrating_end(this);
2062
2063                 navigation_goalrating_timeout_set(this);
2064         }
2065 }
2066
2067 void havocbot_role_ctf_setrole(entity bot, int role)
2068 {
2069         string s = "(null)";
2070         switch(role)
2071         {
2072                 case HAVOCBOT_CTF_ROLE_CARRIER:
2073                         s = "carrier";
2074                         bot.havocbot_role = havocbot_role_ctf_carrier;
2075                         bot.havocbot_role_timeout = 0;
2076                         bot.havocbot_cantfindflag = time + 10;
2077                         if (bot.havocbot_previous_role != bot.havocbot_role)
2078                                 navigation_goalrating_timeout_force(bot);
2079                         break;
2080                 case HAVOCBOT_CTF_ROLE_DEFENSE:
2081                         s = "defense";
2082                         bot.havocbot_role = havocbot_role_ctf_defense;
2083                         bot.havocbot_role_timeout = 0;
2084                         break;
2085                 case HAVOCBOT_CTF_ROLE_MIDDLE:
2086                         s = "middle";
2087                         bot.havocbot_role = havocbot_role_ctf_middle;
2088                         bot.havocbot_role_timeout = 0;
2089                         break;
2090                 case HAVOCBOT_CTF_ROLE_OFFENSE:
2091                         s = "offense";
2092                         bot.havocbot_role = havocbot_role_ctf_offense;
2093                         bot.havocbot_role_timeout = 0;
2094                         break;
2095                 case HAVOCBOT_CTF_ROLE_RETRIEVER:
2096                         s = "retriever";
2097                         bot.havocbot_previous_role = bot.havocbot_role;
2098                         bot.havocbot_role = havocbot_role_ctf_retriever;
2099                         bot.havocbot_role_timeout = time + 10;
2100                         if (bot.havocbot_previous_role != bot.havocbot_role)
2101                                 navigation_goalrating_timeout_expire(bot, 2);
2102                         break;
2103                 case HAVOCBOT_CTF_ROLE_ESCORT:
2104                         s = "escort";
2105                         bot.havocbot_previous_role = bot.havocbot_role;
2106                         bot.havocbot_role = havocbot_role_ctf_escort;
2107                         bot.havocbot_role_timeout = time + 30;
2108                         if (bot.havocbot_previous_role != bot.havocbot_role)
2109                                 navigation_goalrating_timeout_expire(bot, 2);
2110                         break;
2111         }
2112         LOG_TRACE(bot.netname, " switched to ", s);
2113 }
2114
2115
2116 // ==============
2117 // Hook Functions
2118 // ==============
2119
2120 MUTATOR_HOOKFUNCTION(ctf, PlayerPreThink)
2121 {
2122         entity player = M_ARGV(0, entity);
2123
2124         int t = 0, t2 = 0, t3 = 0;
2125         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)
2126
2127         // initially clear items so they can be set as necessary later.
2128         STAT(CTF_FLAGSTATUS, player) &= ~(CTF_RED_FLAG_CARRYING         | CTF_RED_FLAG_TAKEN            | CTF_RED_FLAG_LOST
2129                                                    | CTF_BLUE_FLAG_CARRYING             | CTF_BLUE_FLAG_TAKEN           | CTF_BLUE_FLAG_LOST
2130                                                    | CTF_YELLOW_FLAG_CARRYING   | CTF_YELLOW_FLAG_TAKEN         | CTF_YELLOW_FLAG_LOST
2131                                                    | CTF_PINK_FLAG_CARRYING     | CTF_PINK_FLAG_TAKEN           | CTF_PINK_FLAG_LOST
2132                                                    | CTF_NEUTRAL_FLAG_CARRYING  | CTF_NEUTRAL_FLAG_TAKEN        | CTF_NEUTRAL_FLAG_LOST
2133                                                    | CTF_FLAG_NEUTRAL | CTF_SHIELDED | CTF_STALEMATE);
2134
2135         // scan through all the flags and notify the client about them
2136         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2137         {
2138                 if(flag.team == NUM_TEAM_1 && !b1) { b1 = true; t = CTF_RED_FLAG_CARRYING;              t2 = CTF_RED_FLAG_TAKEN;                t3 = CTF_RED_FLAG_LOST; }
2139                 if(flag.team == NUM_TEAM_2 && !b2) { b2 = true; t = CTF_BLUE_FLAG_CARRYING;             t2 = CTF_BLUE_FLAG_TAKEN;               t3 = CTF_BLUE_FLAG_LOST; }
2140                 if(flag.team == NUM_TEAM_3 && !b3) { b3 = true; t = CTF_YELLOW_FLAG_CARRYING;   t2 = CTF_YELLOW_FLAG_TAKEN;             t3 = CTF_YELLOW_FLAG_LOST; }
2141                 if(flag.team == NUM_TEAM_4 && !b4) { b4 = true; t = CTF_PINK_FLAG_CARRYING;             t2 = CTF_PINK_FLAG_TAKEN;               t3 = CTF_PINK_FLAG_LOST; }
2142                 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; }
2143
2144                 switch(flag.ctf_status)
2145                 {
2146                         case FLAG_PASSING:
2147                         case FLAG_CARRY:
2148                         {
2149                                 if((flag.owner == player) || (flag.pass_sender == player))
2150                                         STAT(CTF_FLAGSTATUS, player) |= t; // carrying: player is currently carrying the flag
2151                                 else
2152                                         STAT(CTF_FLAGSTATUS, player) |= t2; // taken: someone else is carrying the flag
2153                                 break;
2154                         }
2155                         case FLAG_DROPPED:
2156                         {
2157                                 STAT(CTF_FLAGSTATUS, player) |= t3; // lost: the flag is dropped somewhere on the map
2158                                 break;
2159                         }
2160                 }
2161         }
2162
2163         // item for stopping players from capturing the flag too often
2164         if(player.ctf_captureshielded)
2165                 STAT(CTF_FLAGSTATUS, player) |= CTF_SHIELDED;
2166
2167         if(ctf_stalemate)
2168                 STAT(CTF_FLAGSTATUS, player) |= CTF_STALEMATE;
2169
2170         // update the health of the flag carrier waypointsprite
2171         if(player.wps_flagcarrier)
2172                 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);
2173 }
2174
2175 MUTATOR_HOOKFUNCTION(ctf, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
2176 {
2177         entity frag_attacker = M_ARGV(1, entity);
2178         entity frag_target = M_ARGV(2, entity);
2179         float frag_damage = M_ARGV(4, float);
2180         vector frag_force = M_ARGV(6, vector);
2181
2182         if(frag_attacker.flagcarried) // if the attacker is a flagcarrier
2183         {
2184                 if(frag_target == frag_attacker) // damage done to yourself
2185                 {
2186                         frag_damage *= autocvar_g_ctf_flagcarrier_selfdamagefactor;
2187                         frag_force *= autocvar_g_ctf_flagcarrier_selfforcefactor;
2188                 }
2189                 else // damage done to everyone else
2190                 {
2191                         frag_damage *= autocvar_g_ctf_flagcarrier_damagefactor;
2192                         frag_force *= autocvar_g_ctf_flagcarrier_forcefactor;
2193                 }
2194
2195                 M_ARGV(4, float) = frag_damage;
2196                 M_ARGV(6, vector) = frag_force;
2197         }
2198         else if(frag_target.flagcarried && !IS_DEAD(frag_target) && CTF_DIFFTEAM(frag_target, frag_attacker)) // if the target is a flagcarrier
2199         {
2200                 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
2201                         && time > frag_target.wps_helpme_time + autocvar_g_ctf_flagcarrier_auto_helpme_time)
2202                 {
2203                         frag_target.wps_helpme_time = time;
2204                         WaypointSprite_HelpMePing(frag_target.wps_flagcarrier);
2205                 }
2206                 // todo: add notification for when flag carrier needs help?
2207         }
2208 }
2209
2210 MUTATOR_HOOKFUNCTION(ctf, PlayerDies)
2211 {
2212         entity frag_attacker = M_ARGV(1, entity);
2213         entity frag_target = M_ARGV(2, entity);
2214
2215         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)) && (frag_target.flagcarried))
2216         {
2217                 GameRules_scoring_add_team(frag_attacker, SCORE, ((SAME_TEAM(frag_attacker, frag_target)) ? -autocvar_g_ctf_score_kill : autocvar_g_ctf_score_kill));
2218                 GameRules_scoring_add(frag_attacker, CTF_FCKILLS, 1);
2219         }
2220
2221         if(frag_target.flagcarried)
2222         {
2223                 entity tmp_entity = frag_target.flagcarried;
2224                 ctf_Handle_Throw(frag_target, NULL, DROP_NORMAL);
2225                 tmp_entity.ctf_dropper = NULL;
2226         }
2227 }
2228
2229 MUTATOR_HOOKFUNCTION(ctf, GiveFragsForKill)
2230 {
2231         M_ARGV(2, float) = 0; // frag score
2232         return (autocvar_g_ctf_ignore_frags); // no frags counted in ctf if this is true
2233 }
2234
2235 void ctf_RemovePlayer(entity player)
2236 {
2237         if(player.flagcarried)
2238                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2239
2240         for(entity flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2241         {
2242                 if(flag.pass_sender == player) { flag.pass_sender = NULL; }
2243                 if(flag.pass_target == player) { flag.pass_target = NULL; }
2244                 if(flag.ctf_dropper == player) { flag.ctf_dropper = NULL; }
2245         }
2246 }
2247
2248 MUTATOR_HOOKFUNCTION(ctf, MakePlayerObserver)
2249 {
2250         entity player = M_ARGV(0, entity);
2251
2252         ctf_RemovePlayer(player);
2253 }
2254
2255 MUTATOR_HOOKFUNCTION(ctf, ClientDisconnect)
2256 {
2257         entity player = M_ARGV(0, entity);
2258
2259         ctf_RemovePlayer(player);
2260 }
2261
2262 MUTATOR_HOOKFUNCTION(ctf, ClientConnect)
2263 {
2264         if(!autocvar_g_ctf_leaderboard)
2265                 return;
2266
2267         entity player = M_ARGV(0, entity);
2268
2269         if(IS_REAL_CLIENT(player))
2270         {
2271                 int m = min(RANKINGS_CNT, autocvar_g_cts_send_rankings_cnt);
2272                 race_send_rankings_cnt(MSG_ONE);
2273                 for (int i = 1; i <= m; ++i)
2274                 {
2275                         race_SendRankings(i, 0, 0, MSG_ONE);
2276                 }
2277         }
2278 }
2279
2280 MUTATOR_HOOKFUNCTION(ctf, GetPressedKeys)
2281 {
2282         if(!autocvar_g_ctf_leaderboard)
2283                 return;
2284
2285         entity player = M_ARGV(0, entity);
2286
2287         if(CS(player).cvar_cl_allow_uidtracking == 1 && CS(player).cvar_cl_allow_uid2name == 1)
2288         {
2289                 if (!player.stored_netname)
2290                         player.stored_netname = strzone(uid2name(player.crypto_idfp));
2291                 if(player.stored_netname != player.netname)
2292                 {
2293                         db_put(ServerProgsDB, strcat("/uid2name/", player.crypto_idfp), player.netname);
2294                         strcpy(player.stored_netname, player.netname);
2295                 }
2296         }
2297 }
2298
2299 MUTATOR_HOOKFUNCTION(ctf, PortalTeleport)
2300 {
2301         entity player = M_ARGV(0, entity);
2302
2303         if(player.flagcarried)
2304         if(!autocvar_g_ctf_portalteleport)
2305                 { ctf_Handle_Throw(player, NULL, DROP_NORMAL); }
2306 }
2307
2308 MUTATOR_HOOKFUNCTION(ctf, PlayerUseKey)
2309 {
2310         if(MUTATOR_RETURNVALUE || game_stopped) return;
2311
2312         entity player = M_ARGV(0, entity);
2313
2314         if((time > player.throw_antispam) && !IS_DEAD(player) && !player.speedrunning && (!player.vehicle || autocvar_g_ctf_allow_vehicle_touch))
2315         {
2316                 // pass the flag to a team mate
2317                 if(autocvar_g_ctf_pass)
2318                 {
2319                         entity head, closest_target = NULL;
2320                         head = WarpZone_FindRadius(player.origin, autocvar_g_ctf_pass_radius, true);
2321
2322                         while(head) // find the closest acceptable target to pass to
2323                         {
2324                                 if(IS_PLAYER(head) && !IS_DEAD(head))
2325                                 if(head != player && SAME_TEAM(head, player))
2326                                 if(!head.speedrunning && !head.vehicle)
2327                                 {
2328                                         // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc)
2329                                         vector head_center = WarpZone_UnTransformOrigin(head, CENTER_OR_VIEWOFS(head));
2330                                         vector passer_center = CENTER_OR_VIEWOFS(player);
2331
2332                                         if(ctf_CheckPassDirection(head_center, passer_center, player.v_angle, head.WarpZone_findradius_nearest))
2333                                         {
2334                                                 if(autocvar_g_ctf_pass_request && !player.flagcarried && head.flagcarried)
2335                                                 {
2336                                                         if(IS_BOT_CLIENT(head))
2337                                                         {
2338                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2339                                                                 ctf_Handle_Throw(head, player, DROP_PASS);
2340                                                         }
2341                                                         else
2342                                                         {
2343                                                                 Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_CTF_PASS_REQUESTED, player.netname);
2344                                                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_PASS_REQUESTING, head.netname);
2345                                                         }
2346                                                         player.throw_antispam = time + autocvar_g_ctf_pass_wait;
2347                                                         return true;
2348                                                 }
2349                                                 else if(player.flagcarried && !head.flagcarried)
2350                                                 {
2351                                                         if(closest_target)
2352                                                         {
2353                                                                 vector closest_target_center = WarpZone_UnTransformOrigin(closest_target, CENTER_OR_VIEWOFS(closest_target));
2354                                                                 if(vlen2(passer_center - head_center) < vlen2(passer_center - closest_target_center))
2355                                                                         { closest_target = head; }
2356                                                         }
2357                                                         else { closest_target = head; }
2358                                                 }
2359                                         }
2360                                 }
2361                                 head = head.chain;
2362                         }
2363
2364                         if(closest_target) { ctf_Handle_Throw(player, closest_target, DROP_PASS); return true; }
2365                 }
2366
2367                 // throw the flag in front of you
2368                 if(autocvar_g_ctf_throw && player.flagcarried)
2369                 {
2370                         if(player.throw_count == -1)
2371                         {
2372                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_delay)
2373                                 {
2374                                         player.throw_prevtime = time;
2375                                         player.throw_count = 1;
2376                                         ctf_Handle_Throw(player, NULL, DROP_THROW);
2377                                         return true;
2378                                 }
2379                                 else
2380                                 {
2381                                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CTF_FLAG_THROW_PUNISH, rint((player.throw_prevtime + autocvar_g_ctf_throw_punish_delay) - time));
2382                                         return false;
2383                                 }
2384                         }
2385                         else
2386                         {
2387                                 if(time > player.throw_prevtime + autocvar_g_ctf_throw_punish_time) { player.throw_count = 1; }
2388                                 else { player.throw_count += 1; }
2389                                 if(player.throw_count >= autocvar_g_ctf_throw_punish_count) { player.throw_count = -1; }
2390
2391                                 player.throw_prevtime = time;
2392                                 ctf_Handle_Throw(player, NULL, DROP_THROW);
2393                                 return true;
2394                         }
2395                 }
2396         }
2397 }
2398
2399 MUTATOR_HOOKFUNCTION(ctf, HelpMePing)
2400 {
2401         entity player = M_ARGV(0, entity);
2402
2403         if(player.wps_flagcarrier) // update the flagcarrier waypointsprite with "NEEDING HELP" notification
2404         {
2405                 player.wps_helpme_time = time;
2406                 WaypointSprite_HelpMePing(player.wps_flagcarrier);
2407         }
2408         else // create a normal help me waypointsprite
2409         {
2410                 WaypointSprite_Spawn(WP_Helpme, waypointsprite_deployed_lifetime, waypointsprite_limitedrange, player, FLAG_WAYPOINT_OFFSET, NULL, player.team, player, wps_helpme, false, RADARICON_HELPME);
2411                 WaypointSprite_Ping(player.wps_helpme);
2412         }
2413
2414         return true;
2415 }
2416
2417 MUTATOR_HOOKFUNCTION(ctf, VehicleEnter)
2418 {
2419         entity player = M_ARGV(0, entity);
2420         entity veh = M_ARGV(1, entity);
2421
2422         if(player.flagcarried)
2423         {
2424                 if(!autocvar_g_ctf_allow_vehicle_carry && !autocvar_g_ctf_allow_vehicle_touch)
2425                 {
2426                         ctf_Handle_Throw(player, NULL, DROP_NORMAL);
2427                 }
2428                 else
2429                 {
2430                         player.flagcarried.nodrawtoclient = player; // hide the flag from the driver
2431                         setattachment(player.flagcarried, veh, "");
2432                         setorigin(player.flagcarried, VEHICLE_FLAG_OFFSET);
2433                         player.flagcarried.scale = VEHICLE_FLAG_SCALE;
2434                         //player.flagcarried.angles = '0 0 0';
2435                 }
2436                 return true;
2437         }
2438 }
2439
2440 MUTATOR_HOOKFUNCTION(ctf, VehicleExit)
2441 {
2442         entity player = M_ARGV(0, entity);
2443
2444         if(player.flagcarried)
2445         {
2446                 setattachment(player.flagcarried, player, "");
2447                 setorigin(player.flagcarried, FLAG_CARRY_OFFSET);
2448                 player.flagcarried.scale = FLAG_SCALE;
2449                 player.flagcarried.angles = '0 0 0';
2450                 player.flagcarried.nodrawtoclient = NULL;
2451                 return true;
2452         }
2453 }
2454
2455 MUTATOR_HOOKFUNCTION(ctf, AbortSpeedrun)
2456 {
2457         entity player = M_ARGV(0, entity);
2458
2459         if(player.flagcarried)
2460         {
2461                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_NUM(player.flagcarried.team, INFO_CTF_FLAGRETURN_ABORTRUN));
2462                 ctf_RespawnFlag(player.flagcarried);
2463                 return true;
2464         }
2465 }
2466
2467 MUTATOR_HOOKFUNCTION(ctf, MatchEnd)
2468 {
2469         entity flag; // temporary entity for the search method
2470
2471         for(flag = ctf_worldflaglist; flag; flag = flag.ctf_worldflagnext)
2472         {
2473                 switch(flag.ctf_status)
2474                 {
2475                         case FLAG_DROPPED:
2476                         case FLAG_PASSING:
2477                         {
2478                                 // lock the flag, game is over
2479                                 set_movetype(flag, MOVETYPE_NONE);
2480                                 flag.takedamage = DAMAGE_NO;
2481                                 flag.solid = SOLID_NOT;
2482                                 flag.nextthink = false; // stop thinking
2483
2484                                 //dprint("stopping the ", flag.netname, " from moving.\n");
2485                                 break;
2486                         }
2487
2488                         default:
2489                         case FLAG_BASE:
2490                         case FLAG_CARRY:
2491                         {
2492                                 // do nothing for these flags
2493                                 break;
2494                         }
2495                 }
2496         }
2497 }
2498
2499 MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
2500 {
2501         entity bot = M_ARGV(0, entity);
2502
2503         havocbot_ctf_reset_role(bot);
2504         return true;
2505 }
2506
2507 MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
2508 {
2509         M_ARGV(1, string) = "ctf_team";
2510 }
2511
2512 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
2513 {
2514         entity spectatee = M_ARGV(0, entity);
2515         entity client = M_ARGV(1, entity);
2516
2517         STAT(CTF_FLAGSTATUS, client) = STAT(CTF_FLAGSTATUS, spectatee);
2518 }
2519
2520 MUTATOR_HOOKFUNCTION(ctf, GetRecords)
2521 {
2522         int record_page = M_ARGV(0, int);
2523         string ret_string = M_ARGV(1, string);
2524
2525         for(int i = record_page * 200; i < MapInfo_count && i < record_page * 200 + 200; ++i)
2526         {
2527                 if (MapInfo_Get_ByID(i))
2528                 {
2529                         float r = stof(db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/time")));
2530
2531                         if(!r)
2532                                 continue;
2533
2534                         // TODO: uid2name
2535                         string h = db_get(ServerProgsDB, strcat(MapInfo_Map_bspname, "/captimerecord/netname"));
2536                         ret_string = strcat(ret_string, strpad(32, MapInfo_Map_bspname), " ", strpad(-6, ftos_decimals(r, 2)), " ", h, "\n");
2537                 }
2538         }
2539
2540         M_ARGV(1, string) = ret_string;
2541 }
2542
2543 bool superspec_Spectate(entity this, entity targ); // TODO
2544 void superspec_msg(string _center_title, string _con_title, entity _to, string _msg, float _spamlevel); // TODO
2545 MUTATOR_HOOKFUNCTION(ctf, SV_ParseClientCommand)
2546 {
2547         entity player = M_ARGV(0, entity);
2548         string cmd_name = M_ARGV(1, string);
2549         int cmd_argc = M_ARGV(2, int);
2550
2551         if(IS_PLAYER(player) || MUTATOR_RETURNVALUE || !cvar("g_superspectate")) { return false; }
2552
2553         if(cmd_name == "followfc")
2554         {
2555                 if(!g_ctf)
2556                         return true;
2557
2558                 int _team = 0;
2559                 bool found = false;
2560
2561                 if(cmd_argc == 2)
2562                 {
2563                         switch(argv(1))
2564                         {
2565                                 case "red":    if(ctf_teams & BIT(0)) _team = NUM_TEAM_1; break;
2566                                 case "blue":   if(ctf_teams & BIT(1)) _team = NUM_TEAM_2; break;
2567                                 case "yellow": if(ctf_teams & BIT(2)) _team = NUM_TEAM_3; break;
2568                                 case "pink":   if(ctf_teams & BIT(3)) _team = NUM_TEAM_4; break;
2569                         }
2570                 }
2571
2572                 FOREACH_CLIENT(IS_PLAYER(it), {
2573                         if(it.flagcarried && (it.team == _team || _team == 0))
2574                         {
2575                                 found = true;
2576                                 if(_team == 0 && IS_SPEC(player) && player.enemy == it)
2577                                         continue; // already spectating this fc, try another
2578                                 return superspec_Spectate(player, it);
2579                         }
2580                 });
2581
2582                 if(!found)
2583                         superspec_msg("", "", player, "No active flag carrier\n", 1);
2584                 return true;
2585         }
2586 }
2587
2588 MUTATOR_HOOKFUNCTION(ctf, DropSpecialItems)
2589 {
2590         entity frag_target = M_ARGV(0, entity);
2591
2592         if(frag_target.flagcarried)
2593                 ctf_Handle_Throw(frag_target, NULL, DROP_THROW);
2594 }
2595
2596
2597 // ==========
2598 // Spawnfuncs
2599 // ==========
2600
2601 /*QUAKED spawnfunc_item_flag_team1 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2602 CTF flag for team one (Red).
2603 Keys:
2604 "angle" Angle the flag will point (minus 90 degrees)...
2605 "model" model to use, note this needs red and blue as skins 0 and 1...
2606 "noise" sound played when flag is picked up...
2607 "noise1" sound played when flag is returned by a teammate...
2608 "noise2" sound played when flag is captured...
2609 "noise3" sound played when flag is lost in the field and respawns itself...
2610 "noise4" sound played when flag is dropped by a player...
2611 "noise5" sound played when flag touches the ground... */
2612 spawnfunc(item_flag_team1)
2613 {
2614         if(!g_ctf) { delete(this); return; }
2615
2616         ctf_FlagSetup(NUM_TEAM_1, this);
2617 }
2618
2619 /*QUAKED spawnfunc_item_flag_team2 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2620 CTF flag for team two (Blue).
2621 Keys:
2622 "angle" Angle the flag will point (minus 90 degrees)...
2623 "model" model to use, note this needs red and blue as skins 0 and 1...
2624 "noise" sound played when flag is picked up...
2625 "noise1" sound played when flag is returned by a teammate...
2626 "noise2" sound played when flag is captured...
2627 "noise3" sound played when flag is lost in the field and respawns itself...
2628 "noise4" sound played when flag is dropped by a player...
2629 "noise5" sound played when flag touches the ground... */
2630 spawnfunc(item_flag_team2)
2631 {
2632         if(!g_ctf) { delete(this); return; }
2633
2634         ctf_FlagSetup(NUM_TEAM_2, this);
2635 }
2636
2637 /*QUAKED spawnfunc_item_flag_team3 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2638 CTF flag for team three (Yellow).
2639 Keys:
2640 "angle" Angle the flag will point (minus 90 degrees)...
2641 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2642 "noise" sound played when flag is picked up...
2643 "noise1" sound played when flag is returned by a teammate...
2644 "noise2" sound played when flag is captured...
2645 "noise3" sound played when flag is lost in the field and respawns itself...
2646 "noise4" sound played when flag is dropped by a player...
2647 "noise5" sound played when flag touches the ground... */
2648 spawnfunc(item_flag_team3)
2649 {
2650         if(!g_ctf) { delete(this); return; }
2651
2652         ctf_FlagSetup(NUM_TEAM_3, this);
2653 }
2654
2655 /*QUAKED spawnfunc_item_flag_team4 (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2656 CTF flag for team four (Pink).
2657 Keys:
2658 "angle" Angle the flag will point (minus 90 degrees)...
2659 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2660 "noise" sound played when flag is picked up...
2661 "noise1" sound played when flag is returned by a teammate...
2662 "noise2" sound played when flag is captured...
2663 "noise3" sound played when flag is lost in the field and respawns itself...
2664 "noise4" sound played when flag is dropped by a player...
2665 "noise5" sound played when flag touches the ground... */
2666 spawnfunc(item_flag_team4)
2667 {
2668         if(!g_ctf) { delete(this); return; }
2669
2670         ctf_FlagSetup(NUM_TEAM_4, this);
2671 }
2672
2673 /*QUAKED spawnfunc_item_flag_neutral (0 0.5 0.8) (-48 -48 -37) (48 48 37)
2674 CTF flag (Neutral).
2675 Keys:
2676 "angle" Angle the flag will point (minus 90 degrees)...
2677 "model" model to use, note this needs red, blue yellow and pink as skins 0, 1, 2 and 3...
2678 "noise" sound played when flag is picked up...
2679 "noise1" sound played when flag is returned by a teammate...
2680 "noise2" sound played when flag is captured...
2681 "noise3" sound played when flag is lost in the field and respawns itself...
2682 "noise4" sound played when flag is dropped by a player...
2683 "noise5" sound played when flag touches the ground... */
2684 spawnfunc(item_flag_neutral)
2685 {
2686         if(!g_ctf) { delete(this); return; }
2687         if(!cvar("g_ctf_oneflag")) { delete(this); return; }
2688
2689         ctf_FlagSetup(0, this);
2690 }
2691
2692 /*QUAKED spawnfunc_ctf_team (0 .5 .8) (-16 -16 -24) (16 16 32)
2693 Team declaration for CTF gameplay, this allows you to decide what team names and control point models are used in your map.
2694 Note: If you use spawnfunc_ctf_team entities you must define at least 2!  However, unlike domination, you don't need to make a blank one too.
2695 Keys:
2696 "netname" Name of the team (for example Red, Blue, Green, Yellow, Life, Death, Offense, Defense, etc)...
2697 "cnt" Scoreboard color of the team (for example 4 is red and 13 is blue)... */
2698 spawnfunc(ctf_team)
2699 {
2700         if(!g_ctf) { delete(this); return; }
2701
2702         this.classname = "ctf_team";
2703         this.team = this.cnt + 1;
2704 }
2705
2706 // compatibility for quake maps
2707 spawnfunc(team_CTF_redflag)    { spawnfunc_item_flag_team1(this);    }
2708 spawnfunc(team_CTF_blueflag)   { spawnfunc_item_flag_team2(this);    }
2709 spawnfunc(info_player_team1);
2710 spawnfunc(team_CTF_redplayer)  { spawnfunc_info_player_team1(this);  }
2711 spawnfunc(team_CTF_redspawn)   { spawnfunc_info_player_team1(this);  }
2712 spawnfunc(info_player_team2);
2713 spawnfunc(team_CTF_blueplayer) { spawnfunc_info_player_team2(this);  }
2714 spawnfunc(team_CTF_bluespawn)  { spawnfunc_info_player_team2(this);  }
2715
2716 spawnfunc(team_CTF_neutralflag) { spawnfunc_item_flag_neutral(this);  }
2717 spawnfunc(team_neutralobelisk)  { spawnfunc_item_flag_neutral(this);  }
2718
2719 // compatibility for wop maps
2720 spawnfunc(team_redplayer)      { spawnfunc_info_player_team1(this);  }
2721 spawnfunc(team_blueplayer)     { spawnfunc_info_player_team2(this);  }
2722 spawnfunc(team_ctl_redlolly)   { spawnfunc_item_flag_team1(this);    }
2723 spawnfunc(team_CTL_redlolly)   { spawnfunc_item_flag_team1(this);    }
2724 spawnfunc(team_ctl_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2725 spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
2726
2727
2728 // ==============
2729 // Initialization
2730 // ==============
2731
2732 // scoreboard setup
2733 void ctf_ScoreRules(int teams)
2734 {
2735         GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
2736         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
2737         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
2738         field(SP_CTF_CAPTIME, "captime", SFL_LOWER_IS_BETTER | SFL_TIME);
2739         field(SP_CTF_PICKUPS, "pickups", 0);
2740         field(SP_CTF_FCKILLS, "fckills", 0);
2741         field(SP_CTF_RETURNS, "returns", 0);
2742         field(SP_CTF_DROPS, "drops", SFL_LOWER_IS_BETTER);
2743         });
2744 }
2745
2746 // code from here on is just to support maps that don't have flag and team entities
2747 void ctf_SpawnTeam (string teamname, int teamcolor)
2748 {
2749         entity this = new_pure(ctf_team);
2750         this.netname = teamname;
2751         this.cnt = teamcolor - 1;
2752         this.spawnfunc_checked = true;
2753         this.team = teamcolor;
2754 }
2755
2756 void ctf_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up.
2757 {
2758         ctf_teams = 0;
2759
2760         entity tmp_entity;
2761         for(tmp_entity = ctf_worldflaglist; tmp_entity; tmp_entity = tmp_entity.ctf_worldflagnext)
2762         {
2763                 //if(tmp_entity.team == NUM_TEAM_3) { ctf_teams = max(3, ctf_teams); }
2764                 //if(tmp_entity.team == NUM_TEAM_4) { ctf_teams = max(4, ctf_teams); }
2765
2766                 switch(tmp_entity.team)
2767                 {
2768                         case NUM_TEAM_1: BITSET_ASSIGN(ctf_teams, BIT(0)); break;
2769                         case NUM_TEAM_2: BITSET_ASSIGN(ctf_teams, BIT(1)); break;
2770                         case NUM_TEAM_3: BITSET_ASSIGN(ctf_teams, BIT(2)); break;
2771                         case NUM_TEAM_4: BITSET_ASSIGN(ctf_teams, BIT(3)); break;
2772                 }
2773                 if(tmp_entity.team == 0) { ctf_oneflag = true; }
2774         }
2775
2776         havocbot_ctf_calculate_middlepoint();
2777
2778         if(NumTeams(ctf_teams) < 2) // somehow, there's not enough flags!
2779         {
2780                 ctf_teams = 0; // so set the default red and blue teams
2781                 BITSET_ASSIGN(ctf_teams, BIT(0));
2782                 BITSET_ASSIGN(ctf_teams, BIT(1));
2783         }
2784
2785         //ctf_teams = bound(2, ctf_teams, 4);
2786
2787         // if no teams are found, spawn defaults
2788         if(find(NULL, classname, "ctf_team") == NULL)
2789         {
2790                 LOG_TRACE("No \"ctf_team\" entities found on this map, creating them anyway.");
2791                 if(ctf_teams & BIT(0))
2792                         ctf_SpawnTeam("Red", NUM_TEAM_1);
2793                 if(ctf_teams & BIT(1))
2794                         ctf_SpawnTeam("Blue", NUM_TEAM_2);
2795                 if(ctf_teams & BIT(2))
2796                         ctf_SpawnTeam("Yellow", NUM_TEAM_3);
2797                 if(ctf_teams & BIT(3))
2798                         ctf_SpawnTeam("Pink", NUM_TEAM_4);
2799         }
2800
2801         ctf_ScoreRules(ctf_teams);
2802 }
2803
2804 void ctf_Initialize()
2805 {
2806         ctf_captimerecord = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time")));
2807
2808         ctf_captureshield_min_negscore = autocvar_g_ctf_shield_min_negscore;
2809         ctf_captureshield_max_ratio = autocvar_g_ctf_shield_max_ratio;
2810         ctf_captureshield_force = autocvar_g_ctf_shield_force;
2811
2812         InitializeEntity(NULL, ctf_DelayedInit, INITPRIO_GAMETYPE);
2813 }